Planet Survivors

Your posts, emails and private messages have done their job again! I have decided to show you how to create a full game from scratch. I chose to pick a space / logical game combination because we will (hopefully) learn many new things and we won't need a year to finish the game. Most of the artwork and the models were created by the artists in my team, but you are welcome to get involved in this project if you have interesting ideas, models, and so on. The finished game will be released as freeware and you will get credit for your work; I'll give you more details about this in a thread posted at Conitec's forum.

This month I have created a big part of the code used for the menu, except the save / load system. You can start a game by clicking the 3D ship on the left side of the screen, you can move the mouse over the ship to start its engines, you can access the help panel, you can "Exit" the game and so on. Let's start discussing the code right away:

panel main_pan
{
    bmap = main_tga;
    layer = 20;
    pos_x = 0;
    pos_y = 0;
    button = 397, 475, exit2_tga, exit1_tga, exit2_tga, exit_game, null, mouse_over;
    button = 559, 431, help2_tga, help1_tga, help2_tga, show_help, null, mouse_over;
    button = 647, 329, load2_tga, load1_tga, load2_tga, load_game, null, mouse_over;
    button = 681, 226, save2_tga, save1_tga, save2_tga, save_game, null, mouse_over;
    flags = overlay, refresh, visible;
}

The definition used for the main panel is typical; it contains four buttons that exit the game, show the help panel, load or save a game.

panel help_pan
{
   bmap = help_tga;
   layer = 20;
   pos_x = 0;
   pos_y = 601;
   on_click = show_main;
   flags = overlay, refresh, visible;
}

The help panel is, as you can see, "under construction" because we don't know what to write on it yet. There are only 2 things that need to be discussed here: we have an initial pos_y = 601, which keeps the panel outside the screen for now, and a function named show_main that is run every time we click the help panel.

function main()
{
    fps_max = 50;
    level_load (intro_wmb);
    wait (3);
    time_smooth = 0.9;
    mouse_spot.x = 31;
    mouse_spot.y = 17;
    camera.x = 0;
    camera.y = 0;
    camera.z = 350;
    camera.tilt = -90;
    mouse_mode = 2;

Function main limits the frame rate to 50 fps and then loads the intro_wmb level. This is a simple cube textured with a black texture; I have included the planets.wad file as well, so make sure that you copy it inside your \wads folder. We wait until the level is loaded, and then we set time_smooth to a value that is the average of the last 10 frames. I have used a big bitmap for the crosshair, so I had to choose convenient mouse_spot.x and mouse_spot.y values. The "hot spot" gives the position of the active pixel on the crosshair; that's the pixel that triggers all the events for the mouse.

The last few lines of code choose a convenient camera position and make it look downwards (tilt = -90), and then show the mouse pointer.

    while (game_started == 0) // as long as we haven't started the game
    {
       mouse_pos.x = pointer.x;
       mouse_pos.y = pointer.y;
       if (random (1) > 0.95) // use the second bitmap for the pointer every 20 frames (or so)
       {
            mouse_map = pointer2_tga;
       }
       else
       {
            mouse_map = pointer1_tga;
       }
       wait (1);
    }
}

I have defined a var named game_started; as long as the value for this var continues to be zero, we use pointer.x and pointer.y to update the position of our crosshair. If random(1) > 0.95, which could happen every 20 frames or so, we replace the pointer1_tga bitmap with its slightly modified brother named pointer2_tga; otherwise, we set back the original pointer1_tga. This small trick shows a little more activity on the screen.

Let's see the code that will be used by the player while the main menu is active:

action player_intro
{
    if (version >= 6.00) // using A6?
    {
       my.ambient = -50;
       my.roll = 6;
    }
    else // using A5
    {
       my.ambient = 80;
       my.roll = 20;
    }
    my.enable_touch = on;
    my.enable_release = on;
    my.enable_click = on;
    my.event = toggle_engines;
}

I have noticed that the models look pretty different in A5 and A6, so I chose to set different ambient values and roll angles for the ship depending on the version number of the engine. On a side note, you can see the ship through main_pan because the dark area of the main panel has RGB = 000. The ship is sensitive to touching (move the crosshair over it to trigger its event), releasing (move the crosshair away from the ship to trigger the event) and to clicking (click the ship to trigger the event). I'm using this event function for all the events:

function toggle_engines()
{
    if (event_type == event_touch)
    {
       my.engine = on;
       if (snd_playing (engine_handle) == 0)
       {
            engine_handle = snd_loop (engine1_wav, 10, 0);
       }
    }
    if (event_type == event_release)
    {
       snd_stop (engine_handle);
       my.engine = off;
    }
    if (event_type == event_click)
    {
       start_level1();
    }
    plasma_jet();
}

If we touch the ship, we set its "engine" (another name for flag1) to on; this flag will be used to start or stop the plasma jets (the engines) inside the particle function. If the engine1_wav sound isn't playing already, we play it in a loop using the snd_loop instruction.

If we move the crosshair away from the ship model, we stop the engine1_wav sound and we set the "engine" flag off. Finally, if we click the ship, function start_level1() is run. The last line of code inside our event function updates the status of the engines' it sets them to on or off, depending on "my.engine".

Let's see some simple functions now:

function mouse_over() // is executed when we are over the menu buttons
{
    snd_play (mouse_wav, 70, 0);
}

function exit_game()
{
   exit;
}

function load_game()
{
   beep;
}

function save_game()
{
   beep;
}

Function mouse_over() is executed when the crosshair is placed over one of the main menu buttons, and function exit_game() simply shuts down the engine. Load and save haven't been implemented yet, so these functions will generate a beep when you press the corresponding buttons.

function show_help()
{
   while (main_pan.pos_y > -600)
    {
       main_pan.pos_y -= 10;
       help_pan.pos_y = main_pan.pos_y + 600;
       wait (1);
    }
    snd_play (menu_wav, 20, 0);
}

Function show_help displays the help panel when we click the "help" button; it decreases pos_y for main_pan and for help_pan in a while loop, keeping the difference of 600 pixels on the y axis between them all the time. I chose "10" for the scrolling speed, which means that we subtract 10 pixels from pos_y every frame, but you can use any other value that divides 600: 5, 6, 8, 10, 12, 15, 20, 25, 30, ...

The scrolling stops when main_pan has disappeared and help_pan covers the entire screen. Now how can we get main_pan back when we need it?

function show_main()
{
    while (main_pan.pos_y < 0)
    {
       main_pan.pos_y += 10;
       help_pan.pos_y = main_pan.pos_y + 600;
       wait (1);
    }
    snd_play (menu_wav, 20, 0);
}

The function above runs when we click help_pan, and it does the same thing with show_help(), but the panels scroll in the opposite direction. The same menu_wav sound is played when the panels have stopped moving.

I'd like us to study the code used for the plasma jets (the engines) now, so let me show you the effect from a better angle.

function plasma_jet()
{
    proc_late();
    var engine_left;
    var engine_right;
    var jet_speed;
    while (my.engine == on)
    {
       vec_for_vertex (engine_left, my, 52);
       vec_for_vertex (engine_right, my, 54); // right engine vertex
       temp.pan = my.pan;
       temp.tilt = my.tilt;
       temp.roll = my.roll;
       jet_speed.x = -5;
       jet_speed.y = 0;
       jet_speed.z = 0;
       vec_rotate (jet_speed, temp);
       effect(particle_jet, 4, engine_left, jet_speed);
       effect(particle_jet, 4, engine_right, jet_speed);
       wait(1);
    }
}

The function above is called by the event function associated to the player. The first line of code makes sure that the function waits until engine_left and engine_right are set correctly, because they depend on the current position of the player. The particles will be generated for as long as my.engine (my.flag1) is set on for the player. The starting points for the particles are given by the vertices 52 and 54; these aren't the exact vertices that were created for the engines, but they look better when you see the model from a top view, and that's how we see it in our menu.

The size of the plasma jet is given by jet_speed.x; this vector is rotated by the angles stored in temp, the result being a particle jet that follows the ship correctly, regardless of its position and angles. We generate 4 particles at the same time for every engine, and we tell them to behave as instructed by function particle_jet()

function particle_jet()
{
    my.alpha = 10 + random(90);
    my.bmap = jet_tga;
    my.flare = on;
    my.bright = on;
    my.beam = on;
    my.move = on;
    my.size = 10;
    my.function = particle_fade;
}

Please note that the transparency factor can have any value from 10 to 100; this way we get a plasma jet that changes its length all the time. Don't worry if your engine can't use beam effects; the particles look pretty much the same without beaming, because the particles have a short life. The size of the particles is given by 10 and the function that drives every particle is particle_fade():

function particle_fade()
{
    my.alpha -= 20 * time;
    if (my.alpha <= 0)
    {
       my.lifespan = 0;
    }
}

The particles don't live too long; as soon as their alpha is smaller than or equal to zero, they are removed.

What happens when we click the ship model? The following function is run:

function start_level1()
{
    mouse_mode = 0;
    snd_stop (engine_handle);
    main_pan.visible = off;
    level_load (level1_wmb);
}

This function hides the mouse pointer, stops the engine sound, hides the main panel and then loads the first playable level; it's a white cube with a rotating ship inside it for now, but I promise to make it look much more interesting next month. Level1_wmb has its own script file associated with it (leve1.wdl) and this is where we find the code that rotates the new ship.

action enemy1
{
    while (1)
    {
       my.pan += 3 * time;
       my.roll += 2 * time;
       wait (1);
    }
}

Remember this important trick: every new level should have its own script file, even if you plan to use actions and functions that are included in the older scripts.

Don't forget that you are all invited to help me create and finish this game, so keep an eye on the forum. I'll see you all next month!

 

Elevator

This article teaches you how to create an elevator for a building with 3 stories; of course that you can expand the code if you have a taller building. Let's start with function main:

function main()
{
    on_d = null;
    level_load (elevator_wmb);
    mouse_map = pointer_pcx;
    mouse_mode = 0;
    while (1)
    {
       mouse_pos.x = pointer.x;
       mouse_pos.y = pointer.y;
       wait (1);
    }
}

We disable the "D" key (useful if you own A5) because we will use the "W", "S", "A", "D" keys to move. We load the level, we set the bitmap that will be used for the mouse pointer, but we hide it for now. Finally, we've got some standard mouse code that updates the position of the cursor every frame.

I said that we are going to hide the mouse pointer at game start, but we can show it anytime by pressing the right mouse button:

function toggle_crosshair()
{
    mouse_mode = (mouse_mode == 0) * 2;
}

on_mouse_right = toggle_crosshair;

This simple function sets mouse_mode to 0 or 2, depending on the number of clicks; this way, the pointer appears / disappears from the screen.

action player1
{
    player = my;
    my.invisible = on; // hide the player model
    while (1)
    {
       vec_set (camera.pos, my.pos);
       camera.z += 50;
       camera.pan = my.pan;
       camera.tilt += 10 * mouse_force.y * time;
       my.pan -= 20 * mouse_force.x * time;
       vec_set (temp, my.x);
       temp.z -= 3000;
       trace_mode = ignore_me + ignore_passable + use_box;
       temp.z = -trace (my.x, temp); // trace 3000 quants below player's feet
       temp.x = 10 * (key_w - key_s) * time * ready_for_clicks;
       temp.y = 8 * (key_a - key_d) * time * ready_for_clicks;
       ent_move (temp, nullvector);
       wait (1);
    }
}

The action attached to the player makes its model invisible; the camera will have the same x and y position with the player, but its z will be placed 50 quants above player's origin. The pan and tilt angles can be changed using the mouse; that's how the player can look around it. We are doing a trace 3000 quants below player's feet and we set its height according to the result of the trace. Finally, we allow the player to move forward / backward using the "W" and "S" keys; strafe with "A" and "D". Please note that temp.x and temp.y are multiplied with ready_for_clicks; this is a variable that can freeze the player if it is set to zero. Don't worry, we'll talk about this var really soon.

The elevator entity uses a simple action:

var floor1 = 39;
var floor2 = 520;
var floor3 = 1032;

action elevator
{
    my_elevator = my;
    my.z = floor1;
}

I have assigned the previously defined "my_elevator" pointer to the elevator entity, and I'm setting its height to the z coordinate that was chosen for the first floor. The values for floor1...floor3 are experimental; feel free to adjust them until they work ok for your level.

You won't be surprised to hear that the elevator includes 3 buttons; these are nothing more than small wmb entities that have this action attached to them:

entity* button_red;
entity* button_green;
entity* button_blue;

action button
{
    my.light = on;
    my.lightrange = 0;
    my.unlit = on;
    my.ambient = -100;
    if (my.skill1 == 1)
    {
       button_red = my;
       my.lightred = 255;
       my.lightgreen = 0;
       my.lightblue = 0;
    }
    if (my.skill1 == 2)
    {
       button_green = my;
       my.lightred = 0;
       my.lightgreen = 255;
       my.lightblue = 0;
    }
    if (my.skill1 == 3)
    {
       button_blue = my;
       my.lightred = 0;
       my.lightgreen = 0;
       my.lightblue = 255;
    }
    my.enable_click = on;
    my.event = button_clicked;
}

I have used a single wmb entity for all the buttons, and I have changed their appearance using distinct colored lights for every button. Don't forget to set skill1 to 1 for the red button, skill1 = 2 for green and skill1 = 3 for blue. All the buttons are sensitive to clicking and run their button_clicked event function as soon as the player presses them:

function button_clicked()
{
    if (ready_for_clicks == 0) {return;}
    ready_for_clicks = 0;
    if (my.skill1 == 1)
    {
       if (my_elevator.z == floor1)
       {
            ready_for_clicks = 1;
            return;
       }
       if ((my_elevator.z == floor2) || (my_elevator.z == floor3))
       {
            while (my_elevator.z > floor1)
            {
                 my_elevator.z -= 5 * time;
                 player.z = my_elevator.z + 48;
                button_red.z = my_elevator.z + 81;
                button_green.z = my_elevator.z + 105;
                button_blue.z = my_elevator.z + 129;
                wait (1);
            }
        }
       my_elevator.z = floor1;
    }

I have defined a variable named ready_for_clicks and I have set its value to 1. That variable will be set to zero as soon as one of the buttons is clicked, and will remain zero until the elevator stops; this way we don't allow the player to play with the elevator while it is moving. If we have pressed the red button (skill1 = 1) and the elevator is at the first floor already, we enable clicking again (ready_for_clicks = 1) and then we get out of the function. If the elevator is at the second or third floor, it must move to the first floor, so the while loop lowers its height until it reaches the height set for floor1.

The player is kept 48 quants above the floor level; that's an experimental value, because it depends on the height of the model. Funny thing is... we have to move the 3 buttons together with the elevator as well; otherwise, they would disappear and the player wouldn't be able to choose a new floor if he wants to! Once again 81, 105 and 129 are experimental values. The last line of code sets the height of the elevator to the value given by floor1; we need to correct the potential offsets.

    if (my.skill1 == 2)
    {
       if (my_elevator.z == floor2)
       {
            ready_for_clicks = 1;
            return; // do nothing
       }
       if (my_elevator.z == floor1)
       {
            while (my_elevator.z < floor2)
            {
                 my_elevator.z += 5 * time;
                 player.z = my_elevator.z + 48;
                 button_red.z = my_elevator.z + 81;
                 button_green.z = my_elevator.z + 105;
                 button_blue.z = my_elevator.z + 129;
                 wait (1);
            }
      }
     if (my_elevator.z == floor3)
    {
          while (my_elevator.z > floor2)
          {
               my_elevator.z -= 5 * time;
               player.z = my_elevator.z + 48;
               button_red.z = my_elevator.z + 81;
               button_green.z = my_elevator.z + 105;
               button_blue.z = my_elevator.z + 129;
               wait (1);
           }
      }
      my_elevator.z = floor2;
  }

If the player presses the second (green) button and the elevator is at the second floor already, the function returns. On the other hand, if the elevator is at the first floor, it will increase its z until it becomes bigger than or equal to floor2. If the elevator is at the third floor, it decreases its z until it becomes smaller than or equal to floor2. The last line of code sets the height of the elevator to the value given by floor2.

  if (my.skill1 == 3) // pressed the blue (3rd floor) button?
  {
     if (my_elevator.z == floor3) // if the elevator is at the third floor already
     {
         ready_for_clicks = 1; // enable clicking again
         return; // do nothing
     }
     if ((my_elevator.z == floor1) || (my_elevator.z == floor2)) // if the elevator is at the first or second floor
     {
         while (my_elevator.z < floor3)
         {
              my_elevator.z += 5 * time;
              player.z = my_elevator.z + 48;
              button_red.z = my_elevator.z + 81;
              button_green.z = my_elevator.z + 105;
              button_blue.z = my_elevator.z + 129;
              wait (1);
         }
     }
     my_elevator.z = floor3;
   }
  button_red.z = my_elevator.z + 81;
  button_green.z = my_elevator.z + 105;
  button_blue.z = my_elevator.z + 129;
  ready_for_clicks = 1;
}

If the player presses the third (blue) button and the elevator is at the third floor already, the function returns. If the elevator is at the first or second floor, it will increase its z until it becomes bigger than or equal to floor3. The last few lines of code keep the correct position of the buttons on the elevator, and the last line enables clicking again. Let's take a look at a few lines inside the action used for the player:

action player1
{
       ................................................................................
       temp.x = 10 * (key_w - key_s) * time * ready_for_clicks;
       temp.y = 8 * (key_a - key_d) * time * ready_for_clicks;
       ent_move (temp, nullvector);
       wait (1);
    }
}

Do you see what's happening here? When the elevator moves, ready_for_clicks is set to zero and the player can't move at all, because temp.x and temp.y are 0. As soon as the elevator stops, ready_for_clicks is set to 1 and the player is able to move again. That's a safe method to keep the player inside the elevator while it is moving; the camera can change its pan and tilt angles, so the player can look around if the mouse pointer is off, but it won't be able to move its feet until the elevator stops.