Code snippets

Top  Previous  Next

Stratego 2

 

If you've liked Stratego in Aum2, you'll love Stratego2 because if features:
- True 3D levels
- Selectable units (click)
- Multiple unit selection (drag a frame around the units)
- Different sounds for every unit

I will assume that you are familiar with Stratego so I won't explain how to create buildings, units and so on. All this stuff is explained in Aum2 - we'll concentrate on the new stuff.

Stratego 2 is a standalone project so it has got its own main function:

function main()
{
    level_load (stratego2_wmb);
    wait (2); // wait for the level to be loaded
    clip_size = 0; // show all the triangles for all the models
    fps_max = 40; // lock the frame rate
    camera.x = 0;
    camera.y = 0;
    camera.z = camera_height;
    camera.tilt = -90;
    camera.pan += 90;
    mouse_mode = 2;
    mouse_map = pointer_map;
    mouse_pos.x = screen_size.x / 2;
    mouse_pos.y = screen_size.y / 2;
    move_camera();
    multiple_selection();
    while (index < 9)
    {
         temp.x = start_pos[index];
         temp.y = start_pos[index + 1];
         temp.z = start_pos[index + 2];
         ent_create (guard_mdl, temp, init_unit);
         index += 3;
    }
}

The camera looks down because its tilt = -90; its pan is rotated with 90 degrees because it needs to point in the right direction. The mouse pointer is made visible, being placed in the center of the screen for now and then a few functions are initialized (we'll talk about them a little later). Three units (guard.mdl) are created; their starting positions are read from:

var start_pos[9] = 200, 200, 500, -200, 200, 500, 0, -200, 500;

The first guard is spawned at xyz = 200, 200, 500, the second at -200, 200, 500 and so on. Every guard runs function init_unit():

function init_unit()
{
    wait (1);
    my.enable_click = on;
    my.enable_entity = on;
    my.enable_block = on;
    my.event = move_unit;
    vec_set (temp, my.pos);
    temp.z -= 3000;
    trace_mode = ignore_me + ignore_sprites + ignore_models + use_box;
    my.z -= trace (my.pos, temp); // place the unit on the ground (it has been spawned in the air)
    my.target_x = 100000;
    while (1)
    {
         while (my.target_x < 50000) {wait (1);}
         if ((my.x > upleft_coords.x) && (my.x < upright_coords.x) && (my.y < upleft_coords.y) && (my.y > downleft_coords.y) && (my.unit_selected == 0))
         {
              my.unit_selected = 1;
              move_unit();
         }
         else
         {
              if (my.unit_selected == 1)
              {
                   vec_set (upleft_coords, nullvector); // reset selection coords
                   vec_set (upright_coords, nullvector);
                   vec_set (downleft_coords, nullvector);
                   vec_set (downright_coords, nullvector);
              }
         }
         wait (1);
    }
}

The unit will react on mouse clicks, collisions with level geometry and collision with other entities. The guards are generated up in the air (their z coord was set to 500 in the array, remember?) so we need to move them on the ground. We are tracing 3000 quants below the the unit and we're substracting the result from its initial z; my.target_x = 100000 simply says that the unit hasn't got any target yet (the coordinates are given in quants so 100000 quants is just a way of saying "out of range").

You'll be seeing a lot of my.target_x or my.unit_selected, etc in Stratego 2 but don't worry: these are skills that are defined at the beginning of stratego2.wdl: for example, my.target_x is the same thing with my.skill12 but it looks better this way, isn't it?

Every unit has a while(1) loop that keeps running even if the unit if performing other task; if unit's target_x is bigger than 50000 (we agreed that target_x = 100000 means no target, right?) and the unit was selected (together with other entities or not) by dragging a frame around it, the unit will be selected (my.unit_selected = 1) and it will start moving towards its (yet to be set) target. As soon as the unit is selected, the selection coords are reset. Let's talk a little about these coords:

function multiple_selection()
{
    while (1)
    {
         if (mouse_left == 1)
         {
              upper_left.visible = on;
              upper_right.visible = on;
              lower_left.visible = on;
              lower_right.visible = on;
              if (first_click == 0) // make sure that this "if" branch is executed only once
              {
                   first_click = 1;
                   upper_left.pos_x = pointer.x; // store panel's position
                   upper_left.pos_y = pointer.y;
              }
              lower_right.pos_x = pointer.x; // store panel's position
              lower_right.pos_y = pointer.y;

              lower_left.pos_x = upper_left.pos_x; // store panel's position
              lower_left.pos_y = lower_right.pos_y;

              upper_right.pos_x = lower_right.pos_x; // store panel's position
              upper_right.pos_y = upper_left.pos_y;

         }
         else // finished multiple selection
         {
              upper_left.visible = off;
              upper_right.visible = off;
              lower_left.visible = off;
              lower_right.visible = off;
              first_click = 0;

              upleft_coords.x = upper_left.pos_x; // project upper_left's panel coords on the map
              upleft_coords.y = upper_left.pos_y;
              upleft_coords.z = camera_height;
              vec_for_screen (upleft_coords, camera);

              upright_coords.x = upper_right.pos_x; // project upper_left's panel coords on the map
              upright_coords.y = upper_right.pos_y;
              upright_coords.z = camera_height;
              vec_for_screen (upright_coords, camera);

              downleft_coords.x = lower_left.pos_x; // project upper_left's panel coords on the map
              downleft_coords.y = lower_left.pos_y;
              downleft_coords.z = camera_height;
              vec_for_screen (downleft_coords, camera);

              downright_coords.x = lower_right.pos_x; // project upper_left's panel coords on the map
              downright_coords.y = lower_right.pos_y;
              downright_coords.z = camera_height;
              vec_for_screen (downright_coords, camera);
         }
         wait (1);
    }
}

Function multiple_selection() sets the coords that are needed by every unit to see if it has been selected (or not) by dragging a frame around it. Let's take a look at this picture:


When we click on the map (mouse_left = 1) four simple panels (upper_left, upper_right, lower_left, lower_right) are displayed on the screen. These panels are the little green ones in the picture above. The first panel (upper_left) appears at pointer's position and remains there; the rest of the panels will change their positions on screen depending on pointer's movement in order to "draw" frames of different widths / heights. Of course that we could use a single, square-sized "entity" panel and change its scale on x and y. 

When the mouse is released (the frame has been "drawn" around the units) we're moving to the "else" branch: the panels are made invisible and the coordinates for the four corners are translated to level geometry coords using vec_for_screen, like in this picture:

At this point we have created a virtual frame that has the same size with the one that was drawn over the screen; the code in init_unit checks if the corresponding unit is placed inside this virtual frame:

if ((my.x > upleft_coords.x) && (my.x < upright_coords.x) && (my.y < upleft_coords.y) && (my.y > downleft_coords.y) && (my.unit_selected == 0))
{
..............
}


In my example all the units are placed inside the virtual frame so all of them will be selected.

Here's the scary function move_unit():

function move_unit()
{
    if (event_type == event_block || event_type == event_entity)  // collision with other entities or level geometry
    {
         if (abs(my.x - my.target_x) + abs(my.y - my.target_y) < 100) // close to the target but other entities are already there
         {
              my.target_reached = 1; // the unit has reached the target
              my.unit_selected = 0;
              my.target_x += 100000; // move the target far away to get out of this "if" branch
         }
         else
         {
              // caveman's path finding code - read more about it in Aum2
              my.pan += 90 - random(180);
              waitt (10); // wait a little
              temp.x = my.target_x; // stored mouse pointer coordinates
              temp.y = my.target_y;
              temp.z = 0;
              vec_sub (temp, my.x);
              vec_to_angle (my.pan, temp); // rotate unit towards the target again
              my.tilt = 0; // stand tall :)
         }
    }
    if (event_type == event_click || my.unit_selected == 1)
    {
         my.destination = 0;
         my.target_reached = 0; // target not available yet
         ent_create (selected_pcx, my.pos, selected_unit);
         my.selected_handle = snd_play (selected_snd, 70, 0);
         snd_tune (my.selected_handle, 70, 80 + random(20), 0); // different "huh?" voices
         while (mouse_left == 1) {wait(1);} // wait for the mouse release
         while (my.destination == 0) // as long as the target hasn't been set
         {
              if (mouse_left == 1)
              {
                   mouse_map = pointerhigh_map;
                   my.ok_handle = snd_play (ok_snd, 70, 0); // store the sound handle in a skill, no need to use a separate var
                   snd_tune (my.ok_handle, 70, 80 + random(50), 0); // different "ok" voices
                   waitt (4); // show the cross for 0.25 seconds
                   mouse_map = pointer_map;
                   my.destination = 1;
                   temp.x = mouse_pos.x;
                   temp.y = mouse_pos.y;
                   temp.z = camera_height;
                   vec_for_screen (temp, camera); // temp holds pointer's coords now
                   my.target_x = temp.x; // store pointer's coords before they get lost
                   my.target_y = temp.y;
                   vec_sub (temp, my.x);
                   vec_to_angle (my.pan, temp); // rotate the unit towards the target
                   my.tilt = 0; // we only need the correct "pan" angle, not tilt
                   my.fuel = 300; // maximum path length (fuel)
                   while (abs(my.x - my.target_x) + abs(my.y - my.target_y) > 3 && my.fuel > 0 && my.target_reached != 1) // stop near the target
                   {
                        my.unit_selected = 0;
                        vec_set (temp, my.x);
                        temp.z -= 3000;
                        trace_mode = ignore_me + ignore_sprites + ignore_models + use_box;
                        unit_speed.z = -trace (my.x, temp);

                        move_mode = ignore_you + ignore_passable;
                        ent_move (unit_speed, nullvector);
                        ent_cycle("walk", my.animation_frame); // play walk frames animation
                        my.animation_frame += 5 * time; // "walk" animation speed
                        if (my.animation_frame > 100) {my.animation_frame = 0;} // loop animation
                        my.fuel -= time; // burn fuel
                        wait (1);
                   }
                   my.target_reached = 1; // the unit has reached the target
                   my.target_x += 100000;
                   ent_cycle("stand", 0); // the unit stands still now
              }
             wait (1);
         }
    }
}

This function will run if the unit is clicked on (its event_click is triggered) or if my.unit_selected = 1 (the unit has been selected by dragging a frame around it). If one of these things happen, we create a green sprite that surrounds the unit, play a "huh?" sound with a random frequency (that's why we use snd_tune and random) and then we wait until the left mouse button is released. Now we have to set the destination: if we click somewhere on the map, the pointer changes to pointerhigh_map (a cross), the "ok" sound - again with a different frequency for every unit - is played and the mouse pointer is set back to normal. Another vec_for_screen instruction will convert the mouse coordinates to a position in the level - that's the target for our units! The guards will rotate towards this target and move towards it as long as the distance between them and the target is smaller than 3.

On the other hand, the target might be unreachable for a certain unit; if this is the case, the unit should stop after a certain period of time. This is why I am using my.fuel: the unit starts with fuel = 300 and burns it as it moves in the level. When fuel < 0 the unit will stop even if it hasn't reached the target.

I'm using a trace again because I have to keep the units on the ground all the time; the units are tracing only when they're moving. When the unit has reached the target (or ran out of fuel) its target_x is set to a value around 100000 (which means that it has no target) and its target_reached flag is set.

    if (event_type == event_block || event_type == event_entity)  // collision with other entities or level geometry
    {
         if (abs(my.x - my.target_x) + abs(my.y - my.target_y) < 100) // close to the target but other entities are already there
         {
              my.target_reached = 1; // the unit has reached the target
              my.unit_selected = 0;
              my.target_x += 100000; // move the target far away to get out of this "if" branch
         }
         else
         {
              // caveman's path finding code - read more about it in Aum2
              my.pan += 90 - random(180);
              waitt (10); // wait a little
              temp.x = my.target_x; // stored mouse pointer coordinates
              temp.y = my.target_y;
              temp.z = 0;
              vec_sub (temp, my.x);
              vec_to_angle (my.pan, temp); // rotate unit towards the target again
              my.tilt = 0; // stand tall :)
         }
    }

I'm not sure if you've missed the lines above in move_unit - I haven't. These lines are useful when the unit collides with another entity or with a level block. If the target is really close when the collision occurs, it's quite sure that another unit has reached the target already. All the units have the same target (if more than one entity is selected) so the unit that has reached the target on the 2nd, 3rd, etc place will stop close to the target, as soon as it "touches" another entity in that area. If the unit has collided with a block or another unit but it is far from the target, its pan is changed, the unit walks for a few frames in another direction and then it tries to reach the target again.

function selected_unit()
{
    my.unlit = on; // shouldn't be affected by the lights in the level
    my.ambient = 100; // make it bright
    my.oriented = on;
    my.passable = on;
    my.tilt = 90;
    while (you.target_reached == 0) // as long as the target hasn't been reached
    {
         vec_set (my.pos, you.pos); // move with the unit
         wait (1);
    }
    ent_remove (me); // the target has been reached so the square has to disappear
}

Function selected_unit moves the green sprote that surrounds the player until the unit reached its target, and then it removes the green sprite.

The last function is taken from Aum2:

function move_camera()
{
    waitt (4); // wait for the level to load
    level_marginx = 1000; // the map has 2000 quants on x (-1000...+1000)
    level_marginy = 1000; // and on y (-1000...+1000)
    while (1)
    {
         mouse_pos.x = pointer.x;
         mouse_pos.y = pointer.y;
         if (mouse_pos.x < 1 && camera.x > level_marginx * (-1)) {camera.x -= 10 * time;}
         if (mouse_pos.x > screen_size.x - 2 && camera.x < level_marginx) {camera.x += 10 * time;}
         if (mouse_pos.y > screen_size.y - 2 && camera.y > level_marginy * (-1)) {camera.y -= 10 * time;}
         if (mouse_pos.y < 1 && camera.y < level_marginy) {camera.y += 10 * time;}
         wait (1);
    }
}

You can set the level boundaries (as far as the camera is concerned) with level_marginx and level_marginy and adjust its scrolling speed by replacing 10 with smaller or bigger values.

I can see that Stratego1 & 2 could be used to create a good strategy game; the only things that weren't covered in Aum so far are AI (pathfinding) and close combat. Start your strategy game today - I'll make sure that you get these additions in a few months :)

Turrets

These mean turrets can kill you in a second or two because they were trained to do this since they were 5 years old. Take a look at the action that is attached to them:

action turret
{
    if (my.skill1 == 0) {my.skill1 = 1000;} // set the range with skill1, default = 1000 quants
    if (my.skill2 == 0) {my.skill2 = 180;} // default viewing angle in front of the turret = 180
    my.enable_impact = on;
    my.event = destroy_turret;
    while (player == null) {wait (1);}
    while (my != null && player != null)
    {
         my.skill10 = abs(ang(player.pan) - ang(my.pan));
         if (vec_dist (my.x, player.x) < my.skill1)
         {
              if ((my.skill10 > 180 - my.skill2 / 2) && (my.skill10 < 180 + my.skill2 / 2))
              {
                   vec_set (temp.x, player.x);
                   vec_sub (temp.x, my.x);
                   vec_to_angle (my.pan, temp);
                   ent_create (bullet_mdl, my.pos, move_bullet);
              }
         }
         waitt (8); // fires 2 bullets a second
    }
}

The turrets have adjustable range and viewing angle; if you forget to set skill1 and skill2 in Wed they'll use the default values. If the unit is hit by a bullet, it will be destroyed; we'll talk about that a little later. As long as the turret and the player are "alive", the turret computes the distance to the player, rotates towards it and fires 2 bullets a second.

my.skill10 = abs(ang(player.pan) - ang(my.pan));
................................
if ((my.skill10 > 180 - my.skill2 / 2) && (my.skill10 < 180 + my.skill2 / 2))

These two lines make sure that the turret and the player see each other; I could have used a scan_entity instruction as well. The turret can "see" the player if the angle between the player and the turret ranges from 180 - skill2 / 2 .... 180 + skill2 / 2. If we are using the default values for skill2, the turret will see the player is the angle between them is between 90 and 270 degrees.

function destroy_turret()
{
    wait (1);
    if (you == player) {return;} // can't be destroyed by running into it
    snd_play (explode_snd, 70, 0);
    ent_remove (me);
}

If the turret impacts with something, it will be destroyer. We don't want to allow the player to destroy the turret by simply running into it so if (you == player) nothing happens. The function that moves turret's bullet is typical and I have explained how it works several times in previous Aum editions:

function move_bullet()
{
    wait (1);
    my.enable_entity = on;
    my.enable_block = on;
    my.event = remove_bullet;
    my.passable = on;
    my.pan = you.pan;
    bullet_speed.x = 100;
    bullet_speed.y = 0;
    bullet_speed.z = 0;
    bullet_speed *= time;
    while (my != null)
    {
         if (you == null) {return;}
         if (vec_dist (my.x, you.x) < 100) // don't collide with the turret
         {
              my.passable = on;
         }
         else
         {
              my.passable = off;
         }
         ent_move (bullet_speed, nullvector);
         wait (1);
    }
}

Here's the final function:

function remove_bullet()
{
    wait (1);
    if (you == player) {player._health -= 10;}
    ent_remove (me);
}

If the bullet hits the player, it substracts 10 healthpoints from player's health and it disappears; if the bullet has hit a wall it will disappear without causing any damage.