Moon Alert

This game was developed for Sinclair Spectrum and Commodore 64 about 10 years ago. However, I think that it is a great game (if you look at the game concept itself, not at my poor graphics...)

Before we begin, let's take a look at the game (the comments were added with yellow):

You control the tank (I needed a model with a turret) and you can make it jump over several black holes by controlling its speed: bigger speed will help you jump over larger holes. To make the things even more complicated, you have to shoot the ufo enemies or they will shoot you.

I will start with the text and the panel definitions; I use a single string to display "Score:" and "Lives:" and two digits for their values:

text moon_text
{
font = alert_font;
pos_x = 0;
pos_y = 0;
string = "Score: Lives:";
flags = visible;
}

panel moon_panel // displays the score using 3 digits and lives using 1 digit
{
pos_x = 0;
pos_y = 0;
digits = 190, 0, 3, alert_font, 1, score;
digits = 720, 0, 1, alert_font, 1, lives;
flags = refresh, d3d, visible;
}

Function main show all the triangles for all the models, limits the frame rate to 30 fps and loads the level:

function main()
{
clip_size = 0;
fps_max = 30;
level_load (alert_wmb);
}

The player controls the tank using this action:

action players_vehicle
{
player = me;
my.z = 20;
ent_create(barrel_mdl, nullvector, attach_barrel);
vehicle_speed.y = 0;
vehicle_speed.z = 0;

The tank will be placed 20 quants above the floor; this way we make sure that it can't get stuck in the floor after a big jump. We create the barrel and we attach it to the tank using function attach_barrel(). The tank will move with the speed given by vehicle_speed; it moves from left to right (on x) so we reset the other two speed components on y and z.

while (lives > 0)
{
if (key_cul == 1 && speed_offset > 0) // use cursor left to slow down the vehicle
{
speed_offset -= 1 * time;
}
if (key_cur == 1 && speed_offset < 5) // use cursor up to increase the speed of the vehicle
{
speed_offset += 1 * time;
}
vehicle_speed.x = 5 + speed_offset; // speed = 5...10
vehicle_speed *= time;
if (key_space == 1)
{
vehicle_jumps();
}

As long as the player is alive, it can decrease or increase the speed of the tank by pressing the left or right arrow keys. The speed ranges from 5 to 10 because speed_offset can range from 0 to 5. If we press the "space" key the tank will jump; we will talk about this function a little later.

move_mode = ignore_passable;
ent_move (vehicle_speed, nullvector);
camera.x = my.x;
camera.y = -2500;
camera.z = 700;
camera.pan = 90;
score += 0.01 * time; // add to the score as long as the player manages to survive
wait (1);
}
}

The tank ignores the passable entities while moving. The camera follows the tank every frame, like in the picture below:

The score increases (slowly) all the time because it isn't that easy to survive in the level, right? If the player shoots one of the enemies the score will increase a lot. Let's move to the function that creates the barrel:

function attach_barrel()
{
proc_late(); // prevent shaking
my.passable = on;
while(lives > 0) // as long as the player isn't dead
{
vec_set(my.x,you.x);
my.pan = you.pan;
my.tilt = barrel_offset;
if (key_cuu == 1 && barrel_offset < 90) // use cursor up
{
barrel_offset += 3 * time;
}
if (key_cud == 1 && barrel_offset > 0) // and cursor down to change the speed
{
barrel_offset -= 3 * time;
}

The first line of code prevents the turret from lagging one frame behind the tank. As long as the player is alive, the turret will get the same position with the tank; more than that, the turret will use the same pan angle. We want to be able control the tilt angle for our turret, because we must shoot down the ufos and they appear at different heights and positions. We change barrel_offset using the up and down arrow keys; this will change the tilt angle for the turret in this range: 0...90 degrees.

vec_for_vertex(rocket_coords, my, 33); // vertex for player's rocket
if (key_ctrl == 1 && rocket_ptr == null && lives > 0) // ctrl fires a rocket
{
ent_create (rocket_mdl, rocket_coords, fire_rocket);
snd_play (rocket_wav, 100, 0);
}
if (you.invisible == off) // used for blinking when the player loses a life
{
my.invisible = off;
}
else
{
my.invisible = on;
}
wait(1);
}
ent_remove(my);
}

We will fire from the 33rd vertex of the turret so we store its position in rocket_coords. If we press "Ctrl" and we don't have any other rocket on the screen (rocket_ptr = null) and the player isn't dead, we create a rocket and then we play a sound. Function fire_rocket() will tell you everything about rocket_ptr; we will talk about it a little later.

The loses a life if it runs into a hole or if it is hit by a rocket fired by one of the ufos; when one of these events happen, the tank will blink a few times in order to show what has happened. I thought that it wouldn't look nice if the tank would blink and the turret would be visible all the time, so the turret will be visible only when its creator (you), the tank, is visible. When the number of lives is smaller than 1 (the player has died) the turret disappears.

Let's see the function associated to the rocket:

function fire_rocket()
{
while (rocket_ptr != null) {wait (1);} // disable auto fire
rocket_ptr = me;
wait (1);
my.pan = you.pan;
my.tilt = you.tilt;
my.enable_impact = on;
my.enable_block = on;
my.enable_entity = on;
my.event = rocket_explodes;
while ((my != null) && (vec_dist(my.x, player.x) < 2000))
{
rocket_speed.x = 100;
rocket_speed.y = 0;
rocket_speed.z = 0;
rocket_speed *= time;
move_mode = ignore_you + ignore_passable; // ignores the barrel -> can't collide with it
ent_move (rocket_speed, nullvector);
wait (1);
}
ent_remove (me);
}

You might wonder what's with that rocket_ptr pointer; why would we need it? I chose a new method of disabling auto fire. When the player fires a rocket, a new entity with the name rocket_ptr is created. If we want to fire a new rocket again but the old rocket hasn't disappeared, rocket_ptr isn't set to null so the new rocket can't be fired because of this line.

while (rocket_ptr != null) {wait (1);} // disable auto fire

The rocket will use the same pan and tilt as its creator (the turret); it will be sensitive to impact, level blocks and entities. If the rocket collides with anything, its rocket_explodes event will run. As long as the rocket exists and its distance to the player is smaller than 2000 quants, the rocket will move ignoring any passable entity and its creator (you). This way the rockets don't collide with their creator - the turret. When the rocket is over 2000 quants away from the player, it is removed because it would waste resources for nothing.

What happens when a rocket hits something? The answer is given by the following function:

function rocket_explodes()
{
wait (1);
snd_play (explosion_wav, 40, 0);
vec_set (temp, my.pos);
temp.y -= 50; // move the explosion sprite a little closer to the player
ent_create(explosion_pcx, temp, animated_explosion);
ent_remove (me);
}

We wait for a frame to avoid the "dangerous event" warning, we play the explosion sound and then we create a sprite explosion 50 quants closer to the player, like in the picture below:

We do that because a part of the explosion sprite would be covered by the entity that was hit and that would look bad, isn't it? The sprite will run its animated_explosion function and the rocket will be removed. Let's take a look at the function that animates the sprite:

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

We wait for another frame and then we make the sprite passable, we set its flare and bright flags and then we make it shine by giving it a big ambient value. The animation frames for the explosion will be played with the speed given by 1 * time. As soon as the sprite reaches the last of its animation frames, it is removed.

The last function associated to the player is the one that makes it jump:

function vehicle_jumps()
{
if (my.skill40 == 1) {return;} // disable multiple jumps
snd_play (jump_wav, 30, 0);
my.skill40 = 1;
while (my.z < 300)
{
vehicle_speed.z = 5 * time;
wait (1);
}
while (my.z > 20)
{
vehicle_speed.z = -5 * time;
wait (1);
}
vehicle_speed.z = 0;
my.z = 20; // restore the exact vehicle height
my.skill40 = 0; // allow a new jump
snd_play (landed_wav, 30, 0);
}

Do you remember when I told you that the tank moves using only its vehicle_speed.x component? I lied; how could it jump without changing vehicle_speed.z? We know that the entity skills are set to zero when we start the game and my.skill40 makes no exception. We play a jumping sound and then we set skill40 to 1. This way if you press "space" twice the tank won't jump again while it is in the air. As long as tank's height is smaller than 300 quants, we increase its height by giving vehicle_speed.z a positive value. When the tank has reached over 300 quants on z, it gets out of the while loop and enters the second while loop, starting to fall because vehicle_speed.z has received a negative value. This happens until tank's height is below 20 quants and if you are wondering why I chose this value, the answer is simple: I didn't want the tank to get stuck in the floor, which is placed close to zero quants on the z axis.

We got out of the second while loop so we reset vehicle_speed.z, we set the initial tank height (20 quants), we reset skill40 (we allow a new jump) and then we play a landed_wav sound.

Time to take a look at the black holes. I am pretty sure that you will be surprised to find out that they aren't holes, but black wmb entities. I use textures from standard.wad for all the projects in Aum but this time I couldn't do that because I needed a few plain colors. I have included the wad used for this project (colors.wad) so make sure that you put it in your \wads folder.

We want our tank to "collide" with the black holes, right? However, this isn't such an easy task. Let's imagine this situation:

Do you think that the tank will manage to get out of this hole? Neither do I. It is clear that we must use a different hole system so I have decided that the black holes will be black wmb entities, like in the picture below:

Now try to imagine what would happen if you would texture the white wmb entity with black; it would look like a black hole because we are using a black background. The tank can collide with the black entity and it will look like it has collided with the hole. The level was created from a simple block and these wmb entities, isn't that nice?

Every "hole" has a simple action attached to it:

action obstacle
{
my.enable_impact = on;
my.enable_entity = on;
my.event = obstacle_hit;
}

Every hole entity is sensitive to impact and other entities; its event is obstacle_hit:

function obstacle_hit()
{
my.passable = on; // don't trigger other events for this entity
lives -= 1;
player.invisible = on; // make the player blink 3 times
sleep (0.3);
snd_play (lost_wav, 60, 0);
player.invisible = off;
sleep (0.3);
player.invisible = on;
sleep (0.3);
snd_play (lost_wav, 60, 0);
player.invisible = off;
sleep (0.3);
player.invisible = on;
sleep (0.3);
snd_play (lost_wav, 60, 0);
player.invisible = off;
}

As soon as the tank hits a hole, that hole is made passable. The next line of code decreases the number of lives by 1 - now you can see why we had to make the hole passable: we don't want to trigger several events (take more lives) for a single hole. The player will blink 3 times and a sound will be played 3 times.

The game would be too easy if the only enemies would be the holes. Let's see the real enemies:

action ufo
{
while (player == null) {wait (1);}
my.y = player.y; // we don't have to align the enemies in Wed
my.enable_impact = on;
my.enable_entity = on;
my.event = destroy_ufo;
while (vec_dist (player.x, my.x) > 1000) // if the player is far away
{
wait (1); // wait
}

The enemy waits for the player to be created, then it changes its y to match player's y, like in the picture below:

The tank fires its rockets in a straight line so if we don't align the ufos in Wed carefully the rockets could miss them. That simple line of code aligns the ufos for us.

The ufo is sensitive to impact and other entities (player's rockets); its destroy_ufo event will run when it is hit by a rocket. The ufo will stand still if the player is more than 1000 quants away.

while (my.x > player.x)
{
my.x -= 5 * time;
if (total_frames % 90 == 1)
{
ent_create (rocket_mdl, my.pos, fire_bomb); // fire a rocket every 3 seconds
}
wait (1);
}
}

This new while loop will start to run as soon as the tank has come closer than 1000 quants. The ufo will move from right to left by changing its x coordinate (no need to use an ent_move instruction here). We want to have a smooth ufo movement so we need to change its position every frame but we don't want it to fire every frame because the player wouldn't have any chance. I have decided to use total_frames, the variable that holds the total number of frames displayed since the start of the game. We have limited the frame rate to 30 fps in main so total_frames is increased with 30 every second. The expression (total_frames % 90 == 1) is true when total_frames = 1, 91, 181, ... and so on. This way the rocket is fired every 3 seconds.

Ok, let's see the function associated to the rocket fired by the ufo:

function fire_bomb()
{
vec_set (temp.x, player.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp); // rotate the bomb towards the player
my.enable_impact = on;
my.enable_block = on;
my.enable_entity = on;
my.event = hit_player; // same event here
while (my != null)
{
bomb_speed.x = 100;
bomb_speed.y = 0;
bomb_speed.z = 0;
bomb_speed *= time;
move_mode = ignore_you + ignore_passable; // ignores the barrel -> can't collide with it
ent_move (bomb_speed, nullvector);
wait (1);
}
}

This function is similar to function fire_rocket, used by the tank. The only significant difference appears because of the first 3 lines of code; these lines will orient the rocket fired by the ufo towards the tank. If the player hits the ufo this function will run:

function destroy_ufo()
{
score += 20;
ent_remove (me);
}

This function adds 20 points to the score and removes the ufo. If the ufo hits the player, this function will run:

function hit_player()
{
snd_play (explosion_wav, 40, 0);
vec_set (temp, my.pos);
temp.y -= 50; // move the explosion sprite a little closer to the player
ent_create(explosion_pcx, temp, animated_explosion);
ent_remove (me);
lives -= 1;
}

We play a sound and we create the animated explosion sprite. The enemy rocket is removed and "lives" is decreased.

Rocket camera

If you want to be able to see through the "eyes" of your rocket, this code snippet will be useful. If you want to learn how to create a gun from scratch, this piece of code will be extremely useful.

I didn't want to use the template guns because I don't like the idea of changing the templates, although some of you have made me do it a couple of times :)
First of all we define an "entity" that will be used for the weapon that appears on the screen; set its x y z coordinates until it looks ok for you:

entity rocket_ent
{
type = <rlauncher.mdl>;
layer = 10;
view = camera;
x = 20; // x y z set he position of the entity
y = -10;
z = -10;
}

The camera that follows the rocket uses a new view:

view rocket_view
{
layer = 15;
pos_x = 0; // appears in the upper left corner
pos_y = 0;
size_x = 640;
size_y = 100;
arc = 80;
flags = visible;
}

The new view has 640 x 100 pixels; if your game uses a different resolution change these values. Rocket_view has a different arc, a little bigger than the one used for the camera (default = 60); this way you can see a few more details in rocket_view. Let's see the action attached to the rocket launcher:

action rocket_launcher
{
my.passable = on;
while (player == null) {wait (1);}
while (vec_dist (player.x, my.x) > 100)
{
my.pan += 3 * time;
wait (1);
}
ent_remove (me);
rocket_ent.visible = on;

The gun is passable and rotates until the player comes closer than 100 quants. When this happens, the launcher is removed and the rocket launcher "entity" is made visible.

while (player._health > 0) // fire only if the player is alive
{
if (key_ctrl == 1)
{
while (key_ctrl == 1) {wait (1);} // disable auto fire
vec_set (temp, rocket_ent.pos);
vec_for_screen (temp, camera);
ent_create (rocket_mdl, temp, move_rocket);
}
wait (1);
}
}

We can fire the gun only if the player is alive. If we press fire (Ctrl) we have to wait until we release the key and then we create the rocket inside the rocket_ent entity. If you read the manual, you know that these particular entities don't exist in the 3D world; they work just like your regular panels. So how do we create the rocket inside rocket_ent? We set temp to rocket_ent's position and then we use the great vec_for_screen instruction to convert the 2D position to 3D world coordinates.

Let's take a look at the function that moves the rocket:

function move_rocket()
{
my.enable_entity = on;
my.enable_block = on;
my.passable = on;
my.event = remove_rocket;
my.pan = camera.pan;
my.tilt = camera.tilt;

The rocket is sensitive to entities and level blocks; we must make it passable for now because it might collide with the player. If the rocket collides with an entity or with a block, its remove_rocket event will be triggered. The rocket will have the same pan and tilt angles with the camera so it will fly in the direction pointed by the camera.

while (my != null)
{
rocket_view.x = my.x - 100 * cos(my.pan);
rocket_view.y = my.y - 100 * sin(my.pan);
rocket_view.z = my.z + 10;
rocket_view.pan = my.pan;
rocket_view.tilt = -5;
rocket_speed.x = 30;
rocket_speed *= time;
ent_move (rocket_speed, nullvector);
if (vec_dist (camera.x, my.x) > 30)
{
my.passable = off;
}
wait (1);
}
}

As long as the rocket exists, we place the new view (rocket_view) 100 quants behind the rocket and 10 quants above it. The new view will have its tilt angle set to -5, so it will look down. The rocket will move with the speed given by rocket_speed.x; change 30 to modify the speed. If the distance between the camera and the rocket is bigger than 30 quants, the rocket becomes impassable; now it is safe to assume that it will not collide with the model used for the player.

If the rocket hits something we will see this function running:

function remove_rocket()
{
if (you != null)
{
you._health -= 150;
}
vec_set (temp, my.pos);
ent_create(explosion_pcx, temp, sprite_explosion);
ent_remove (me);
}

If the rocket hits an entity, it decreases 150 health points from its health; if we hit a car it won't hurt it at all but if we hit an actor it will kill it. We create an explosion sprite at the impact point and we make it run the following function:

function sprite_explosion()
{
my.passable = on;
my.flare = on;
my.bright = on;
my.ambient = 100;
ent_playsound (my, explosion_wav, 400);
while (my.frame < 7)
{
my.frame += 1 * time;
wait (1);
}
ent_remove (me);
}

There's nothing strange here; the explosion is passable and bright. We play an explosion sound, we go thru all the animation frames and then we remove the sprite.