Accurate crosshairs

One of the problems with the template guns is that the bullets don't hit the center of the crosshair (press K to show / hide the crosshair if you are using the templates). The problem is obvious if you are close to the wall and you are using a bullet that has hit_hole inside its definition; weapon_mg_animated is a good example. The reason is simple: the weapon isn't placed in the center of the screen (it's on the right side usually) so the bullet can't hit the center of the screen which happens to be the center of the crosshair as well. More than that, if you want to shoot small objects placed at big distances - maybe a rope that will lower a bridge - you will miss them.

Now that you know about this problem, you will be happy to hear that you can solve it with only 11 lines of code. We have to modify weapons.wdl but don't be afraid - I have included the modified weapons.wdl file.

The idea is simple: we create two positions (vectors) in front of the player: one at 20 quants and the other one at 10,000 quants and we trace between them. Both positions are created in the center of the screen so we know that every time we shoot a bullet it will hit the center of the crosshair - it's a straight line!

If we wouldn't use this trick we would have to place all our weapons in the center of the screen, like in the picture below:

Let's see this awesome piece of code - it is placed inside function gun_fire() in weapons.wdl:

 damage = MY._DAMAGE;
 fire_mode = MY._FIREMODE;

 my.skill40 = (screen_size.x + bmap_width(cross_bmp)) / 2;
 my.skill41 = (screen_size.y + bmap_height(cross_bmp)) / 2;
 my.skill42 = 20;
 vec_set (my.skill43, my.skill40);
 my.skill45 = 10000;
 vec_for_screen (my.skill40, camera);
 vec_for_screen (my.skill43, camera);
 trace_mode = ignore_me + ignore_passable;
 trace (my.skill40, my.skill43);
 vec_set (gun_target, target);
 vec_diff (shot_speed, gun_target, gun_muzzle);

 // if player is not already passable
 if(player.passable == OFF)
 {
      player.passable = ON;   // make it so the gun doesn't hit the player
      gun_shot();       // fire the shot
       player.passable = OFF;
 }
 else
 {
      gun_shot();
 }

The old template code is red, the new code is yellow.

I have used skill40..skill42 as a var and skill43..skill45 as another var. First we get the coordinates of the center of the screen. If you aren't sleepy you have noticed that the center of the screen would be given by something like this:
 
 my.skill40 = (screen_size.x - bmap_width(cross_bmp)) / 2; // with "-" not "+"
 my.skill41 = (screen_size.y - bmap_height(cross_bmp)) / 2; // with "-" not "+"
 
 You are right: it is a small bug in weapons.wdl; the crosshair panel isn't placed exactly in the center of the screen - its upper left corner is there, so we have to add half of the bitmap to get its center. Let's move on: skill42 is set 20 quants in front of the screen because we don't want our trace instruction to hit the player or its weapon, giving us a wrong result.

Now we copy skill40 to skill43; we use these skills as vectors so skill44 = skill41 and skill45 = skill42 automatically. However, skill45 needs to be set 10,000 quants away from the player so we modify its value. The following vec_for_screen instructions will create those virtual positions inside the 3D world. We trace between those two positions and we set the result (target - read the manual about it) to gun_target and then we get the direction (shot_speed) from gun_muzzle to gun_target.

I had to move this line at the beginning of weapons.wdl:

BMAP cross_bmp, <cross.pcx>;

because I needed cross_bmp in my code and it wasn't defined before it.

 
 

Thanks

The following standalone project should get you started if you plan to create a tank game.

Function main is really simple: it disables the "D" key because we will use it to move the player (WSAD) and loads the level:

function main()
{
     on_d = null; // disable the standard "debug" key
     level_load (level1_wmb);
}

The tank that is controlled by the player consists of 3 parts: body, turret and barrel. The turret and the barrel are attached to the body using 2 ent_create instructions in action player_tank:

ent_create(challenger_turret, nullvector, attach_player_turret);
ent_create(challenger_barrel, nullvector, attach_player_barrel);

Let's take a look at the functions that attach these tank parts to the body:

function attach_player_turret()
{
     my.passable = on;
     while(you) // as long as the creator exists
     {
          vec_set(my.x, you.x);
          my.pan = you.pan + turret_offset;
          my.frame = you.frame;
          my.next_frame = you.next_frame;
          wait(1);
     }
    ent_remove(my);
}

The turret is passable and exists only if the body exists. The turret has the same position and animation frame with the body all the time. There's something interesting with this line of code:

          my.pan = you.pan + turret_offset;

The body and the turret can be rotated at different angles, like in the picture below; turret_offset is the difference between their pan angles at a certain moment.

In my example, turret_offset = 60 degrees. Function attach_player_barrel() is similar:

function attach_player_barrel()
{
     my.passable = on;
     while(you) // as long as the creator exists
     {
          vec_set(my.x,you.x);
          my.pan = you.pan + turret_offset;
          my.tilt = you.tilt + barrel_offset;
          my.frame = you.frame;
          my.next_frame = you.next_frame;
          vec_for_vertex(rocket_coords, my, 33); // vertex for player's rocket
          if (mouse_left == 1 && procket_ptr == null && player.shield > 0)
         {
              ent_create (tank_rocket, rocket_coords, player_rocket);
              snd_play (tankrocket_snd, 100, 0);
         }
         wait(1);
    }
    ent_remove(my);
}

The barrel must have the same pan angle with the turret so it uses the same turret_offset var but it can have a different tilt angle and that value is stored in barrel_offset. The barrel is the rocket generator so we use its 33rd vertex as the starting point for all the rockets fired by the player.

I wanted to disable autofire because it wouldn't be fair (nor realistic) to shoot several rockets a second; I did this by assigning a pointer - procket_ptr - to the rocket when it is created. If the rocket explodes and disappears, procket_ptr is set to null and we can create another one. If we press the left mouse button (LMB) and the previously created rocket has disappeared and the player tank is alive, we create a rocket and we play a sound.

Let's take a look at the action associated to the player:

action player_tank
{
     player = me;
     my.enable_impact = on;
     my.enable_entity = on;
     my.event = tank_damaged;
     ent_create(challenger_turret, nullvector, attach_player_turret);
     ent_create(challenger_barrel, nullvector, attach_player_barrel);
     my.shield = 250; // 250 shield points for player's tank

I'm the player and I'm sensitive to impact or other entities. When one of these events happen, function tank_damaged() will run. I have 250 shield points; shield is just another name for skill20. The turret and the barrel were discussed a few lines above.

     while (my.shield > 0) // the 10th hit will destroy player's tank
     {
          camera.pan = my.pan;
          camera.x = my.x - 500 * cos (my.pan);
          camera.y = my.y - 500 * sin (my.pan);
          camera.z = my.z + 300;
          camera.tilt = -20; // the camera looks down

          if (mouse_force.x != 0) // if we move the mouse on x
          {
               if (turret_handle == 0)
               {
                    turret_handle = snd_play (turret_snd, 70, 0);
               }
               turret_offset -= 2 * mouse_force.x * time;
          }
          else
          {
               turret_handle = 0;
          }
          if (mouse_force.y != 0) // if we move the mouse on y
          {
               barrel_offset += 2 * mouse_force.y * time;
          }

As long as my shield is bigger than zero, the camera follows me, being placed 500 quants behind me and 300 quants above me. The camera looks down at me.

If we move the mouse on the x axis and the turret sound isn't playing, we play the sound and we change turret_offset - this will change the pan for the turret and the barrel at the same time. If we move the mouse on the y axis, we change barrel_offset - the tilt angle for the barrel.

          if (player_speed.x != 0) //
          {
               my.pan -= 0.1 * (key_d - key_a) * player_speed.x * time;
               if (engine_handle == 0)
               {
                    engine_handle = snd_loop (tank_snd, 50, 0);
                    snd_tune (engine_handle, 25, 100, 0); // original frequency (100%)
                    my.skill10 = 100;
               }
          }
          else
         {
               my.skill10 -= 5 * time;
               snd_tune (engine_handle, 25, my.skill10, 0);
               if (my.skill10 < 20) // below 20% of the original frequency
               {
                    snd_stop (engine_handle);
                    engine_handle = 0;
               }
          }

The tank will change its pan angle by pressing the "A" and "D" keys. Please note that the tank can rotate only if it is moving, like (almost) any vehicle with wheels. If the tank engine sound isn't playing, we start the sound at its original frequency. If the tank isn't moving ("W" or "S" aren't pressed) the sound frequency will be decreased (using skill10 and snd_tune) until it goes below 20% of the original frequency and then it will be stopped.

          player_speed.x = 1.5 * (key_w - key_s) * time + max (1 - time * 0.15, 0) * player_speed.x;
          player_speed.y = 0;
          player_speed.z = 0;
          move_mode = ignore_you + ignore_passable;
          ent_move(player_speed, nullvector);
          wait (1);
     }
}

The player moves with "W" and "S" using a simple formula that was discussed in previous Aum editions; change 1.5 to modify the speed and 0.15 to modify inertia.

When the player presses the LMB, a rocket is created:

function player_rocket()
{
     procket_ptr = me;
     my.skill40 = 1; // it is a rocket
     my.pan = you.pan;
     my.tilt = you.tilt;
     my.enable_impact = on;
     my.enable_block = on;
     my.enable_entity = on;
     my.event = explode_rocket;
     my.skill5 = 0; // used for vertical movement
     procket_speed.x = 100;
     procket_speed.y = 0;
     procket_speed.z = 0;
     procket_speed *= time;
     while (my != null)
     {
          my.skill5 += time;
          if (my.skill5 > 20)
          {
               procket_speed.z -= fall_speed * time;
          }
          move_mode = ignore_you + ignore_passable; // ignores the barrel -> can't collide with it
          ent_move (procket_speed, nullvector);
          wait (1);
     }
}

Every rocket gets the procket_ptr pointer in order to disable autofire, remember? By setting skill40 to 1 we can identify if a certain entity is a rocket or not - we'll use this feature later. The rocket has the same orientation (pan and tilt) with the barrel and it is sensitive to impact, level blocks and other entities. If one of these events happen, function explode_rocket() will run. The rocket will move 100 * time quants every frame.

As long as the rocket hasn't exploded, skill5 is increased inside a while loop and the rocket moves. When skill5 grows bigger than 20, the rocket starts to loose height (the effect of the gravity) and if it hasn't hit anything yet it will hit the ground. Play with fall_speed and with 20 to get different trajectories.

When the rocket hits the ground, a wall or another entity it explodes using the following function:

function explode_rocket()
{
     my.skill10 = vec_dist (player.x, my.x);
     if (my.skill10 < 1000) {my.skill10 = 1000;}
     my.skill10 = 100000 / my.skill10;
     snd_play (tankexplo_snd, my.skill10, 0);

     vec_set (temp, my.pos);
     temp.z += 50; // create the explosion sprite a little higher
     ent_create(explosion_pcx, temp, animate_explosion);
     ent_remove (me);
}

I have decided to replace the ent_playsound function with one that works better in this particular game. The rocket gets the distance between it and the player and stores it in skill10. If the distance is below 1000 quants, the explosion sound will be played at its maximal volume, otherwise the sound will be played at lower volumes, depending on the value stored in skill10.

We have to create an explosion at the impact point, right? We do that using an explosion sprite and the function animate_explosion; please note that the sprite is created 50 quants above the impact point - this allows us to see the whole sprite:

function animate_explosion()
{
     my.passable = on;
     my.ambient = 100;
     while (my.frame < 7)
     {
          my.frame += 1 * time;
          wait (1);
     }
     ent_remove (me);
}

The sprite is passable and bright and plays its frames; when it has reached the last frames it is removed.

What happens if one of the tanks is hit?

function tank_damaged()
{
     if (you.skill40 != 1) {return;}
     wait (1);
     my.shield -= 25;
     if (my.shield <= 0) // player's tank was hit 10 times or the enemy tank was hit twice
     {
          while (1)
          {
               effect (tank_smoke, 5, my.pos, normal);
               sleep (0.2);
          }
     }
}

The tank could collide with another entity: maybe a fountain, another tank, etc. If this entity hasn't got its skill40 set to1 the function returns. Every rocket hit will take 25 shield points so the player should die after 10 hits (player.shield = 250, remember?)

If the player was hit 10 times or the enemy tanks were hit twice (their shield = 50) we start to emit smoke:

function tank_smoke()
{
     temp.x = random(2) - 1;
     temp.y = random(2) - 1;
     temp.z = random(2) + 1;
     vec_add (my.vel_x, temp);
     my.alpha = 30 + random(30);
     my.bmap = tanksmoke_pcx;
     my.size = 100;
     my.flare = on;
     my.move = on;
     my.lifespan = 100;
     my.function = fade_smoke;
}

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

The smoke has a random speed on x and y and a positive speed on z, so it moves upwards. The smoke particles will decrease their alpha and when they become invisible they are removed.

Now it is the time to look at the enemy tanks. I have used a model that has the turret and the barrel attached to the body in order to keep the things simple for you.

action enemy_tank
{
     my.enable_impact = on;
     my.enable_entity = on;
     my.event = tank_damaged;
     my.shield = 50;

The tank is sensitive to impact and other entities and shares the same tank_damaged function with the player. Two rockets fired by the player will destroy the tank.

     while (my.shield > 0)
     {
          vec_set (temp.x, player.x);
          vec_sub (temp.x, my.x);
          vec_to_angle (my.skill4, temp); // turn towards the player

As long as the enemy tank is "alive" it will rotate towards the player, trying to shoot it. We don't want a sudden rotation, but a smooth one so we don't rotate the tank towards the player, but skill4. Now we know that skill4 holds the correct angle towards the player but we need to rotate the tank smoothly towards the player, like in real life.

          if (abs(my.skill4 - my.pan) < 1)
          {
               vec_for_vertex(my.skill1, my, 41); // vertex for the enemy rocket
               ent_create (tank_rocket, my.skill1, enemy_rocket);
               sleep (3);
          }
          else
          {
               my.pan += ang(my.skill4 - my.pan) * 0.03 * time; // smooth rotation code
               enemy_speed.x = 5 * time;
               enemy_speed.y = 0;
               enemy_speed.z = 0;
               move_mode = ignore_you + ignore_passable;
               ent_move(enemy_speed, nullvector);
          }
          wait (1);
     }
}

If the difference between the angle stored in skill4 and the current pan angle of the enemy tank is smaller than 1 degree, we set the starting point for the enemy rocket to the 41st vertex on the tank and we fire a rocket every 3 seconds. If the difference of the two angles is bigger than 1 degree, the tank will rotate smoothly and it will move at the same time, like a vehicle on wheels.

The function for the enemy rockets is different because the enemy fires a rocket every 3 seconds:

function enemy_rocket()
{
     my.skill40 = 1; // it is a rocket
     my.pan = you.pan;
     my.tilt = you.tilt;
     my.enable_impact = on;
     my.enable_block = on;
     my.enable_entity = on;
     my.event = explode_rocket;
     erocket_speed.x = 100;
     erocket_speed.y = 0;
     erocket_speed.z = 0;
     erocket_speed *= time;
     while (my != null)
     {
          move_mode = ignore_you + ignore_passable; // ignores the barrel -> can't collide with it
          ent_move (erocket_speed, nullvector);
          wait (1);
     }
}

We set skill40 to 1; this way the entity will be detected as a rocket. The enemy rocket has the same pan and tilt with the enemy tank, it is sensitive to impacts, level blocks or other entities and it has the same event with the rockets fired by the player. Please note that this rocket moves in a straight line, without gravity so it should hit your tank pretty often.

Before turning this game skeleton into a AAA money making scheme you should add gravity and better AI to it. Don't forget to send me a free copy of the game :)