dan

ACKNEX USER MAGAZINE
(free scripts and artwork - monthly updates)
Online magazine by George Pirvu.
Contains a Beginner's Corner
the Hot Feature of the Month
Screenshot of the Month
code snippets
example games
and
interviews with game developers.

I made a compilation of the "Beginner's corner" and "Code Snippets" of the first thirty numbers of aum

Alain Brégeon

Who is Georges Pirvu

In AUM one Georges Pirvu say :

"Finally, here's some information about me, the AUM editor. My name is George Dan Pirvu, I'm 30 years old and I work as a programmer and computer hardware engineer. I started to work with Conitec's engines in 1997; a few months later J.C. Lotter agreed that I should take care of the wdl section in the old Acknex User Magazine. I like to play games, but most of all I like to build them. I have worked at several customer projects and I am one of the proofs that you can make a living using the A4 engine."


AUM 33

AUM 45 | AUM 46 | AUM 47 | AUM 48
Code Snippets fountain Planet Survivors - part X Beginner's corner compass flying birds | Code Snippets Beginner's corner | Code Snippets Beginner's corner fdfdfdfdfd | Code Snippets Beginner's corner
AUM 41 | AUM 42 | AUM 43 | AUM 44
Code Snippets advenced explosion Planet Survivors - part VI Beginner's corner Randotunes Bouncing ball | Code Snippets Flame_thrower Planet Survivors - part VII Beginner's corner trap adventure cameras | Code Snippets torch Planet Survivors - part VIII Beginner's corner escalator fog of war fdfdfdfdfd | Code Snippets train tracks Planet Survivors - part IX Beginner's corner infinite level
AUM 37 | AUM 38 | AUM 39 | AUM 40
Code Snippets Grenades Planet Survivors - part II Beginner's corner Flares Buy now | Code Snippets Editable text Planet Survivors - part III Beginner's corner Intelligent music Comment tags | Code Snippets customizable user interface Planet Survivors - part IV Beginner's corner scripted cutscenes fdfdfdfdfd | Code Snippets deadly spiders Planet Survivors - part V Beginner's corner painting skins & sprites underwater effects
AUM 33 | AUM 34 | AUM 35 | AUM 36
Code Snippets Breakable walls Car AI part II Beginner's corner Access code Data sticks | Code Snippets RPG movement Car AI part III Beginner's corner Realistic water Starfield effect | Code Snippets Context menu ddfCar AI part IVddd Beginner's corner working with materials fdfdfdfdfd | Code Snippets elevator Planet Survivors part Iddd Beginner's corner Using several physics objects fdfdfdfdfd
AUM 29 | AUM 30 | AUM 31 | AUM 32
Code Snippets Lazer eyeslaser Perfect AI part III Beginner's corner Fade in and fade out Dust | Code Snippets Stationary weapons Perfect AI part IV Beginner's corner Volumetric fog Split screen shooter game | Code Snippets Fake mirrors Perfect AI the making of Beginner's corner Using the SDK | Code Snippets Player selection Car AI part I Beginner's corner Splinter Cell panel Strings above entities
AUM 25 | AUM 26 | AUM 27 | AUM 28
Code Snippets Typing tutor Physics demo Beginner's corner Fogger Instant debugger | Code Snippets Force field Pong Beginner's corner Vertex - based effects Star Wars text | Code Snippets Take-off Perfect AI part I Beginner's corner Water drops minimap | Code Snippets Heat seeking missiles Perfect AI part III Beginner's corner Sight range True level changing
AUM 21 | AUM 22 | AUM 23 | AUM 24
Code Snippets Tanks Accurate crosshairs Beginner's corner Waterfall Breakable windows | Code Snippets Security cameras Spreadsheet viewer Beginner's corner Pianissimo Light gate | Code Snippets Darts Robby le robot Beginner's corner Blood pool Remote cameras | Code Snippets Moon alert Rocket camera Beginner's corner aspect Landmine
AUM 17 | AUM 18 | AUM 19 | AUM 20
Code Snippets music skeleton inc. Beginner's corner New debug animated sprites | Code Snippets Intelligent elevators Inventory Beginner's corner Flashlight with batteries Night vision | Code Snippets Battleship Beginner's corner Connect 2 PCs | Code Snippets Calculator Model viwer Beginner's corner Head shots Alarm clock
AUM 13 | AUM 14 | AUM 15 | AUM 16
Code Snippets Power-ups Virtua cup Beginner's corner House of terror High scores | Code Snippets Scrolling panel 3rd shooter Beginner's corner Energy balls Disco | Code Snippets Air combat Teleport Beginner's corner Cheat code Intelligent rain | Code Snippets Flickgen Smooth model shading Beginner's corner Timer Vertical mirror
AUM 9 | AUM 10 | AUM 11 | AUM 12
Code Snippets John Wayne radar Beginner's corner cash and carry Footprints | Code Snippets Steroïds New car Beginner's corner Bow & arrow Surveillance camerace | Code Snippets Stratego 2 Turrets Beginner's corner Self-timed bomb Sniper gun | Code Snippets Sword combat Random forest Beginner's corner Time demo animated hud
AUM 5 | AUM 6 | AUM 7 | AUM 8
Code Snippets Redefining the keys Cutscène camera Beginner's corner Password request splitball | Code Snippets Eclipse 3D Game Space Beginner's corner Campath Car demo | Code Snippets pacman Shoot three switches Beginner's corner Slicing blades npc code | Code Snippets Mesh deformation menu Beginner's corner Footstep Exploding barrels
AUM 1 | AUM 2 | AUM 3 | AUM 4
Code Snippets non traduit car instructions obsolètes Beginner's corner Dfdfdfd fdfdfdfdfd | Code Snippets SCRAP - SCReen cAPture code Stratego 1 - RTS Beginner's corner clocks and string instructions panels texts and butons | Code Snippets The rope and the ladder maniac miner Beginner's corner Sneaking (Thief like) fdfdfdfdfd | Code Snippets Intelligent cameras like Resident Evil Earthquake Beginner's corner Mastering cameras First AI demo


Untitled

3rd shooter

No, it isn't the 3rd shooter in this magazine, it's the 3rd person shooter code. The good news is that you can use the code for any arcade game. This project has its own main function:

function
main()
{
level_load
(shoot3rd_wmb);
wait (2);
clip_size = 0;
fps_max = 50;
on_d = null;
}

There's nothing special in main; I have locked the frame rate to 50 fps and the "D" key won't bring the debug panel on because we need the key for player's movement (WSAD).

The action attached to the player is the biggest one so let's throw it in before you get tired:

action player_init
{
player = me;
player.skill20 = 100;
my.enable_impact = on;
my.event = player_damage;
camera.z = my.z + 700;
camera.tilt = -90;
ent_create(soldiergun_mdl, nullvector,
attach_soldiergun); // give him a gun

We use player's skill20 to store the health. The player is sensitive to impact; if it is hit by something its player_damage event is triggered. The camera is placed 700 quants above the player and looks down at it. The player gets a gun, but we'll talk about function attach_soldiergun a little later.

while (my.skill20 > 0)
{
my.pan += 4 * (key_a
- key_d) * time - 20 * mouse_force.x * time;
player_speed.x = 10 * (key_w - key_s) *
(1 - mouse_left) * time;
player_speed.y = 0;

As long as the player is alive (skill20 > 0), it can rotate by changing its pan with "A" and "D". The player moves forward / backward with "W" and "S", but only if the left mouse button (LMB) isn't pressed. I had to do this because when the player fires, its gun must point towards the enemy and when this player model walks its weapon points to the left.

      if (mouse_left == 1)
{
ent_cycle
("attack", 100);
ent_create (bullet_mdl,
my.skill12, init_shot);
snd_play (shoot1_snd,
70, 0);
while
(mouse_left == 1) {wait (1);}
}
else
{
if (player_speed.x !=
0)
{
ent_cycle
("walk", my.skill10);
my.skill10
+= 10 * time;
if
(my.skill10 > 100) {my.skill10 = 0;}
}
}

If we press the LMB, the player will change its frame to the last "attack" frame and a bullet will be fired. We don't want to have autofire, so we wait until the LMB is released. If LMB isn't pressed, the player will play its "walk" animation frames in a loop.

     vec_set (temp, my.x);
temp.z -= 3000;
trace_mode = ignore_me + ignore_sprites + ignore_models
+ use_box;
player_speed.z = -trace (my.x, temp);
ent_move(player_speed, nullvector);

     camera.pan = my.pan;
camera.x = my.x + 250 * cos (my.pan);
camera.y = my.y + 250 * sin
(my.pan);

wait (1);
}
}

The player moves using gravity - you've seen similar code in stratego2. Finally, the camera is centered in a point that is placed 250 quants in front of the player.

function attach_soldiergun()
{
proc_late();
my.passable = on;
my.metal = on;
while(you != null)
{
vec_set(my.x,you.x);
vec_set(my.pan,you.pan);
my.frame = you.frame;
my.next_frame = you.next_frame;
vec_for_vertex(you.skill12, my, 6); // soldier's bullet position is stored in soldier's skill12
wait(1);
}
ent_remove(my);
}

The function above gives player a gun that will exist as long as the player isn't removed from the level. The gun was animated together with the player and then it was saved as a separate model so we only need to set its x, pan and frame to be the equal with player's x, pan and frame. We store the bullet vertex coords in player's skill12 (you.skill12 = player.skill12 because the player has created the gun).

If the player or the enemy shoot, function init_shot runs:

function
init_shot()
{
my.pan = you.pan;
my.tilt = you.tilt;
my.skill10 = 1;
my.enable_entity = on;
my.enable_block = on;
my.event = remove_bullet;
my.ambient = 100;
my.skill1 = 0;
bullet1_speed.x = 10;
bullet1_speed.y
= 0;
bullet1_speed.z
= 0;
bullet1_speed
*= time;
while ((my
!= null) && (my.skill1 < 500)) // 500 = max distance
{
my.skill1 += 1 * time;
move_mode = ignore_you
+ ignore_passents;
ent_move (bullet1_speed, nullvector);
wait (1);
}
ent_remove
(me);
}

function
remove_bullet()
{
wait (1);
ent_remove (me);
}

The bullet has the same pan and tilt with its creator; it can collide with entites and level blocks. The bullet will exist (as long as it hasn't collided with anything) for a limited period of time - until its skill1 reaches 500. The bullet ignores its creator or any passable entity so it can't collide with the gun that has fired it, with water, etc. If the bullet hits something it is removed after 1 frame.

If the player is hit by an enemy bullet, its player_damage function will be triggered:

function player_damage()
{
if (you.skill10
!= 1)
{
return;
}
else
{
my.skill20 -= 10;
if (my.skill20 <=
0)
{
my.enable_impact
= off;
my.event = null;
my.skill10
= 0;
my.skill30
= 1; // I'm dead
while (my.skill10 <
67)
{
ent_cycle("death",
my.skill10);
my.skill10
+= 6 * time;

if (camera.z > player.z + 300)
{
camera.z -= 10 * time;
}
wait (1);
}
}
}
}

The player could be hit by a wall, by an enemy or by an enemy bullet. This is why we have to check skill10 at the beginning of the function to make sure that the player is hit by a bullet (we've set it to 1 in function init_shot, remember?) Every bullet will take 10 health points and if player's health goes below 0, the player will play its "death" animation. The camera will zoom in a little by decreasing its height in order to show a bigger player corpse.

Time to move on to the enemy! First of all, the action that creates the enemies:

action generate_enemy
{
my.scale_x
*= 0.7;
my.scale_y = my.scale_x;
my.passable = on;
my.oriented = on;
my.bright = on;
my.flare = on;
while (player == null) {wait (1);}
while (vec_dist
(my.x, player.x) < 500) {wait (1);}
waitt (48 + random (64));
vec_set (temp,
my.pos);
temp.z += 50;
ent_create (enemy_mdl, my.pos, move_enemy);
}

These 3rd person games allow us to create the enemies on the fly; I have used a few oriented sprites as enemy generators. If the player comes closer to them, they will start creating enemies. This way you can spread enemy generators throught player's path.

The generators will until the player is created; if it comes closer than 500 quants, the enemy is created after 3..7 seconds, 50 quants above the sprite. The enemy moves using the function below:

function move_enemy
{
my.enable_impact = on;
my.event = enemy_damage;
ent_create(enemygun_mdl, nullvector, attach_enemygun); // give him a gun

The enemy is sensitive to impact too; it gets a gun using a similar function.

while (vec_dist(my.x, player.x) > (300 + random(100)))
{
enemy_speed.x = 10 * time;
enemy_speed.y = 0;
enemy_speed.z = 0;

vec_set (temp.x, player.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp);

      move_mode = ignore_you + ignore_passents;
ent_move (enemy_speed, nullvector);
ent_cycle ("walk",
my.skill10);
my.skill10 += 10 *
time;
if (my.skill10 >
100) {my.skill10 = 0;}
my.tilt = 0;
wait (1);
}

As
long as the player is farther than 300..400 quants, the enemy will move towards
it, playing its "walk" animation in a loop.

ent_cycle("attack", 100);
waitt (2);
while (my.skill30 == 0 && player.skill20 > 0)
{
vec_set (temp.x, player.x);
vec_sub (temp.x, my.skill12);
vec_to_angle (my.pan,
temp);
ent_create (bullet_mdl,
my.skill12, init_shot);
my.tilt = 0;
snd_play (shoot1_snd, 70, 0);
waitt (32);
}
}

When the enemy has come close enough, it changes to "attack" and starts firing at the player if the player and the enemy are alive. The enemy rotates towards the player and fires a bullet every 2 seconds.

If the enemy is hit by a bullet, its enemy_function runs. The good news is that the enemies can shoot each other :)

function
enemy_damage()
{
if (you.skill10
!= 1) {return;}
wait (1);
my.enable_impact = off;
my.event = null;
my.skill10 = 0; // start with the first
death frame
my.skill30 = 1; // I'm dead
while (my.skill10 < 67)
{
ent_cycle("death", my.skill10); // play
death animation
my.skill10 += 6 * time;
wait (1);
}
my.passable
= on; // can pass through the corpse
}

First of all we check if the enemy has impacted with a bullet. If this is true we play its "death" animation and then we make its corpse passable because the player should be able to pass through it.

This was all for today. I'll see you in a few weeks with a fresh standalone project!


Untitled

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.


Untitled

Air combat

This time we have a (let's hope) fun player vs choppers game with lots of interesting features. Every standalone project has its main function:

function main()
{
sky_clip = -90; // renders the whole sky
on_d = null; // disables the debug panel
level_load (aircombat_wmb);
wait (2);
game_init();
}

The first line renders the whole sky; it is an open area and we need to see it without any clipping. We disable the "D" key because we will use it for movement, we load the level and when it has loaded we call the game_init() function:

function game_init()
{
while (player == null) {wait (1);}
while (1)
{
if ((mouse_left
== 1) && (player.health > 0)) // fire
{
snd_play (shoot_snd, 70, 0);
ent_create (rocket_mdl, player.pos, move_bullet);
recoil = 1;
waitt (2);
recoil = 0;
while (mouse_left == 1) {wait (1);} // disable autofire
}
temp_counter +=
1;
if (temp_counter
== 50)
{
temp_counter -= 49;
}
if (chopper_index[temp_counter]
< distance && chopper_index[temp_counter] > 0)
{
distance = chopper_index[temp_counter];
found_chopper = temp_counter;
}
else
{
distance = chopper_index[found_chopper];
}
if (remaining_choppers
== 0)
{
aiming_pan.visible = off;
}
wait (1);
}
}

This function waits until the player is created. When this happens, a while (1) loop is started. If we press the left mouse button (LMB) and the player is still alive, a sound is played and a rocket is created at player's position. We set the recoil to 1 for about 0.1 seconds and then we wait until the LMB is released; we'll talk about recoil a little later.

The rest of the code deals with the auto targeting (not aiming) system. If the current chopper is closer than the others and it is a valid chopper, its distance to the player will be stored in a var named distance, otherwise we keep the old distance. When all the choppers are down we hide the target panel.

We can have up to 50 choppers; we measure their distance to the player and the closest chopper will place a target panel on the screen. All you have to do is to make sure that you shoot in its center and the chopper will be hit.

The code that moves the player and the camera is included in the action below:

action player_moves
{
player = me;
my.health = 100;
my.invisible = on;
my.enable_impact = on;
my.enable_entity = on;
my.event = player_event;
while (my.health > 0)
{
vec_set (camera.pos,
my.pos);
camera.tilt +=
20 * mouse_force.y * time;
camera.pan -= 20
* mouse_force.x * time;
my.pan = camera.pan;
my.tilt = camera.tilt;
player_speed.x
= 15 * (key_w - key_s) * time - 0.5 * recoil;
player_speed.y
= 10 * (key_a - key_d) * time;
vec_set (temp,
my.x);
temp.z -= 1000;
trace_mode = ignore_me
+ use_box;
player_speed.z
= -trace (my.x, temp);
move_mode = ignore_you
+ ignore_passable;
ent_move(player_speed,
nullvector);
wait (1);
}
}

The player has 100 health points and it is invisible because we are in 1st person model; it is sensitive at impact with entities. As long as the player is alive, it can be moved using the WSAD keys and the mouse. You can play with the values for camera.pan and tilt, player_speed.x and y to modify the sensitivity and the speed. The speed for the forward movement is stored in player_speed.x; do you remember that we have set recoil to 1 for 0.1 seconds? This way the player will be moved backwards every time it fires a rocket. You can change 0.5 to get different recoils. The movement code includes gravity code so you can use any level (my example is set up in a flat level).

If the player is hit by an entity (the choppers fire sprites) its player_event function will run:

function
player_event()
{
wait (1);
exclusive_global;
if (my.health > 0)
{
my.health -= 0.5;
snd_play (playerhit_snd,
40, 0);
}
else
{
my.health = 0;
}
}

If
the player is alive, its health is decreased and a sound is played. If the player
is dead, is health is set to zero because it doesn't look good to see that your
dead player has health = -25. Let's take a look at the function that moves player's
bullet (a rocket):

function move_bullet()
{
my.passable = on;
my.enable_entity = on;
my.enable_block = on;
my.event = remove_me;
my.pan = camera.pan;
my.tilt = camera.tilt;
my.skill1 = 0;
rocket_speed.x = 500 * time;
rocket_speed.y = 0;
rocket_speed.z = 0;
while (my.skill1 < 100)
{
if (vec_dist (my.x,
player.x) > 50)
{
my.passable = off;
}
my.roll += 25 *
time;
my.skill1 += 1
* time;
effect (create_smoke,
1, my.x, normal);
move_mode = ignore_you
+ ignore_passable;
ent_move (rocket_speed,
nullvector);
wait (1);
}
ent_remove (me);
}

function
remove_me()
{
waitt (2);
ent_remove (me);
}

The rocket is passable at the moment of creation but its passable flag will be set to off as soon as the distance between the rocket and the player is over 50 quants. The rocket rotates around its roll angle and creates a smoke trail. If the rocket has travelled too much without hitting anything, it will be removed. The same thing happens when the rocket hits an entity or a level block.

Time to see the
functions that create the rocket smoke trail:

function create_smoke
{
temp.x = random(4) - 2;
temp.y = random(4) - 2;
temp.z = random(3) + 3;
vec_add (my.vel_x, temp);
my.flare = on;
my.bright = on;
my.alpha = 50;
my.bmap = smoke_pcx;
my.size = 30;
my.move = on;
my.lifespan = 100;
my.function = fade_smoke;
}

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

The smoke will move upwards; the particles will disappear as soon as their alpha goes below zero or their size goes below 2 (this should happen first if you are using my values, but you can change them).

Every chopper is created by an object that has the action chopper_generator attached to it.

action
chopper_generator
{
my.invisible = on;
my.passable = on;
waitt (64 + random (64));
ent_create (chopper_mdl,
my.pos, move_chopper);
}

I have used a cube for every chopper generator; the cube is made invisible and passable. The chopper will be created 4..8 seconds after the level is loaded and the function associated to it is scary :

function
move_chopper()
{
chopper_number += 1;
remaining_choppers += 1;
my.skill1 = chopper_number;
my.enable_impact = on;
my.enable_block = on;
my.enable_entity = on;
my.event = chopper_event;
my.skill40 = 1;

Every chopper has an unique number associated to it; we are using a var named chopper_number and every time move_chopper() is run, chopper_number is increased and the result is copied in skill1. The chopper will react if it collides with other entities or with level blocks. Finally, we set skill40 to 1 for all the choppers - this will be our method to detect if a certain entity is a chopper or not.

     while (my != null)
{
vec_for_vertex(my.skill18,
my, 55);
if (my.skill42
== 0 && abs(my.x + my.y - player.x - player.y) > 2000)
{
vec_set
(temp.x, player.x);
vec_sub
(temp.x, my.x);
vec_to_angle
(my.pan, temp);
}
my.tilt = 0;
ent_cycle("fly",
my.skill10);
my.skill10 += my.skill11 *
time;
if (my.skill11
< 10)
{
my.skill11
+= 0.2 * time; // increase the rotation speed
}
if (my.skill10
> 100)
{
my.skill10
= 0; // loop animation
}

As long as the chopper is "alive" we store the vertex that will be used for firing in skill18. If the difference between player's coords and chopper's coords is above 2000 quants, the chopper will rotate and it will move towards the player. We don't need to tilt the chopper but because vec_to_angle does that we set tilt to zero. The chopper will rotate its propeller faster and faster because skill11 is growing from 0 to 10.

if ((my.skill17 < 3 + random(3)) && (my.skill12 == 0))
{
my.skill15 = 0; // skills 15..17 are used a var here
my.skill16 = 0;
my.skill17 += 0.05 * time;
}
else
{
my.skill12 = 1;
if (my.skill15 < 5 * time)
{
my.skill15 += 2 * time;
}
my.skill16 = 0;
my.skill17 = 0;
if (500 < abs(my.x + my.y - player.x - player.y) < 1000) // close to the player -> fire bullets
{
my.skill25 += 3 * time; // a simple counter
my.skill26 = int (my.skill25);
if ((my.skill26 % 100 == 0) && (vec_dist (player.x, my.x) > vec_dist (player.x, my.skill18))) // change "100" to adjust the fire rate
{
ent_create (bullet_pcx, my.skill18, chopper_bullet); // don't fire until the chopper is up
}
}
}

Every chopper has a random altitude; skill15..17 are used as a var here, with skill15 storing the speed on x, skill16 the speed on y and skill17 the speed on z. You can see that the chopper moves only upwards until skill17 reaches the value set by 3 + random(3); you can change these values if you want to set different heights. When the chopper has reached its maximum height, it stops moving upwards and starts to move faster and faster in the direction given by its pan angle. If the difference between player's coords and chopper's coords is bigger than 500 and smaller than 1000, the chopper will fire bullets.

Now we have to face another challenge: we don't want the chopper to fire 50-100 bullets a second (it wouldn't be fair) but we want it move at 50-100 frames a second in the same while loop. If we would use a waitt(8) at the end of the while loop the chopper would fire 2 bullets a second - and this would be ok - but the chopper would move and change its animation frames only twice a second, right?

The solution is easy: we use a skill and we add to its value every frame. When the content of the skill has reached a certain value, we fire a bullet. I'm using skill26 to store the integer part of skill25; if skill26 % 100 = 0 (in other words, if skill26 = 0 or 100 or 200 or 300 or..) we fire a bullet. The good news is that you can change 100 to any other value to adjust the fire rate.

Wait! I see that the if branch has another weird condition inside it. What's with this line?

if (.......vec_dist (player.x, my.x) > vec_dist (player.x, my.skill18)

Well, you caught me. This part of the if branch makes sure that the chopper can't shoot with its back! I could have checked player's and chopper's angles but I want to show you as many different things as possible so I have decided to use a trick - take a look at the picture below:

If the distance between the player and chopper's "gun" (stored in skill18) is smaller than the distance between the player and chopper's origin, the chopper will fire. If the distance between the player and the origin is smaller than the distance between the player and the gun, the chopper won't shoot because the player is behind it.

        chopper_index[my.skill1] =
vec_dist(my.x, player.x);
if (distance > chopper_index[my.skill1]
- 4000)
{
vec_set
(temp, my.x);
if
(vec_to_screen (temp, camera)) // if the target is visible
{
aiming_pan.pos_x = temp.x - bmap_width(aiming_pcx) / 2;
aiming_pan.pos_y = temp.y - bmap_height(aiming_pcx) / 2;
aiming_pan.visible = on;
}
else
{
aiming_pan.visible = off;
}
}
move_mode
= ignore_you + ignore_passable;
ent_move(my.skill15, nullvector);
wait (1);
}
}

The array chopper_index stores the distance between every chopper and the player; if one of the choppers is closer, the target will appear at its position. I have decreased the distance with 4000 quants to add hysteresis - to make sure that the target isn't changing quickly from one target to the other as they come closer and closer to the player. Play with this value - it depends on the size of your level.

If the target is visible on the screen, we set the correct position for the aiming panel and we make it visible, otherwise we hide the panel. Finally, the chopper moves using skills15..17 and ignore passable entities.

If the chopper impacts with a rocket, a wall or with another chopper, its chopper_event() function will run:

function
chopper_event()
{
if (((event_type == event_entity) || (event_type == event_impact)) &&
(you.skill40 == 1))
{
my.pan -= 180;
my.skill42 = 1;
waitt (64);
my.skill42 = 0;
}
else
{
chopper_index[my.skill1]
= 0;
distance = 20000;
remaining_choppers
-= 1;
ent_playsound (my,
chopperx_snd, 1000);
my.passable = on;
my.ambient = 50;
waitt (2);
ent_create (explo13_pcx,
my.pos, explode_me);
waitt (4);
my.transparent
= on;
ent_create (explo13_pcx,
my.pos, explode_me);
ent_remove (me);
}
}

If the chopper collides with an entity and this entity is another chopper (we've set skill40 to 1 for all the choppers, right?), the chopper will get away for 4 seconds and then it will come back. This happens because the chopper chases the player only if its skill42 is set to zero - check the beginning of the while loop in function move_chopper.

If the chopper collides with a rocket, we won't target it anymore and we set a big distance for it - this makes the closest chopper alive to take control. We play an explosion sound and we create two sprites that overlap and run the same function to make the explosion look better and then we remove the chopper.

function explode_me()
{
my.oriented = on;
my.flare = on;
my.bright = on;
my.ambient = 100;
while (my.frame < 14)
{
if (my.scale_x
< 3)
{
my.scale_x += 0.5 * time;
my.scale_y = my.scale_x;
}
my.frame += 1 *
time;
vec_set (temp.x,
player.x);
vec_sub (temp.x,
my.x);
vec_to_angle (my.pan,
temp);
wait (1);
}
ent_remove (me);
}

When the chopper explodes is creates 2 sprites; their scale will grow until it reaches 3 and at the same time their animation frames will run. The sprites will turn towards the player to make sure that the explosion looks great; a simple sprite won't look that good when you shoot a chopper above your head.

Do you feel a little way too tired? We could stop here but there's a little function left to be to discussed:

function
chopper_bullet()
{
my.passable = on;
my.oriented = on;
my.flare = on;
my.bright = on;
my.scale_x = 0.5;
my.scale_y = my.scale_x;
my.enable_entity = on;
my.enable_block = on;
my.event = remove_me;

     vec_set (temp.x, player.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp);
// the bullet moves towards the player

     my.skill1 = 0;
bullet_speed.x = 100 * time;
bullet_speed.y = 0;
bullet_speed.z = 0;
while (my != null)
{
if (my.roll >
200) {my.passable = off;} // it works :)
my.roll += 25 *
time;
my.skill1 += 1
* time;
move_mode = ignore_you
+ ignore_passable;
ent_move (bullet_speed,
nullvector);
wait (1);
}
}

This function moves chopper's (sprite) bullets. The bullets are passable and they will be rotated towards the player at the moment of creation; if you would put the 3 separate lines in the while loop the player wouldn't be able to dodge any bullet - he would bite the dust every time. The rockets rotate around their roll angle and when roll > 200 their passable flag will be set to off.

I hope that you will enjoy playing with this project - I did!


Untitled

Battleship

I used to play this game a lot when I was younger; you place a few ships on "Your board" and then you try to guess the position of the ships on the "Enemy board". Run the game before reading the rest of this article; you will understand a lot of what's happening here without effort. If you don't know how to run the game read "Beginner's corner".

If you've never used multiplayer code until now you have to know that some of the code will run only on the server and some only on the client. You can't throw in single player code in a multiplayer game! It is always better to write new code from scratch.

Let's start the easy way, with some var definitions:

var number_of_boats = 3;

var server_boats[26];
var client_boats[26];

var server_guess = 0; // stores a number between 1 and 25; the server tries to guess the position of a boat on the client
var client_guess = 0; // stores a number between 1 and 25; the client tries to guess the position of a boat on the server

var
server_ready = 0;
var client_ready = 0;
var server_plays = 0;
var
client_plays = 0;

var
killed_by_server = 0;
var killed_by_client =
0;

We are going to place 3 boats on our board and we are using 2 arrays to store the boat positions. The boats will be placed on a 5 x 5 = 25 squares grid but I have used arrays with 26 elements because I want to store square1 in server_boats[1] and client_boats[1], etc. I'm not using server_boats[0] and client_boats[0] to make the things easier for you.

Server_guess and client_guess store a number between 1 and 25; if the client has clicked a specific square on our grid, client_guess is set to that value and sent to the server, like in the picture below:

Server_ready (client_ready) wil be set to 1 when the server (client) is ready to play: it has placed its 3 boats and has pressed the "Start" button. Server_plays (client_plays) will be set to 1 while the server (client) is playing, having access to the mouse pointer. Finally, killed_by_server and killed_by_client shows the number of boats killed by the server and client.

I'm going to show you a lot of panel definitions this month:

panel main_pan
{
bmap = main_pcx;
pos_x = 0;
pos_y = 0;
layer = 10;

     button = 50, 390, start1_pcx, start1_pcx, start2_pcx,
null, start_game, null;
button = 50, 460, quit1_pcx, quit1_pcx, quit2_pcx, null, quit_game, null;
button = 386, 192, boat_pcx,
boat_pcx, boat_pcx, null, place_boat, null;

flags = refresh, d3d, visible;
}

The main panel is the big picture that appears when you start the game; it has 3 buttons that are used to place the boats on your grid, start the game and quit.

panel disabled_pan
{
bmap = disabled_pcx;
pos_x = 595;
pos_y = 379;
layer = 30; // appears over
the main panel
alpha = 40;
flags = transparent, refresh,
d3d, visible;
}

Disabled_pan appears over the "Enemy board" and blocks access to it until we press the "Start" button.

panel enemyboard_pan
{
pos_x = 0;
pos_y = 0;
layer = 15; // appears over the main panel but it is covered by disabled_pan

     button = 599, 383, bomb_pcx, invisible_pcx, invisible_pcx,
fire_bomb, null, null;
button = 631, 383, bomb_pcx, invisible_pcx, invisible_pcx, fire_bomb, null, null;
button = 663, 383, bomb_pcx,
invisible_pcx, invisible_pcx, fire_bomb, null, null;
button = 695, 383, bomb_pcx,
invisible_pcx, invisible_pcx, fire_bomb, null, null;
button = 727, 383, bomb_pcx,
invisible_pcx, invisible_pcx, fire_bomb, null, null;

     button = 599, 415, bomb_pcx, invisible_pcx, invisible_pcx,
fire_bomb, null, null;
button = 631, 415, bomb_pcx, invisible_pcx, invisible_pcx, fire_bomb, null, null;
button = 663, 415, bomb_pcx,
invisible_pcx, invisible_pcx, fire_bomb, null, null;
button = 695, 415, bomb_pcx,
invisible_pcx, invisible_pcx, fire_bomb, null, null;
button = 727, 415, bomb_pcx,
invisible_pcx, invisible_pcx, fire_bomb, null, null;

     button = 599, 447, bomb_pcx, invisible_pcx, invisible_pcx,
fire_bomb, null, null;
button = 631, 447, bomb_pcx, invisible_pcx, invisible_pcx, fire_bomb, null, null;
button = 663, 447, bomb_pcx,
invisible_pcx, invisible_pcx, fire_bomb, null, null;
button = 695, 447, bomb_pcx,
invisible_pcx, invisible_pcx, fire_bomb, null, null;
button = 727, 447, bomb_pcx,
invisible_pcx, invisible_pcx, fire_bomb, null, null;

     button = 599, 479, bomb_pcx, invisible_pcx, invisible_pcx,
fire_bomb, null, null;
button = 631, 479, bomb_pcx, invisible_pcx, invisible_pcx, fire_bomb, null, null;
button = 663, 479, bomb_pcx,
invisible_pcx, invisible_pcx, fire_bomb, null, null;
button = 695, 479, bomb_pcx,
invisible_pcx, invisible_pcx, fire_bomb, null, null;
button = 727, 479, bomb_pcx,
invisible_pcx, invisible_pcx, fire_bomb, null, null;

     button = 599, 511, bomb_pcx, invisible_pcx, invisible_pcx,
fire_bomb, null, null;
button = 631, 511, bomb_pcx, invisible_pcx, invisible_pcx, fire_bomb, null, null;
button = 663, 511, bomb_pcx,
invisible_pcx, invisible_pcx, fire_bomb, null, null;
button = 695, 511, bomb_pcx,
invisible_pcx, invisible_pcx, fire_bomb, null, null;
button = 727, 511, bomb_pcx,
invisible_pcx, invisible_pcx, fire_bomb, null, null;

flags = refresh, d3d, visible;
}

The "Enemy board" picture is part of the main panel so enemyboard_pan is an empty panel with 25 invisible buttons; when we click one of the squares on the "Enemy board", we click one of these buttons.

panel
boat1_pan
{
bmap = boat_pcx;
pos_x = 0;
pos_y = 0;
layer = 15; // appears over
the main panel
flags = refresh, d3d;
}

panel
boat2_pan
{
bmap = boat_pcx;
pos_x = 0;
pos_y = 0;
layer = 15; // appears over
the main panel
flags = refresh, d3d;
}

panel
boat3_pan
{
bmap = boat_pcx;
pos_x = 0;
pos_y = 0;
layer = 15; // appears over
the main panel
flags = refresh, d3d;
}

The 3 panels above are the boat pictures that can be placed on our board.

panel boathit1_pan
{
bmap = boathit_pcx;
pos_x = 0;
pos_y = 0;
layer = 20; // appears over
the boat1_pan panel
flags = refresh, d3d;
}

panel
boathit2_pan
{
bmap = boathit_pcx;
pos_x = 0;
pos_y = 0;
layer = 20; // appears over
the boat2_pan panel
flags = refresh, d3d;
}

panel
boathit3_pan
{
bmap = boathit_pcx;
pos_x = 0;
pos_y = 0;
layer = 20; // appears over
the boat3_pan panel
flags = refresh, d3d;
}

The 3 panels above are the boats with flames that appear on our board if the enemy has guessed their position.

panel
destroyed1_pan
{
bmap = destroyed_pcx;
pos_x = 0;
pos_y = 0;
layer = 17; // appears over
the main panel
flags = refresh, d3d;
}

panel
destroyed2_pan
{
bmap = destroyed_pcx;
pos_x = 0;
pos_y = 0;
layer = 17; // appears over
the main panel
flags = refresh, d3d;
}

panel
destroyed3_pan
{
bmap = destroyed_pcx;
pos_x = 0;
pos_y = 0;
layer = 17; // appears over
the main panel
flags = refresh, d3d;
}

The 3 panels above are the boats with flames and the bomb that appear on the "Enemy board" when we manage to destroy an enemy boat.

panel
youwon_pan
{
bmap = youwon_pcx;
pos_x = 250;
pos_y = 200;
layer = 30; // appears over
all the panels
flags = refresh, d3d;
}

panel
youlost_pan
{
bmap = youlost_pcx;
pos_x = 250;
pos_y = 200;
layer = 30; // appears over
all the panels
flags = refresh, d3d;
}

These 2 panels appear at the end of the game and show the winner and the looser.

Function main is simple:

function
main()
{
level_load (dummy_wmb);
fps_max = 40; // lock the frame rate to 40 fps
init_game();
}

Let's take a look at function init_game():

function
init_game()
{
mouse_mode = 2;
mouse_map = cursor_pcx;
while (1)
{
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;

          if (server_ready
+ client_ready == 2)
{
if (killed_by_server != 3 && killed_by_client != 3)
{
disabled_pan.visible = off;
}
ifdef server; // server
if (server_plays == 1)
{
mouse_mode = 2;
}
else
{
mouse_mode = 0;
}
ifelse; // client
if (client_plays == 1)
{
mouse_mode = 2;
}
else
{
mouse_mode = 0;
}
endif;
}
wait
(1);
}
}

This is the first function that includes multiplayer - specific code so be careful. At game start, the mouse pointer is made visible and the bitmap that will be used for the pointer is cursor_pcx; the pointer will change its coordinates because of the while (1) loop. If server_ready = 1 and client_ready = 1 (all the boats are placed and the server & client have pressed their "Start" buttons) and the game isn't over (the server or the client haven't destroyed all the enemy boats) we hide the disabled_pan that was covering our "Enemy board", so we can click on it from now on.

If our PC is the server (this PC ran the game using -sv -cl) and it is its turn to make a move (remember server_plays?) we show the mouse pointer on the server. If server_plays = 0 (the client must make its move) we hide the mouse pointer on the server. If our pc is the client (this PC ran the game using -cl) and the client should make a move, we show the mouse pointer on the client. If client_plays = 0 (the server must make its move) we hide the mouse pointer on the client.

Try to stop a little and take a look at the code that starts with ifdef server and ends with endif. This is the way to run different pieces of code on server and clients and if you want to create a multiplayer game you should code it this way.

Let's move on; we will look at the function that runs when the player (client or server) presses the "start" button:

function
start_game()
{
if (number_of_boats != 0) {return;}
ifdef server;
server_ready
= 1;
send_var(server_ready);
if
(counter == 0)
{
counter = 1; // make sure that it runs only once
if (sys_seconds % 2 == 0) // generate a random value (0 or 1)
{
server_plays = 1; // shows the pointer on the server; the server has the first
move
client_plays = 0; // the client hides its mouse pointer
}
else
{
client_plays = 1;
server_plays = 0; // the server hides its mouse pointer
}
send_var(client_plays); // shows or hides the mouse pointer on the client
}
ifelse;
client_ready
= 1; // the client is ready to play
send_var(client_ready);
// and sends this var to the server
endif;
}

If we haven't placed all the boats on our board the function does nothing. If we are the server, we show that we are ready to play and we send server_ready over the network to the client. Server_ready isn't updating the client by itself; we need to send it to the client using send_var. I thought that it would be nice to allow both player (client and server) to start the game so we generate a random value on the server and depending on it we allow the server or the client to make the first move. This part of the function runs only of the server; why would we want to have two random numbers when the decision is so simple? The player could press Start (by accident or not) during the game so the random number must be generated only once. I am using sys_seconds and I test it to see if we have an odd or even number. If the number is odd, the server will have the first move; if the number is even the client will be the first to move.

Please remember that all of this happens on the server and the client doesn't know if it should be the first to move or not, so we send client_plays (could be 1 or 0) from the server to the client to make the mouse pointer visible (or not) on the client.

If we are the client (the part after ifelse;) and we have pressed the "Start" button we set client_ready to 1 and we send this var to the server.

Starting to make sense? Let me show you my favorite function from battleship - it runs when you press the "Quit" button:

function quit_game()
{
exit; // shut down the engine
}

I'm sorry but the next function is simply HUGE, although it is really simple. It is the function that runs when you click the boat icon; its role is to place the boats on "Your board". I'm a nice guy so I have decided to show you only the part that deals with the first square (1) and the last square (25).

function place_boat()
{
if (number_of_boats == 0) {return;} // all the boats are placed

If all the boats are placed, we get out of this function

mouse_map = boat_pcx;

If we click the boat icon on the main panel, the mouse pointer changes to boat_pcx. This gives us the illusion that we are carrying the boat icon over the screen when all we do is to use a different mouse pointer.

     while (mouse_left != 1)
{
if (mouse_right
== 1)
{
mouse_map = cursor_pcx;
return;
}
wait (1);
}

As long as we haven't pressed the left mouse button (LMB) we can abort by pressing the right mouse button (RMB). If the RMB is pressed, we change back to the "normal" mouse pointer and we get out of this function.

Let me show you "Your board" here:

if (mouse_pos.x > 40 && mouse_pos.x < 230 && mouse_pos.y > 20 || mouse_pos.y < 200)
{

If the boat is placed somewhere inside "Your board" (I've got these coords in Paint Shop Pro, but you can use any painting program)

if (mouse_pos.x > 40 && mouse_pos.x < 65) // placed on the "A" column
{

If the mouse is within these coordinates the boat will be placed inside the "A" column; it could be any square that starts with A (A1..A5).

if (mouse_pos.y > 20 && mouse_pos.y < 55) // inside A1
{

If the mouse is within these coordinates the boat will be placed at A1.

if (number_of_boats == 1)
{
boat1_pan.pos_x = 51;
boat1_pan.pos_y = 41;
boat1_pan.visible = on;
number_of_boats -= 1;
}

If we are placing our first boat, we are going to use the first boat panel (boat1_pan).

if (number_of_boats == 2)
{
boat2_pan.pos_x = 51;
boat2_pan.pos_y = 41;
boat2_pan.visible = on;
number_of_boats -= 1;
}

If we are placing our second boat, we are going to use the second boat panel (boat2_pan).

if (number_of_boats == 3)
{
boat3_pan.pos_x = 51;
boat3_pan.pos_y = 41;
boat3_pan.visible = on;
number_of_boats -= 1;
}

If we are placing our third boat, we are going to use the third boat panel (boat3_pan).

ifdef server;
server_boats[1] = 1;
ifelse;
client_boats[1] = 1;
endif;
}

If we are the server, server_boats[1] will be set to 1; it is the way we are telling the engine that the first square on our board has a boat. If we are the client, client_boats[1] will be set to 1. Please note that server_boats will store the position of the boats only on the server and client_boats will store the position of the boats only on the client.

.............................................................

The dots above hide the code for the rest of the squares, but excepting the coordinates for the mouse and the panels everything is exactly the same. Let's see the rest of the code:

           if (mouse_pos.y
>= 155 && mouse_pos.y < 185) // inside E5
{
if (number_of_boats == 1)
{
boat1_pan.pos_x = 180;
boat1_pan.pos_y = 169;
boat1_pan.visible = on;
number_of_boats -= 1;
}
if (number_of_boats == 2)
{
boat2_pan.pos_x = 180;
boat2_pan.pos_y = 169;
boat2_pan.visible = on;
number_of_boats -= 1;
}
if (number_of_boats == 3)
{
boat3_pan.pos_x = 180;
boat3_pan.pos_y = 169;
boat3_pan.visible = on;
number_of_boats -= 1;
}
ifdef server;
server_boats[25] = 1;
ifelse;
client_boats[25] = 1;
endif;

If we are the server and we have placed a boat at E5 (the 25th square) we set server_boats[25] to 1. If we are the client, we set client_boats[25] to 1.

}
}
}
mouse_map = cursor_pcx;

The boat was placed so we restore the original mouse pointer (the arrow).

}

The function above makes sure that if you click "Your board" somewhere inside a square the boat will be placed at its correct position every time.

Every time we click the "Enemy board" trying to destroy a ship, we run the function named fire_bomb():

function fire_bomb(square_number)
{
snd_play(bomb_snd, 40, 0);
ifdef server;
server_guess
= square_number;
send_var(server_guess);
server_plays = 0; // the server
fired, now hide the pointer on the server
client_plays = 1; // and make
it visible on the client
ifelse;
client_guess
= square_number;
send_var(client_guess);
client_plays = 0; // the client
fired, now hide the pointer on the client
server_plays = 1; // and make
it visible on the server
endif;
send_var(server_plays);
send_var(client_plays);
}

When we click one of the squares inside the "Enemy board", a sound is played. If we are the server, square_number (and server_guess) is set to the number that is passed as a parameter to this function. This is an extremely important engine feature: all the buttons that belong to a panel have an ID number. If we click the first sqare on the "Enemy board", we click a button that will set square_number and server_guess to 1. If we click the 14th square, square_number and server_guess will be set to 14. This feature has allowed me to use a single function for all the 25 buttons that appear on the panel named enemyboard_pan.

Remember we're on the server, right? The server sends its guess (server_guess) to the client and then stops playing and tells the client to play. If we are on the client, we send client_guess to the server, we stop playing and tell the server that it is its turn to play. Finally, we send server_plays and client_plays over the network to make the mouse pointers visible or not.

I feel you're pretty tired but we only have 2 functions left and they're pretty similar so grab some whatever you want to grab and read on. The server and the clients have a dedicated function that runs whenever something is sent over the network: a new player has joined the game, a var was sent, etc.

on_server server_event;
on_client client_event;

This piece of code tells the server to run the server_event function and the client to run the client_event function every time something is sent over the network. Let's see these complicated event functions:

function
server_event()
{
exclusive_global();

Only a copy of this function is allowed to run.

if (event_type == event_var)
{

If the client has sent a var to the server

          if (client_ready
== 1 && server_ready == 0)
{
snd_play(ready_snd, 50, 0);
}

If the client is ready and the server isn't ready, we play a sound on the server to let it know that the client has placed all the boats on his board and is waiting for the server.

if (server_ready + client_ready != 2) {return;}

The server and the client must be ready in order to start the game; if this isn't true we get out of this function

if (server_boats[client_guess] == 1)

Let's imagine that client_guess = 10 (the client has clicked on the 10th square). If the server was hiding a boat there, server_boats[10] is set to 1 because of that HUGE function we've talked about. Therefore, the line above checks if the client has guessed the position of a boat on the server.

{
snd_play(destroyed_snd, 100, 0); // a boat was hit!
killed_by_client += 1;

We play a sound and we increase killed_by_client

if (killed_by_client == 1)

If this is the first boat destroyed by the client

{
boathit1_pan.pos_x = 52 + ((client_guess - 1) % 5) * 32;
boathit1_pan.pos_y = 41 + 32 * (client_guess > 5 && client_guess <=
10) + 64 * (client_guess > 10 && client_guess <= 15)
+ 96 * (client_guess > 15 && client_guess <= 20) + 128 * (client_guess
> 20);
boathit1_pan.visible = on; // appears on the server over its boat
}

We compute the position for boathit1_pan (boat + flames) that will be placed on the server, over the ship that was hit, on "Your board". Let's see what's happening with these panel coords:

boathit1_pan.pos_x = 52 + ((client_guess - 1) % 5) * 32;
boathit1_pan.pos_y = 41 + 32 * (client_guess > 5 && client_guess <= 10) + 64 * (client_guess > 10 && client_guess <= 15)
+ 96 * (client_guess > 15 && client_guess <= 20) + 128 * (client_guess > 20);

I know that the grid for the boards has a width and height of 32 pixels; if client_guess = 1, boathit1_pan will be placed with its pos_x = 52; if client_guess = 2, pos_x = 52 + 32 and so on. The same thing happens on the y axis but this time the z coordinate changes according to the intervals in the paranthesis. Please note that client_guess isn't a simple guess at this point because the client has guessed one of the boats on the server, so client_guess holds the coordinate of one of the enemy boats.

if (killed_by_client == 2)
{
boathit2_pan.pos_x = 52 + ((client_guess - 1) % 5) * 32;
boathit2_pan.pos_y = 41 + 32 * (client_guess > 5 && client_guess <=
10) + 64 * (client_guess > 10 && client_guess <= 15)
+ 96 * (client_guess > 15 && client_guess <= 20) + 128 * (client_guess
> 20);
boathit2_pan.visible = on; // appears on the server over its boat
}

If we have destroyed the second boat, we show boathit2_pan.

if (killed_by_client == 3)
{
boathit3_pan.pos_x = 52 + ((client_guess - 1) % 5) * 32;
boathit3_pan.pos_y = 41 + 32 * (client_guess > 5 && client_guess <=
10) + 64 * (client_guess > 10 && client_guess <= 15)
+ 96 * (client_guess > 15 && client_guess <= 20) + 128 * (client_guess
> 20);
boathit3_pan.visible = on; // appears on the server over its boat
}

If we have destroyed the third boat, we show boathit3_pan.

server_boats[client_guess] = 0;

The line above resets the element of server_boats to zero. This way a boat can't be hit twice; without this line we could click in the same square three times and we would win the game.

send_var(killed_by_client); // the client has destroyed one of the boats -> make it show destroyed_pcx on its enemy board
}

The client has destroyed one of the boats on the server, so the server sends killed_by_client to the client and the client will show destroyed_pcx (boat, fire and bomb) on its "Enemy board".

          // killed_by_server
is sent from the client
if (killed_by_server
== 1 && destroyed1_pan.visible == off) // the first boat on the client
was destroyed, runs only once
{
destroyed1_pan.pos_x = 599 + ((server_guess - 1) % 5) * 32;
destroyed1_pan.pos_y = 383 + 32 * (server_guess > 5 && server_guess
<= 10) + 64 * (server_guess > 10 && server_guess <= 15)
+ 96 * (server_guess > 15 && server_guess <= 20) + 128 * (server_guess
> 20);
destroyed1_pan.visible = on;
}
if (killed_by_server
== 2 && destroyed2_pan.visible == off) // the second boat on the client
was destroyed, runs only once
{
destroyed2_pan.pos_x = 599 + ((server_guess - 1) % 5) * 32;
destroyed2_pan.pos_y = 383 + 32 * (server_guess > 5 && server_guess
<= 10) + 64 * (server_guess > 10 && server_guess <= 15)
+ 96 * (server_guess > 15 && server_guess <= 20) + 128 * (server_guess
> 20);
destroyed2_pan.visible = on;
}
if (killed_by_server
== 3 && destroyed3_pan.visible == off) // the third boat on the client
was destroyed, runs only once
{
destroyed3_pan.pos_x = 599 + ((server_guess - 1) % 5) * 32;
destroyed3_pan.pos_y = 383 + 32 * (server_guess > 5 && server_guess
<= 10) + 64 * (server_guess > 10 && server_guess <= 15)
+ 96 * (server_guess > 15 && server_guess <= 20) + 128 * (server_guess
> 20);
destroyed3_pan.visible = on;
}

Killed_by_server is sent from the client every time the server has destroyed another boat on the client. If killed_by_server = 1 and destroyed1_pan isn't visible, we show it over server's "Enemy board" at the position that corresponds to the square that was clicked on. If killed_by_server = 2 we show destroyed2_pan and so on.

          if (killed_by_server
== 3) // game over, server wins
{
youwon_pan.visible = on;
disabled_pan.visible = on; // can't throw bombs from now on
server_plays = 1; // show the mouse pointer
client_plays = 1; // on the server and on the client because the game is over
}
if (killed_by_client
== 3) // game over, client wins
{
youlost_pan.visible = on;
disabled_pan.visible = on; // can't throw bombs from now on
server_plays = 1; // show the mouse pointer
client_plays = 1; // on the server and on the client because the game is over
}
}
}

If killed_by_server = 3, the server wins so it displays the youwon_pan panel, makes disabled_pan visible (so that nobody can click on the "Enemy board" from now on) and shows the mouse pointer on the client and on the server, allowing them to exit the game by clicking on "Quit". If the client wins, the server displays the youlost_pan panel, makes disabled_pan visible and shows the mouse pointers.

The event associated to the client is similar but let's take a look at it:

function
client_event()
{
exclusive_global();
if (event_type == event_var)

If the server has sent a var to the client

     {
if (server_ready
== 1 && client_ready == 0)
{
snd_play(ready_snd, 50, 0);
}

If the server is ready but the client isn't ready, play a sound on the client to let it know that the server is waiting

if (server_ready + client_ready != 2) {return;}

The server and the client must be ready in order to start the game

if (client_boats[server_guess] == 1)

If the server has guessed the position of one of the boats on the client

          {
snd_play(destroyed_snd, 100, 0); // a boat was hit!
killed_by_server += 1;
if (killed_by_server == 1)
{
boathit1_pan.pos_x = 52 + ((server_guess - 1) % 5) * 32;
boathit1_pan.pos_y = 41 + 32 * (server_guess > 5 && server_guess <=
10) + 64 * (server_guess > 10 && server_guess <= 15)
+ 96 * (server_guess > 15 && server_guess <= 20) + 128 * (server_guess
> 20);
boathit1_pan.visible = on;
}
if (killed_by_server == 2)
{
boathit2_pan.pos_x = 52 + ((server_guess - 1) % 5) * 32;
boathit2_pan.pos_y = 41 + 32 * (server_guess > 5 && server_guess <=
10) + 64 * (server_guess > 10 && server_guess <= 15)
+ 96 * (server_guess > 15 && server_guess <= 20) + 128 * (server_guess
> 20);
boathit2_pan.visible = on;
}
if (killed_by_server == 3)
{
boathit3_pan.pos_x = 52 + ((server_guess - 1) % 5) * 32;
boathit3_pan.pos_y = 41 + 32 * (server_guess > 5 && server_guess <=
10) + 64 * (server_guess > 10 && server_guess <= 15)
+ 96 * (server_guess > 15 && server_guess <= 20) + 128 * (server_guess
> 20);
boathit3_pan.visible = on;
}

Show the corresponding boathit_pan panel (over "Your board") depending on how many boats were destroyed by the server.

client_boats[server_guess] = 0;

A boat can't be hit twice

send_var(killed_by_server);

The server has destroyed one of the boats -> make it show destroyed_pcx on its "Enemy board".

          }
if (killed_by_client
== 1 && destroyed1_pan.visible == off) // the first boat on the server
was destroyed, runs only once
{
destroyed1_pan.pos_x = 599 + ((client_guess - 1) % 5) * 32;
destroyed1_pan.pos_y = 383 + 32 * (client_guess > 5 && client_guess
<= 10) + 64 * (client_guess > 10 && client_guess <= 15)
+ 96 * (client_guess > 15 && client_guess <= 20) + 128 * (client_guess
> 20);
destroyed1_pan.visible = on;
}
if (killed_by_client
== 2 && destroyed2_pan.visible == off) // the second boat on the server
was destroyed, runs only once
{
destroyed2_pan.pos_x = 599 + ((client_guess - 1) % 5) * 32;
destroyed2_pan.pos_y = 383 + 32 * (client_guess > 5 && client_guess
<= 10) + 64 * (client_guess > 10 && client_guess <= 15)
+ 96 * (client_guess > 15 && client_guess <= 20) + 128 * (client_guess
> 20);
destroyed2_pan.visible = on;
}
if (killed_by_client
== 3 && destroyed3_pan.visible == off) // the third boat on the server
was destroyed, runs only once
{
destroyed3_pan.pos_x = 599 + ((client_guess - 1) % 5) * 32;
destroyed3_pan.pos_y = 383 + 32 * (client_guess > 5 && client_guess
<= 10) + 64 * (client_guess > 10 && client_guess <= 15)
+ 96 * (client_guess > 15 && client_guess <= 20) + 128 * (client_guess
> 20);
destroyed3_pan.visible = on;
}

Killed_by_client is sent from the server; show the corresponding destroyed_pan panel (over the "Enemy board") depending on how many boats were destroyed by the client

          if (killed_by_server
== 3) // game over, server wins
{
youlost_pan.visible = on;
disabled_pan.visible = on; // can't throw bombs from now on
server_plays = 1; // show the mouse pointer
client_plays = 1; // on the server and on the client because the game is over
}
if (killed_by_client
== 3) // game over, client wins
{
youwon_pan.visible = on;
disabled_pan.visible = on; // can't throw bombs from now on
server_plays = 1; // show the mouse pointer
client_plays = 1; // on the server and on the client because the game is over
}
}
}

Show the panels for winner and looser, show disabled_pcx to prevent the players from clicking on the "Enemy board" after the game is over and show the mouse pointers on the server and on the client

You might have noticed that I'm using only event_var in these event functions; I could have used (for example) event_leave to display a panel on the server if the client gets disconnected and so on. The code isn't that difficult; if you have problems understanding it try to run the game and observe small features, then go to the code and see how I did it. Here's an example:

Q: How have I managed to play a sound on the client by clicking "Start" on the server?
A: When the server has placed its boats on "Your board" and the player has pressed "Start", server_ready is set to 1 and sent over the network to the client. Function client_event will run as soon as something is sent from the server, checking if server_ready = 1 (the server is ready) and client_ready = 0 (the client isn't ready); if both conditions are true it will play a sound.


Untitled

Breakable walls

Did you know that you can use wmb entities for many purposes, including destroyable level geometry? This article shows you how to make a hole in the wall using your rocket launcher.

I have used the rocket launcher I've built in Aum24 ("Rocket camera") as the weapon, but you can use any other weapon that fires entities, including the template rocket launcher (if you make a few minor changes to its code). Read the article in Aum24 to get more info about my rocket launcher; I have made a few minor changes to its code, but the first line in the function below is the only one that really matters:

function move_rocket()
{
my.skill1 = 1357;
wait (1);
my.enable_entity = on;
my.enable_block = on;
my.passable = on;
my.event = remove_rocket;
my.pan = camera.pan;
my.tilt = camera.tilt;
rocket_speed.y = 0;
rocket_speed.z = 0;
while (my != null)
{
rocket_speed.x
= 100;
rocket_speed *=
time;
ent_move (rocket_speed,
nullvector);
if (vec_dist (camera.x,
my.x) > 30)
{
my.passable = off;
}
wait (1);
}
}

Every entity (rocket) fired by the rocket launcher will have its skill1 set to 1357 (of course that you can use any other value here). The reason for this is simple: the wmb entity that acts as a piece of wall will check if it was hit by a rocket or not by checking the value stored in skill1. You shouldn't be able to destroy a thick wall by running into it (collision with the player model) or by shooting it with a tiny bullet, or maybe by hitting the wall with your sword; only the rocket entity should be able to do that.

Let's see how these destroyable walls work. First of all, you create a wall with a big hole in it. I have created a simple hole by subtracting a slightly rotated cube from the wall, but I encourage you to create a more complex, irregular shaped hole.

Now go and create several wmb entities that will be used as breakable wall pieces; I have used a single "brick" and I have repeated it 3 times, but more different entities will look much better!

Fill the hole in the wall with several entities; align them carefully (their textures should tile seamlessly) but give the player a small hint: show some cracks, maybe a tiny hole, etc.

Attach action wall_bricks to every entity that fills the hole:

action
wall_bricks
{
my.enable_impact = on;
my.enable_entity = on;
my.event = destroy_me;
}

You can see that the entity is sensitive to impact with other entities; as soon as something hits it, its destroy_me event will run:

function
destroy_me()
{
if (you != null) // hit by an entity?
{
if (you.skill1
== 1357) // hit by a rocket?
{
ent_playsound (my, bricks_wav, 500); // play a sound
sleep (0.5);
ent_remove (me);
}
}
}

If the brick is hit by another entity (you != null) and the entity is a rocket (its skill1 = 1357) we play a sound at the impact point, we wait for half a second and then we remove the entity. Shoot that area of the wall several times and you will create a hole that will be big enough for the player (if you've planned it this way).

Like I said, you can use any other weapons if they fire entities and if you set one of the skills for their bullets to a specific value. The value of the skill must be checked by the event function of the brick entity.


Untitled

Calculator

This calculator allows you to add, substract, multiply and divide. I have added code that takes care of overflow as an example but you can improve the calculator by adding code that deals with divisions by zero, multiple operations (6 * 3 - 4 = 14 instead of 6 * 3 = 18, 18 - 4 = 14) and so on.

Let's start with some string definitions:

string result_string = " "; // for 5 digits
string temp_string = " "; // for 2 digits, number of the button (1..15)

Function main sets the background color to rgb = 000 (black), sets the mouse pointer bitmap, copies "0" to the display string because the calculator has run for the first time and then it allows us to move the mouse.

function
main()
{
screen_color.red = 0;
screen_color.green = 0;
screen_color.blue = 0;
mouse_map = mouse_pcx;
mouse_mode = 2;
str_cpy(result_string, "0");
// start with a zero displayed on the screen
while (1)
{
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
wait (1);
}
}

We will use a panel for all the buttons and a text for the display:

panel main_pan
{
bmap = main_pcx;
pos_x = 0;
pos_y = 0;
layer = 10;

     button = 292, 180, button1_pcx, button1_pcx,
button1_pcx, null, key_pressed, null;
button = 332, 180, button2_pcx, button2_pcx, button2_pcx, null, key_pressed, null;
button = 372, 180, button3_pcx,
button3_pcx, button3_pcx, null, key_pressed, null;
button = 292, 215, button4_pcx,
button4_pcx, button4_pcx, null, key_pressed, null;
button = 332, 215, button5_pcx,
button5_pcx, button5_pcx, null, key_pressed, null;
button = 372, 215, button6_pcx,
button6_pcx, button6_pcx, null, key_pressed, null;
button = 292, 250, button7_pcx,
button7_pcx, button7_pcx, null, key_pressed, null;
button = 332, 250, button8_pcx,
button8_pcx, button8_pcx, null, key_pressed, null;
button = 372, 250, button9_pcx,
button9_pcx, button9_pcx, null, key_pressed, null;
button = 292, 285, button0_pcx,
button0_pcx, button0_pcx, null, key_pressed, null;
button = 332, 285, buttonplus_pcx,
buttonplus_pcx, buttonplus_pcx, null, key_pressed, null;
button = 372, 285, buttonminus_pcx,
buttonminus_pcx, buttonminus_pcx, null, key_pressed, null;
button = 292, 320, buttonmultiply_pcx,
buttonmultiply_pcx, buttonmultiply_pcx, null, key_pressed, null;
button = 332, 320, buttondivide_pcx,
buttondivide_pcx, buttondivide_pcx, null, key_pressed, null;
button = 372, 320, buttonequal_pcx,
buttonequal_pcx, buttonequal_pcx, null, key_pressed, null;

flags = refresh, d3d, visible;
}

text display_text
{
layer = 20;
pos_x = 380;
pos_y = 145;
font = lcd_font;
string = result_string;
flags = visible;
}

The panel definition includes 15 buttons that will appear on the screen. When one of the buttons is clicked, function key_pressed will run and key_number will be set to a value between 1 (for button1_pcx) and 15 (for buttonequal_pcx), depending on what button was clicked. Let's see what this key_pressed function does:

function key_pressed(key_number)
{
snd_play(key_wav, 70, 0);
if (key_number >= 1 &&
key_number <= 10)
{
str_for_num(temp_string,
key_number);
if (str_len(temp_string)
== 2)
{
str_clip(temp_string, 1);
}
if (str_cmp(result_string,
"0"))
{
str_cpy(result_string, temp_string);
}
else // we add
a digit to the existing number
{
if (str_len(result_string) < 5)
{
str_cat(result_string, temp_string);
align_result();
}
}
}

Every time we click a button on the panel, a sound is played. If we have pressed 1, 2, 3, ..., 9 or 0, we convert the corresponding key_number to a string. If we press the 10th button (zero) we have to change 10 (it is the 10th button!) to its correct value (0). That's an easy task because 10 is the only figure that has 2 digits so we simply str_clip the first char from 10 and we get 0.

If the display shows "0" and we have pressed a button, "0" will be replaced with our button. If the display shows something different our button will add another digit to the existing number, got it? You can run the project to see what I mean. I have limited the numbers that can be input to 5 digits. Every time we add a digit, function align_result() ... aligns the result. We'll talk about it later.

     if (key_number >= 11 && key_number
<= 14)
{
display_text.pos_x
= 380; // restore the initial pos_x
str_cpy(result_string,
"0");
number_one = str_to_num(result_string);
add = (key_number
== 11);
substract = (key_number
== 12);
multiply = (key_number
== 13);
divide = (key_number
== 14);
}

If we press "+", "-", "x" or ":" display_text needs to move at its initial position because the display will show "0". The first number appears on the display before pressing "+", "-", "x" or ":" and it is stored in number_one; the second number appears on the display before pressing "=" and it is stored in number_two. We have learned that when we click one of the buttons, we keep adding digits on the display. In fact, we keep adding elements to result_string, and now it is the time to convert this string to a number -> number_one.

If we have clicked "+", add will be set to 1 and substract, multiply and divide will be set to 0.

     if (key_number == 15) // we have pressed "="
{
number_two = str_to_num(result_string);
result = (number_one
+ number_two) * add + (number_one - number_two) * substract + (number_one * number_two)
* multiply + (number_one / number_two) * divide;
if (result <
99999)
{
str_for_num(result_string, result);
}
else // overflow
{
str_cpy(result_string, "ERROR");
}
align_result();
number_one = 0;
number_two = 0;
add = 0;
substract = 0;
multiply = 0;
divide = 0;
while (key_any
== 0) {wait (1);} // don't erase the result - wait for the next mouse click
str_cpy(result_string,
"0"); // reset the display
display_text.pos_x
= 380; // restore the initial pos_x
}
}

When we press "=", result_string (the string displayed on the screen before pressing "=") will be converted to a number and stored in number_two. Now we have to do the real math using a simple formula:

result = (number_one + number_two) * add + (number_one - number_two) * substract + (number_one * number_two) * multiply + (number_one / number_two) * divide;

Well, it is a simple formula because if we want to add 2 numbers add = 1 and substract, multiply and divide = 0 so our formula changes to

result = number_one + number_two

and this looks so much better, isn't it? The same thing happens if we choose "-", "x" or ":".

If the result of the operation is below 99,999 (we have decided to use 5 digits, remember?) the number will be converted to result_string so it will appear on the display. If the result exceeds 99,999 the display will show "ERROR". The result will be aligned using that mysterious align_result() function and then we will reset number_one, number_two, etc. As long as we haven't pressed any key / mouse button, the result will be displayed on the screen; when we press a key or we click a mouse button, the display will be reset and result_string will be moved to its initial position.

I can't stand the pressure! I have to show you the following function:

function align_result()
{
display_text.pos_x = 380 - (str_len(result_string) - 1) * 18; // move / align
the text to the left as more digits appear / disappear
}

This function is simple but it does its job: it aligns the text to the left as more digits appear or disappear. I know that a character has a width of 18 pixels (take a look at the lcd_font definition if you don't believe me) so I substract 18 * (number_of_digits - 1) from 380 pixels in order to move the text that displays result_string to the left or right.

Please remember that this calculator needs more error protection code for division by zero and so on. At this point the operations must be executed like this:

32 x 2 = (it will display 64) and then you will have to type 64 + 7 = (it will display 71)

You can't use 32 x 2 + 7 = ... but if you want to improve the code you can replace number_one and number_two with a big array and store the temporary numbers inside it.


Untitled

Car AI

I know that many of you want to create racing games, so I have decided to start this new car AI series. This month we will learn to create a car that uses a path, accelerates and decelerates, stops after a predefined number of laps, includes a 3D sound whose frequency is tuned depending on the speed of the car... Oh, and we will learn to create a timer that displays seconds and milliseconds!

Let's start with a text and a panel definition:

text
info_txt
{
font = system_font;
pos_x = 5;
pos_y = 5;
string = "Lap:
Time:";
flags = visible;
}

panel
info_pan
{
pos_x = 0;
pos_y = 0;
layer = 10;
digits = 45, 5, 1, system_font,
1, laps_scr;
// digits
= 125, 5, 3.3, system_font, 1, total_time; // A6 version
digits = 125, 5, 3, system_font,
1, total_time; // // A5 version
flags = overlay, refresh, visible;
}

The text displays "Lap:" and "Time:" at the top of the screen and the panel displays the figures. The first "digits" line displays the number of laps and the second "digits" line displays the time in seconds (A5 version) and seconds + milliseconds (A6 version). Remove the comment from the "A6 version" line and comment the "A5 version" line if you own A6.

function
main()
{
fps_max = 30;
level_load (carai_wmb);
while (1)
{
if (key_c != 1)
{
camera.x = -3400;
camera.y = 1325;
camera.z = 840;
camera.pan = 326;
camera.tilt = -17;
}
else
{
camera.x = car1.x - 150 * cos(car1.pan);
camera.y = car1.y - 150 * sin(car1.pan) ;
camera.z = car1.z + 30;
camera.pan = car1.pan;
camera.tilt = 0;
}
if (car1.race_over
== 0)
{
total_time = total_frames / 30;
}
wait (1);
}
}

Function main limits the frame rate to 30 fps and then loads the level. My demo uses two cameras: press and hold the "C" key to switch to a camera that chases the car, or release the "C" key to switch to a fixed, isometric camera view. As soon as you press the "C" key, the camera will be placed 150 quants behind the car and 30 quants above it.

The time is computed and stored in total_time for as long as the car continues to move (car1.skill20 aka car1.race_over isn't set to zero). Let's see how this simple timer formula works:

total_time = total_frames / 30;

We have limited fps_max to 30, right? This means that on a decent pc the engine will render 30 frames each second. The predefined variable named total_frames holds the number of frames that have passed since the engine was started. Let's see a few possible values:

total_frames = 0 -> total_time = 0.000
total_frames = 1 -> total_time = 0.033
total_frames = 2 -> total_time = 0.066
total_frames = 3 -> total_time = 0.100
total_frames = 4 -> total_time = 0.133

............................................................

total_frames = 29 -> total_time = 0.966
total_frames = 30 -> total_time = 1.000
total_frames = 31 -> total_time = 1.033

...........................................................

This way you get a decent timer using a single line of code. Let's examine the action attached to the car:

action
car
{
car1 = me;
my.race_over = 0;
var waypoint_number = 0;
var car_speed;
var target_coords; // the
coordinates of the next waypoint on the path
var engine_handle;
var angle_diff;
my.counting_laps = 1;
my.laps = -1;
if (my.skill1 == 1)
{
ent_path("car01");
}
engine_handle = ent_playloop
(my, engine_wav, 50);
ent_waypoint(target_coords, 1);

I have decided to use a pointer for the car because I wanted to be able to chase it with the camera. Every car will have a skill defined as race_over that will be set to 0 at game start and to 1 when the car finishes the race. There are a few more local variable definitions, but we'll see what they do a little later. If the car has its skill1 set to 1, it will follow the path named "car01" in Wed. The car will play the sound named engine_wav in a loop, and the last line of code sets target_coords to the first point on the "car01" path.

     while ((waypoint_number < 300) &&
(my.race_over == 0))
{
temp.x = target_coords.x
- my.x;
temp.y = target_coords.y
- my.y;
temp.z = 0;
if (vec_to_angle
(angle_diff.pan, temp) < 150)
{
waypoint_number = ent_nextpoint(target_coords);
}
angle_diff.pan
= ang(angle_diff.pan - my.pan);
my.pan += car_speed
* angle_diff.pan * 0.005;

My path has less than 300 points, so "while (waypoint_number < 300)" is the same thing with "while (1)", isn't it? The car will continue to move until my.race_over will be set to 1, and guess what? This will happen at the end of the race! We store the vector from the current position of the car (my.x and my.y) to target_coords (first point on the path) in temp, and then we rotate the car towards the next point on the path. The next destination point will be set when the car comes closer than 150 quants to its current destination.

We increase the pan angle of the car smoothly, depending on angle_diff.pan. You can play with 0.005, but please be aware that if the speed of the car is too big and 0.005 was set to a smaller value, the car might not have enough time to rotate towards its destination point.

          if (((waypoint_number
> 65) || (waypoint_number < 8)) || ((waypoint_number > 34) &&
(waypoint_number < 49)))
{
if (car_speed.x < 60 * time)
{
car_speed.x += 1 * time;
}
}
else
{
if (car_speed.x > 40 * time)
{
car_speed.x -= 0.5 * time;
}
}

The lines of code above accelerate and decelerate depending on the configuration of the track; you shouldn't expect a car driven by the computer to "know" where and why to reduce its speed, right? Take a look at the picture below:

The image shows the relevant points on the "car01" path. The car will accelerate until its speed reaches 60 * time if it moves inside one of these regions: 65...8 or 34...49; else (8...34 or 49...65) the car will decrease its speed until it reaches 40 * time.

          snd_tune (engine_handle,
50, car_speed.x * 5, 0);
car_speed.y = 0;
car_speed.z = 0;
move_mode = ignore_you
+ ignore_passable + glide;
ent_move(car_speed,
nullskill);
vec_set (temp,
my.x);
temp.z -= 1000;
trace_mode = ignore_me
+ ignore_passable + scan_texture;
trace (my.x, temp);
if (str_cmpi ("metalplainl1",
tex_name))
{
add_laps();
}
if (my.laps ==
3)
{
my.race_over = 1; // get out of the while loop after 3 laps
}
wait(1);
}

The car will tune its sound depending on the value set for car_speed.x; we trace below the car in order to get the name of the texture used for the road. If that name is "metalplainl1", the car has completed a lap so we must run the function named add_laps(). If the number of laps for the car is 3, we set race_over to 1, in order to get out of the first while loop.

     while (car_speed.x > 1)
{
car_speed.x -=
1 * time;
snd_tune (engine_handle,
50, car_speed.x * 5, 0);
car_speed.y = 0;
car_speed.z = 0;
move_mode = ignore_you
+ ignore_passable + glide;
ent_move(car_speed,
nullskill);
wait (1);
}
snd_stop (engine_handle);
}

The while loop above decreases
the speed of the car until it goes below 1 * time and tunes the sound accordingly.
The car will move slower and slower until it stops completely, because the while
loop stops running. The car engine sound will be shut down as well, and we will
take a look at the last function:

function
add_laps()
{
if (my.counting_laps == 0) {return;}
my.counting_laps = 0;
my.laps += 1;
snd_play (lap_wav, 100, 0);
laps_scr = my.laps;
sleep (10);
my.counting_laps = 1;
}

The car could detect metalplainl1 below its wheels for more than a frame, especially if its speed is small, and this would translate to several add_laps() calls every lap. The solution is easy: we set counting_laps = 0 after the first function call, and we enable the function again only after 10 seconds, when counting_laps = 1. This way we can increase the number of laps for the car (my.laps += 1), we can play a sound (lap_wav) and we can display the number of laps on the screen (laps_scr) only once per lap, provided that the car needs less than 10 seconds to move over the metalplainl1 block.

Some of you might have noticed an isolated level block in Wed's top view:

That's the block that was used for all the curved portions of the track; I have rotated copies of it with 15, 30, 45, 60 ... degrees and I thought that you might want to use it as a base if you plan to modify my track. Next month we will learn to create several cars controlled by the computer and we will see them interacting with each other, so stay tuned!


Untitled

Car AI

This month we will learn to create several cars that are driven by the computer. These cars will use different paths and they will have different speeds every time, so every race will look different. Oh, and each car will use its horn if one of the other cars stands in its way. We will also see a nice "Finish" panel that displays the places occupied by the cars and their individual racing times.

Don't forget to read the article in Aum32 first! You did that already? Ok, let's start with a few definitions:

define
race_over skill20;
define counting_laps skill21;
define laps skill22;
define
max_speed skill23;
define min_speed skill24;
define distance skill25;
define scans
skill26;
define car_speed skill27;

I have named the skills above in a more friendly way; let's see what they mean:
- my.race_over is a skill that will be set to 1 as soon as the car finishes the race;
- my.counting_laps will enable or disable the counter for the number of laps;
- my.laps will hold the number of laps for the car;
- my.max_speed holds the maximum speed for the car; its value changes every lap;
- my.min_speed holds the minimum speed for the car; its value changes every lap;
- my.distance holds the distance covered by the car;
- my.scans holds the number of scans for a specific car
- my.car_speed holds the current speed of the car.

Don't bother to learn what these skills do; we will get back to them later.

entity* car1;
// needed only for the panels
entity* car2;
entity* car3;

I have defined three pointers named car1, car2 and car3; the driving code doesn't need pointers at all, but the texts that appear at the end of the race need them.

text car1_txt
{
font = system_font;
pos_x = 180;
pos_y = 0;
string = car1_str;
layer = 15;
}

text
car2_txt
{
font = system_font;
pos_x = 180;
pos_y = 0;
string = car2_str;
layer = 15;
}

text
car3_txt
{
font = system_font;
pos_x = 180;
pos_y = 0;
string = car3_str;
layer = 15;
}

panel
finish_pan
{
bmap = finish_pcx;
pos_x = 100;
pos_y = 100;
alpha = 50;
layer = 10;
flags = overlay, refresh,
transparent;
}

You can see that these text definitions and the panel are simple; take a look at the picture below to see them in action:

And now here's another picture that shows who is who:

The texts will appear in a random order over finish_pan, depending on the winner of the race.

This was the introduction. How would you create a racing game that includes several cars controlled by the computer? You could use something like this:

You could use those concentric paths indeed, but I wouldn't buy your game. I like to see the cars running into each other, using their horns, and so on; this will never happen with the path layout above. What do we do then? We duplicate a good path several times, we rotate it a little, we edit some of its points and we get something like this:

You can see that these paths overlap a few times; if your track is bigger than mine then you will have plenty of room for more natural (yet wicked) path twists. Ok, let's take a look at function main now:

function
main()
{
randomize();
fps_max = 30;
level_load (carai2_wmb);
wait (3);
camera.x = -3400;
camera.y = 1325;
camera.z = 840;
camera.pan = 326;
camera.tilt = -17;

The first line of code generates a unique sequence of random number every time. We limit the frame rate to 30 fps, we load the level and then we set the correct position and angles for the camera.

     while (1)
{
if (car1.race_over
== 0)
{
car1_time = total_frames / 30;
}
if (car2.race_over
== 0)
{
car2_time = total_frames / 30;
}
if (car3.race_over
== 0)
{
car3_time = total_frames / 30;
}
wait (1);
}
}

The while loop above computes the racing time for every car for as long as the car continues to move (race_over isn't set to 1). The times are stored in car1_time, car2_time and car3_time. Read the article in Aum 32 to get more info about this simple timer system.

Here comes the big, scary action:

action
car
{
var waypoint_number = 0;
var target_coords;
var engine_handle;
var angle_diff;
var engine_offset;
my.race_over = 0;
my.counting_laps = 1;
my.laps = -1;
my.max_speed = 50;
my.min_speed = 50;
my.enable_scan = on;
my.event = make_way;
engine_offset = random(3);

We define a few variables that will store the current waypoint number and the coordinates of the next target (waypoint) on the path. Engine_handle is the handle (the identifier) for the engine sound; angle_diff holds the difference between the current pan angle of the car and the angle of the next waypoint on the path. Engine_offset will create a unique engine sound for every car. The skill that was defined as race_over is set to zero (the race is starting, not ending) and the lap counter is enabled (counting_laps = 1). The number of laps is set to -1 (it will be set to zero as soon as the cars start the race). The initial max_speed and min_speed values are set to 50, but that will change in a moment, as soon as the cars cross the starting line (well, it's a block, not a line, but let's pretend that it is a line for now).

The car is sensitive to scanning; when it is scanned by another car, its make_way event is run. Finally, engine_offset is set to a random value from 0 to 3.

     if (my.skill1 == 1) // don't forget to set skill1
= 1 for the first car, skill1 = 2 for the second car and so on
{
ent_path("car01");
// if skill1 = 1 the car will follow the path named "car01" without the quotes
car1 = me; // I'm
car 1
}
if (my.skill1 == 2) // same thing for the rest
of the cars
{
ent_path("car02");
car2 = me;
}
if (my.skill1 == 3)
{
ent_path("car03");
car3 = me;
}
engine_handle = ent_playloop (my, engine_wav,
50); // play the engine sound in a loop
ent_waypoint(target_coords, 1); // set the target coordinates to the first point
on the path

The first car has its skill1 set to 1, so it will use the "car01" path. This car will be known as "car1" from now on, because we have assigned it the pointer with this name. The same things happen for the rest of the cars; don't forget to set their skill1 to different values!

The engine_wav sound will be played in a loop; the result of the operation is stored in engine_handle. The next line of code sets the target coordinates to the first point on the path.

while (my.race_over == 0) // this loop will run until my.race_over = 1
{
temp.x = 60; // horizontal scanning angle
temp.y = 60; // vertical scanning angle
temp.z = 150; // scanning range
scan_entity (my.x, temp);

          temp.x = target_coords.x
- my.x;
temp.y = target_coords.y
- my.y;
temp.z = 0;
if (vec_to_angle
(angle_diff.pan, temp) < 150)
{
waypoint_number = ent_nextpoint(target_coords);
}
angle_diff.pan
= ang(angle_diff.pan - my.pan);
my.pan += my.car_speed
* angle_diff.pan * 0.005;

The while loop above will run until my.race_over is set to 1, and this happens only at the end of the race. The car scans in front of it, detecting the other cars nearby (if any). Take a look at the picture below:

If one of the other cars invades the scanning cone, the event function associated to the car will run. We'll talk about the event soon; let's get back to our code.

We get the vector that points toward the target and we store it in temp. If the car comes closer than 150 quants to the target, we tell her to move towards the next point on the path (that's what ent_nexpoint does). We get the difference angle between the current pan angle of the car and the orientation of the next point on the path, and then we rotate the car slowly towards the destination.

          if (((waypoint_number
> 65) || (waypoint_number < 8)) || ((waypoint_number > 34) &&
(waypoint_number < 49))) // horizontal areas of the track?
{
if (my.car_speed < (my.max_speed * time)) // and the speed is below max_speed?
{
my.car_speed += 1 * time; // increase the speed of the car
}
}
else // curved
areas of the track
{
if (my.car_speed > (my.min_speed * time)) // and the speed is above min_speed?
{
my.car_speed -= 0.5 * time; // decrease the speed of the car
}
}
my.skill28 = 0;
// second skill used for car_speed, we are using skill27..skill29 (skill27 was
defined as car_speed)
my.skill29 = 0;
// third skill used for car_speed
move_mode = ignore_you
+ ignore_passable + glide;
my.skill2 = ent_move
(my.car_speed, nullskill);
my.distance +=
my.skill2;
snd_tune (engine_handle,
50, (my.skill2 * 5 + engine_offset), 0);
vec_set (temp,
my.x);
temp.z -= 1000;
trace_mode = ignore_me
+ ignore_passable + scan_texture;
trace (my.x, temp);
// trace 1000 quants below the car
if (str_cmpi ("checker1",
tex_name))
{
add_laps( );
}

If the car moves on a horizontal portion of the track and its speed is below max_speed, we increase the speed of the car; otherwise, if the car is moving on a curved area and its speed is above min_speed, we decrease the speed of the car. For those of you that are too lazy to read the article in Aum 32 again, I have attached the picture that shows where those numbers came from:

We set skill28 and 29 to zero (those would be the y and z components of the car_speed vector, aka skill27...29). The car will move ignoring passable entities and gliding along walls and entities; the distance covered by the car in a frame will be stored in skill2 and the total distance covered by the car will be stored in my.distance. The engine sound is tuned depending on the "real" speed of the car, based on the distance covered by it. Please note that I have also added the random engine_offset, in order to generate that unique engine sound I've told you about. Finally, we trace 1000 quants below the car and we get the name of the road texture. If the name is "checker1", the car has passed over the starting block and function add_laps() is called.

          if (my.laps ==
3) // end of race?
{
my.race_over = 1; // get out of the while loop after 3 laps
place += 1;
if (place == 1) // i'm the first car that finishes the race?
{
if (my.skill1 == 1)
{
str_cpy (car1_str, "1st place: Green car
Time: ");
str_for_num (temp_str, car1_time);
str_cat (car1_str, temp_str);
car1_txt.pos_y = 250;
car1_txt.visible = on;
}
if (my.skill1 == 2)
{
str_cpy (car2_str, "1st place: Red car
Time: ");
str_for_num (temp_str, car2_time);
str_cat (car2_str, temp_str);
car2_txt.pos_y = 250;
car2_txt.visible = on;
}
if (my.skill1 == 3)
{
str_cpy (car3_str, "1st place: Blue car
Time: ");
str_for_num (temp_str, car3_time);
str_cat (car3_str, temp_str);
car3_txt.pos_y = 250;
car3_txt.visible = on;
}
}

If the number of laps is 3 the race has ended for this car; therefore, we set my.race_over to 1, we increment "place" (the place occupied by the car at the end of the race) and we check the value of "place". If place = 1, this car is the first that has finished the race. If its skill1 is set to 1, the first car is the green car (I know that because I've set skill1 to 1 for it in Wed). We write "1st place: Green car.... " in the string that will be used by car1_txt, we convert car1_time from number to string and then we add this string to our "1st place: Green car... " string. We set a good pos_y value for the text and then we make it visible. The result will look like this (just an example):

1st place: Green car Time: 50.342

If my.skill1 = 2, then the red car is the winner (once again, I know I've set skill1 = 2 for it in Wed). We must use a different string this time: "1st place: Red car....", we convert car2_time from number to string and then we add it to the "1st place: Red car..." string. We set a good pos_y value for the text and we display it. The result is shown below:

1st place: Red car Time: 51.324

If my.skill1 = 3, the blue car is the winner and the things work the same way for it.

if (place == 2) // i'm the second car that finishes the race?
{
if (my.skill1 == 1)
{
str_cpy (car1_str, "2nd place: Green car
Time: ");
str_for_num (temp_str, car1_time);
str_cat (car1_str, temp_str);
car1_txt.pos_y = 280;
car1_txt.visible = on;
}
if (my.skill1 == 2)
{
str_cpy (car2_str, "2nd place: Red car
Time: ");
str_for_num (temp_str, car2_time);
str_cat (car2_str, temp_str);
car2_txt.pos_y = 280;
car2_txt.visible = on;
}
if (my.skill1 == 3)
{
str_cpy (car3_str, "2nd place: Blue car
Time: ");
str_for_num (temp_str, car3_time);
str_cat (car3_str, temp_str);
car3_txt.pos_y = 280;
car3_txt.visible = on;
}
}

The things repeat exactly for the second car that finishes the race; the only difference here is the value used for pos_y of the text, which is bigger with 30 pixels, so the second text will be displayed below the first text.

if (place == 3) // i'm the third car that finishes the race?
{
if (my.skill1 == 1)
{
str_cpy (car1_str, "3rd place: Green car
Time: ");
str_for_num (temp_str, car1_time);
str_cat (car1_str, temp_str);
car1_txt.pos_y = 310;
car1_txt.visible = on;
}
if (my.skill1 == 2)
{
str_cpy (car2_str, "3rd place: Red car
Time: ");
str_for_num (temp_str, car2_time);
str_cat (car2_str, temp_str);
car2_txt.pos_y = 310;
car2_txt.visible = on;
}
if (my.skill1 == 3)
{
str_cpy (car3_str, "3rd place: Blue car
Time: ");
str_for_num (temp_str, car3_time);
str_cat (car3_str, temp_str);
car3_txt.pos_y = 310;
car3_txt.visible = on;
}
}
finish_pan.visible = on;
}
wait(1);
}

The same things happen with the third car; the only notable change is a new pos_y value, which makes the text appear even closer to the bottom of the screen, after the first two texts.

while (my.car_speed > 1) // decrease the speed from its current speed to 1
{
my.car_speed -= 1 * time;
snd_tune (engine_handle, 50, my.car_speed * 5, 0); // and tune the sound accordingly
my.skill28 = 0;
my.skill29 = 0;
move_mode = ignore_you + ignore_passable + glide;
ent_move (my.car_speed, nullskill);
wait (1);
}
snd_stop (engine_handle); // stop the engine sound
if (place == 3) // race finished for all the cars?
{
while (finish_pan.alpha < 95)
{
finish_pan.alpha += 2 * time; // increase alpha
wait (1);
}
finish_pan.transparent = off; // the panel is opaque now
}
}

The race is over here, so we decrease the speed from its current value to 1, tuning the sound accordingly. As soon as the speed goes below 1, we stop the engine sound (the car isn't moving here anyway). If place = 3 (all the cars have finished the race) we increase the alpha for finish_pan in a while loop and then we make it opaque, because all the cars are stopped now, so there isn't anything interesting to see on the screen.

Time to look at two tiny functions:

function
add_laps()
{
if (my.counting_laps == 0) {return;}
my.max_speed = 50 + random(10);
my.min_speed = 30 + random(10);
my.laps += 1;
snd_play (lap_wav, 100, 0);
my.counting_laps = 0;
sleep (10);
my.counting_laps = 1;
}

You know that we are using a trace instruction to keep track of the number of laps, right? When one of the cars passes over the starting line (a block that was textured with the "checker1" texture) function add_laps is called. There is a small problem though: a car could trigger more than a single add_laps call if its speed is too small. If the car will hover over that block for 4 frames, function add_laps would be called four times, get it?

That's why I'm using my.counting_laps; if the car passes over the "checker1" block, the number of laps is incremented, but this function won't accept other calls for 10 seconds (only the first line of code inside it will run). We set random max_speed and min_speed values for each car every lap, so the results of the race will be unpredictable, at least for me :) We play a sound and like I said, we stop adding laps for 10 seconds. You can use any value here, as long as it is smaller than the time that is needed to complete a lap.

The final function runs when a car is scanned by the car behind it:

function make_way()
{
my.scans += 1; // increase the number of scan
if (you.car_speed >= 5) // the speed of the car is above 5?
{
you.car_speed -= 0.5 * time; // decrease the speed of the car that has scanned me. Play with 0.5
}
if (my.scans > 5) // too many scans? play with "5"
{
ent_playsound (my, horn_wav, 2000); // I'll use my horn then
my.enable_scan = off; // stop reacting to scans
sleep (1); // for one second
my.enable_scan = on; // enable scans again
my.scans = 0; // reset the number of scans (prepare for a new horn)
}
}

We increase the number stored in my.scans and if the speed of the car that has scanned this scar is above 5, we decrease it slowly. This way the car behind will avoid most of the collisions with the car in front of it. If my.scans is over 5, we play a horn_wav sound, we stop reacting to scans for one second and then we reset my.scans, preparing it for another horn_wav. That's all!

Well, almost. This code runs ok with A5 and A6, but I encourage you to buy A6 because it uses polygon based collisions for models, which means that the cars won't penetrate each other if you replace ent_move with the new A6 movement instruction c_move. Next month we will learn to create the code used for player's car.


Untitled

Car AI

This month we will learn to create the code that will be used for player's car. Our code creates a car that changes its gears automatically, uses its horn, its brake lights, includes a speedometer, and more! One more thing: the car can use 3 different cameras.

All the new code can be found in playercar.wdl, which was included in carai3.wdl, a slightly modified version of the carai2.wdl file. Let's start with a few simple definitions:

panel speedo_pan
{
bmap = speedo_pcx;
pos_x = 640;
pos_y = 450;
layer = 10;
flags = overlay, refresh, visible;
}

entity
needle
{
type = <needle.tga>;
layer
= 20;
scale_x = 0.15;
scale_y = 0.25;
x = 200;
y = -91.5;
z = -65;
view = camera;
flags = overlay, nofilter, visible;
}

I am using the panel and the needle entity to create a speedometer. Why do I need to use an entity for the needle? Well, because a panel can't be rotated, but an entity that changes its roll angle will do the job. You can change scale_x and scale_y to adjust the width and length of the needle; change x to set the proper position of the needle in front of the camera, y and z to set the position on the screen (on the x and y axis).

starter
set_needle()
{
var needle_angle;
while (player
== null) {wait (1);}
while (1)
{
needle_angle = player.skill2 * 12.17 - 140; // -140...+140 degrees = 0...140 mph
needle.roll = max (-140, needle_angle);
wait (1);
}
}

The function above will run by itself; its purpose is to set a correct roll angle for the needle entity, depending on the real speed of the car. We define a local variable named needle_angle, we wait until the player entity is loaded, and then we encounter a while (1) loop with some weird numbers inside it. Where did they come from?

The speed ranges from 0 to 140 m.p.h. but the needle must change its roll angle from -140 to 140 degrees. I know that player.skill2 ranges from 0 to 23 quants / frame. How do I know that? I have used a simple panel with a digit that displays player.skill2 to measure these values with the car being stopped or driven at its maximum speed. Let's take a look at the weird looking formula again:

needle_angle = (0 to 23) * 12.17 - 140

a) needle_angle = 0 * 12.17 - 140 = -140 degrees

b) needle_angle = 23 * 12.17 - 140 = 139.91 degrees

The formula above prooves that needle will change its angle from -140 to 139.91 degrees when the speed ranges from 0 to 23 quants / frame. The next line of code inside the while loop makes sure that the roll angle of the needle doesn't go below -140 degrees.

I'm not going to discuss the function that was used for player's horn because it is way to simple - see for yourself:

function
horn()
{
snd_play (plhorn_wav, 100, 0);
}

on_h = horn;

Here's another simple function called from within player's action; that's the function that allows us to select one of the 3 cameras I've told you about:

function
car_camera()
{
if (key_1 == on) {camera_number = 1;}
if (key_2 == on) {camera_number = 2;}
if (key_3 == on)
{camera_number = 3;}
if (camera_number == 1) // top view
{
camera.x = player.x;
camera.y = player.y;
camera.z = player.z +
900; // play with this value
camera.pan = player.pan;
camera.tilt = -90;
}

The player can choose one of these cameras by pressing the "1", "2" or "3" keys on the keyboard. If we press "1", the camera will have the same x and y positions with the car, but it will be placed 900 quants above the car, looking downwards.

if (camera_number == 2) // simulated first person view
{
camera.x = player.x - 20 * cos(player.pan);
camera.y = player.y - 20 * sin(player.pan);
camera.z = player.z + 18;
camera.pan = player.pan;
camera.tilt = 0;
}

If the player presses "2", he will get a first person view. This is, in fact, an isometric view that is placed 20 quants behind the origin of the car and 18 quants upwards.

  if (camera_number == 3) // true isometric view
{
camera.x = player.x - 200 * cos(player.pan); // 200 =
distance
camera.y = player.y - 200 * sin(player.pan);
// same value here
camera.z = player.z + 150;
// above the player
camera.pan = player.pan;
camera.tilt = -27; // look down at the player
}
wait (1);
}

If the
player presses "3", he will get a true isometric view. The camera is placed 200
quants behind the car anf 150 quants above it; its tilt angle of -27 degrees ensures
us that the player will see a good part of the road ahead.

That was the easy part of the article. Prepare to meet the action that drives player's car (insert creepy laughter here).

action
players_car
{
var players_engine; // engine sound handle
var player_dist;
var player_speed;
var minimum_z;
player = my; // I'm the player
players_engine = ent_playloop (my, plengine_wav, 50);

I have declared a bunch of local variables and I have associated the "player" pointer to "my" car entity. The car will play the plengine_wav sound in a loop.

while (1)
{
player_speed.x = movement_speed * (key_cuu - 0.4 * key_cud); // backwards movement speed = 40%
my.skill1 = player_speed.x * time + max (1 - time * 0.02, 0) * my.skill1;
player_dist.x = my.skill1 * time;
player_dist.y = 0;

          vec_set (temp,
my.x);
temp.z -= 1000;
trace_mode = ignore_me + scan_texture;
player_dist.z = -trace (my.x, temp) - my.min_z;

The player accelerates / decelerates when we press the "up" / "down" arrow keys. Please note that the backwards movement speed can be up to 0.4 = 40% off the maximum speed. The following line of code computes the speed of the car; I have set a friction factor of 0.02 but you might want to try a different value here. The car will move using a time - corrected player_dist.x; the y component of the speed will be zero.

The next lines of code sets the proper height (z) for the car. We use a trace instruction to get the floor level and the name of the texture below the car.

The red line simulates the trace, while the green arrow shows the distance returned by trace. Finally, we subtract my.min_z, which is the distance between the point with the lowest z value and the origin of the car. If you plan to use an animated (for example, destroyable) model for your car, you should use vec_for_min to set the proper min_z value every frame. You will find the code that does this in the playercar.wdl script file.

          if (str_cmpi (tex_name,
"tilegrass"))
{
if (movement_speed > 0.1)
{
movement_speed -= 5 * time;
}
else
{
movement_speed = 0.1;
}
}
else
{
if (movement_speed < 0.9)
{
movement_speed += 10 * time;
}
else
{
movement_speed = 0.9;
}
}

If the name of the texture below the wheels is "tilegrass", the car will decrease its movement speed until it goes below 0.1. If the name of the texture detected by trace isn't "tilegrass", the car will increase its speed until it goes up to 0.9. This way the player will not be encouraged to cheat by using "shortcuts".

         move_mode = ignore_passable
+ glide;
my.skill2 = ent_move
(player_dist, nullvector); // now ent_move the player
if (my.skill2 > 0) // change gears automatically, depending on
the value stored in skill2
{
engine_freq = 15;
}
if (my.skill2 > 8)
{
engine_freq = 20;
}
if (my.skill2 > 15)
{
engine_freq
= 22;
}
if (my.skill2 > 19)
{
engine_freq = 23.5;
}

The car moves ignoring passable entities and gliding along the obstacles that come its way; we store the result (the distance covered each frame) in my.skill2. If the value stored there is positive, the car will choose the proper gear, depending on the speed of the car (my.skill2). I chose the sound tuning factors and the values for skill2 by trial and error; feel free to choose your own values.

       player_speed.pan = rotation_speed * (key_cul
- key_cur) * my.skill2;
my.skill3 = time * player_speed.pan
+ max (1 - time * 0.7, 0) * my.skill3;
my.pan
+= my.skill3 * time;

my.skill4 = max (50,
my.skill2 * engine_freq); // play an engine sound at the minimum speed too
snd_tune (players_engine, 50, my.skill4, 0); // tune the engine sound

The car can rotate (change its pan) only if my.skill2 is bigger than zero, which happens when the car is moving. We use a similar formula to compute the rotation speed, this time using a bigger friction factor. The rotation speed is also a time - corrected value.

We want to tune the engine sound using the value stored in my.skill4 - let's take another look at the line below to see how that skill4 value is computed:

my.skill4 = max (50, my.skill2 * engine_freq); // play an engine sound at the minimum speed too

I wanted to hear an engine sound even when the car isn't moving; therefore, my.skill4 will choose the biggest value between 50 and my.skill2 * engine_freq every frame. If the car is stopped, the engine sound will be tuned using "50"; otherwise, the engine will be tuned depending on the speed of the car (my.skill2) and on the gear number (given by the value set for engine_freq).

if (key_cud == on)
{
my.skin = 2;
}
else
{
my.skin = 1;
}

car_camera(); // update the camera position
wait(1);
}
}

If we press the "down" arrow key, we've hit the brakes, so the car will use its second skin, the one that has the light breaks on. If we don't press "down", the car will use its first, normal skin. That's a simple and efficient method but uses additional video memory (about 100Kb more for player's car). The last few lines call the function car_camera and instruct the engine to wait for a frame.

Come on, the code for the car wasn't that complicated! I wanted to write the code for a simple arcade game after all, not the code for a simulator! Next month we will put together what we've learned so far and we will add what's missing to our racing game.


Untitled

Car AI

This time we will get to play a demo of our game, so open carai4.wmp, build it and run it using carai4.wdl.

The cars will wait until "3" flashes on the screen, but you'd better keep your finger pressed on the "up" arrow key! Well, the speeds for the computer-driven cars aren't too big, so you should win most of the races. Play with the speeds of the cars until you lose most of the races and sell that version of the game. Btw, you can set a different max_laps value in carai4.wdl to get a different number of laps for your game.

I started to write the code for this new AI series a few months ago; you might want to read those articles first. You will see that the "Finish" panel that appears at the end of the race includes the player now; the code used for that is similar to the old code, so I'm not going to explain it again. Let's concentrate on the new stuff that was added to the demo then:

- We've got a decent intro sequence;

- The cars pick a random path at the end of every lap;

- The distance, the current speed and the path number are displayed on a panel that appears at the top of the screen.

There are a few more minor changes to the old player / car AI code, but they are commented heavily. Let's start with the code that is used for the intro:

starter
init_race()
{
sleep (2);
one_ent.visible
= on;
snd_play (lo_wav, 50, 0);
while (one_ent.x
> 100)
{
one_ent.x -= 50
* time;
wait (1);
}
one_ent.visible = off;
two_ent.visible = on;
snd_play (lo_wav, 50, 0);
while (two_ent.x > 100)
{
two_ent.x -= 50 * time;
wait (1);
}
two_ent.visible
= off;
three_ent.visible = on;
snd_play (high_wav,
100, 0);
while (three_ent.x > 100)
{
three_ent.x -= 150 * time;
wait (1);
}
race_started = 1;
three_ent.visible = off;
}

The function above starts by itself when we run the script file. The first line of code gives us 2 seconds to admire the view :) and then makes visible the first figure (one_ent). I have used 3 "entity" definitions and 3 sprites for the 3 figures that appear at game start. Each entity (figure) is moved towards the camera (its x is decreased inside a "while" loop) until it is really close to the camera; we play a sound and then we hide the old figure, displaying the one that follows it. As soon as "3" is closer than 100 quants to the camera, race_started is set to 1 and "3" is made invisible. All the cars (including player's car) have a "while" loop that instructs them to wait until race_started is set to 1, so they will start their engines as soon as that happens.

If you have read the article in Aum33, you know that each computer-driven car uses a path for its movement. This method has many advantages, but it might lead to boring driving simulations; your customers might discover that the cars use the same paths over and over, so they will know for sure that (as an example) the green car isn't touching the inner part of the track! Don't worry, the cars in this month's demo use random paths all the time. You will be surprised to hear that the algorithm isn't using random values, and yet the paths are completely random.

I have used an array definition that looks like this:

var paths[12] = 3, 1, 2, 1, 3, 2, 3, 2, 1, 1, 3, 2;

I have used different colors for different sets of values to make the things easier for you. The cars start with the default paths given by the variables below:

var
car1_path = 1;
var car2_path = 2;
var car3_path = 3;

Let's see how they choose a random path every lap. Every AI-driven car includes a few lines like these:

if
(waypoint_number == 66)
{
pick_new_path();
}

As soon as one of the cars gets close to the end of the lap (waypoint_number = 66), it runs the function pick_new_path, which chooses a new path for the car. A possible result of the function call is shown in the picture above.

function
pick_new_path()
{
if (my.skill40 == 1) {return;} // the car
has picked its path already
my.skill40 = 1;
index += 1; // advance one more step inside the array
index %=
12; // limit index to the first 12 (0..11) elements in the array
if (my.skill1 == 1)
{
car1_path
= paths[index];
}
if (my.skill1 == 2)
{
car2_path = paths[index];
}
if (my.skill1 == 3)
{
car3_path = paths[index];
}
if (paths[index]
== 1)
{
ent_path("car01");
}
if (paths[index] == 2)
{
ent_path("car01");
}
if (paths[index] == 3)
{
ent_path("car03");
}
sleep (10); // wait for 10 seconds
my.skill40 = 0; // the car can pick a random path again (waypoint_number = 66
for several frames)
}

You should trust me when I tell you that any car will have its waypoint_number set to 66 for several frames. This happens because the speed of the car isn't extremely big and the distance between two consecutive nodes on the path isn't extremely small :) Our cars should change their paths only once per lap, so we set skill40 to 1 at first, and then we reject all the other function calls if skill40 is set to 1. We increase index, we limit its value to 11 because our "paths" array uses elements from 0 to 11, and then we get the path number from "paths" for the car that has called the function. Please allow me to remind you that the first car has its skill1 set to 1 in Wed, the second car has its skill1 set to 2, and the third car has its skill1 set to 3. The next "if" branches assign one of the 3 paths (car01, car02 or car03) to the car, depending on the value that was read from "paths". The last few lines in our function wait for 10 seconds (waypoint_number should have a different value by now) and then set back skill40 to zero, allowing a new function call for next lap.

Maybe you are wondering how does this randomness work; why did I say that the paths are random, when I read their values from a predefined array? Well, the trick here is that we don't know which car is the winner. The cars use random speed values every lap, so any car could pick the second path, for example. All you need to do is to make sure that every triplet of values inside the array includes "1", "2" and "3" because you won't want your cars to bump into each other too often.

panel
race_pan
{
bmap = race_pcx;
pos_x = 0;
pos_y = 0;
layer = 10;

   digits = 323, 18, 1, system_font, 1, car1_path; // the path
used by the first car
digits = 510, 18, 1, system_font, 1, car2_path;
// the path used by the second car
digits = 703, 18, 1, system_font,
1, car3_path; // the path used by the third car

   digits = 100, 18, 3, system_font, 1, players_speed; // the
speed of the 1st car
digits = 280, 18, 3, system_font, 1, car1_speed;
// the speed of the 2nd car
digits = 470, 18, 3, system_font,
1, car2_speed; // the speed of the 3rd car
digits = 660, 18, 3,
system_font, 1, car3_speed; // the speed of the player

digits = 64, 18, 3, system_font, 0.01, player.distance; // the distance covered by player's car
digits = 244, 18, 3, system_font, 0.01, car1.distance; // the distance covered by the 1st car
digits = 434, 18, 3, system_font, 0.01, car2.distance; // the distance covered by the 2nd car
digits = 624, 18, 3, system_font, 0.01, car3.distance; // the distance covered by the 3rd car

flags = overlay, refresh, visible;
}

You can see that the panel used in the demo isn't too complicated; it includes "digits" for the paths, the speeds and the distances covered by the cars. The figures for the speeds weren't looking too good; 20 quants a frame isn't a value you would want to display on a speedometer, so I had to write a small function that changes the speeds into something that looks more decent.

starter
compute_speeds() // just makes the speeds look good, really
{
while ((car1 == null) || (car2 == null) || (car3 == null) || (player == null))
{wait (1);}
while (1)
{
car1_speed = car1.car_speed * 6; // 6 = experimental value (same read on the panel
and on the speedo)
car2_speed = car2.car_speed
* 6;
car3_speed = car3.car_speed * 6;
players_speed = player.skill2 * 6; // player.skill2 =
player's "car_speed"
wait (1);
}
}

This function waits until all the cars and the player are created and then it multiplies the speeds in quants / frame with 6 (that's an experimental value); this is how we get the same speed for the player on the speedometer and on the panel. The last set of digits displays the distances (multiplied by 0.01) covered by the cars; those values were stored in skill25 aka "distance".

This article concludes the car AI series. The demo isn't a NFS killer, but it shows you how to build a racing arcade game from scratch. The weak point of the demo is the collision system that allows the cars to penetrate each other. I know that the A6 user will be happy with the new collision system that is planned for A6.3 (though I might be wrong about the version number) but if you own A5 you will have to live with this problem.

Hold on, I think I've found a solution for the A5 users too! Why don't you build your cars using wmb entities? Try this method and share your results with the rest of us using Conitec's forum. Good luck!


Untitled

Customizable user interface

I have to admit that I'm a conservative person. The movement keys need to be configured in a certain way, the hud has to have a certain size and position on the screen, and so on. This article will teach you to create a panel that can be dragged to one of the four corners of the screen, snapping to the correct position automatically.

panel hud_pan
{
bmap = hud_pcx;
layer = 20;
pos_x = 0;
pos_y = 0;
on_click = drag_panel;
flags = overlay, refresh, visible;
}

That's your regular panel definition; the only thing that needs our attention is the function that is run every time we click the panel:

function
drag_panel()
{
while (mouse_left == on)
{
hud_pan.pos_x = pointer.x - bmap_width(hud_pcx) / 2;
hud_pan.pos_y = pointer.y - bmap_height(hud_pcx) / 2;
wait (1);
}

The "while" loop above will run for as long as the player keeps the left mouse button pressed; it keeps the center of the panel to the coordinates set by pointer.x and pointer.y

  if ((hud_pan.pos_x + bmap_width(hud_pcx) / 2) > (screen_size.x / 2))
// using the right side of the screen
{
if ((hud_pan.pos_y + bmap_height(hud_pcx) / 2) > (screen_size.y / 2)) // lower
right corner?
{
hud_pan.pos_x = screen_size.x - bmap_width(hud_pcx);
hud_pan.pos_y = screen_size.y - bmap_height(hud_pcx);
}
else // upper right corner
{
hud_pan.pos_x = screen_size.x
- bmap_width(hud_pcx);
hud_pan.pos_y
= 0;
}
}

If the center of the panel has a pos_x that is greater than half of the screen resolution on the x axis, we do the same test on the y axis and we place hud_pan accordingly in the lower right or the upper right corner.

  else // using the left side of the screen
{
if ((hud_pan.pos_y + bmap_height(hud_pcx) / 2) > (screen_size.y
/ 2)) // lower left corner?
{
hud_pan.pos_x = 0;
hud_pan.pos_y = screen_size.y
- bmap_height(hud_pcx);
}
else // upper left corner
{
hud_pan.pos_x = 0;
hud_pan.pos_y = 0;
}
}
}

If the panel has a small pos_x value, it will be placed on the left side of the screen. We test its pos_y and we place it in the lower left or the upper left corner. We've got another small function to talk about:

starter
init_mouse()
{
mouse_map = pointer_pcx;
while
(1)
{
if (key_space == on)
{
mouse_mode = 2;
}
else
{
mouse_mode = 0;
}
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
wait (1);
}
}

This starter shows the mouse pointer if we press and hold the "Space"
key, allowing us to move the hud panel where we want it. You can see that the
code is pretty flexible; you only need to define the panel and the code will do
the rest of the job for you.


Cutscene camera code

Here's how it works: you press R (record) and move the camera around the player (or wherever you want) ; all this time camera's coordinates (x y z pan tilt roll) are stored in a huge array. Press R again to stop recording - a small file (cutdata.txt) is saved on your hard drive. Now you can load it and play it whenever you want. Let's take a look at the code:

view cutscene_camera{}
var start_pos[6];

action
cutscene_on
{
start_pos[0]
= my.x;
start_pos[1] = my.y;
start_pos[2] = my.z;
start_pos[3] = my.pan;
start_pos[4] = my.tilt;
start_pos[5] = my.roll;

waitt (64);

    camera.visible = off;
cutscene_camera.size_x
= screen_size.x;
cutscene_camera.size_y
= screen_size.y;
cutscene_camera.pos_x
= 0;
cutscene_camera.pos_y
= 0;
cutscene_camera.x
= my.x;
cutscene_camera.y
= my.y;
cutscene_camera.z
= my.z;
cutscene_camera.visible
= on;
on_mouse_right
= null;
while (1)
{
cutscene_camera.x += 30 * cos(cutscene_camera.pan)
* (mouse_right - mouse_left) * time;
cutscene_camera.y
+= 30 * sin(cutscene_camera.pan) * (mouse_right - mouse_left) * time;
cutscene_camera.z
+= 100 * sin(cutscene_camera.tilt) * (mouse_right - mouse_left) * time;
cutscene_camera.pan
-= 20 * (mouse_force.x) * time;
cutscene_camera.tilt += 25 * (mouse_force.y)
* time;
if (key_r == 1 && index ==
0) // if we press r and we aren't recording already
{
record_data();
}
if (key_p == 1) // if we press p
{
play_data();
}
wait (1);
}
}

First of all you have to place an entity in the level and attach it the cutscene_on action; its position should be close to the final cut scene position. We store the initial position (x y z pan tilt roll) in start_pos[6] and then we wait 4 seconds to make sure that the splash screen has disappeared. I have created a new view named cutscene_camera that will replace the default view (camera). The new cutscene_camera view moves using the mouse and its buttons (including the right mouse button - RMB) so I had to disable other right click actions. The code that moves the cutscene_camera is simple (take a look at the cameras in Aum4 - I have similar code there). Maybe you are wondering what's with (mouse_right - mouse_left). When we press the RMB, mouse_right = 1 and the camera will move forward; when we press the LMB, mouse_right = 0 and mouse_left = 1 so the camera will move backwards. If both buttons are pressed the camera won't move because (mouse_right - mouse_left) = 0 (this happens if none of them is pressed, too).

if (key_r == 1
&& index == 0)
{
record_data();
}
if (key_p == 1)
{
play_data();
}

If we press R and we aren't
recording already record_data is called; we should press P to load and play the
previously stored data.

Function record_data stores camera's coordinates in the array and saves the file that contains it on the hard disk.

var cutscene_position[60000];

function
record_data()
{
index
= 0;
while (index < 60000)
{
cutscene_position[index] =
0;
index += 1;
}
index
= 0;
while (key_r == 0 && index
< 59994)
{
cutscene_position[index+0]
= cutscene_camera.x;
cutscene_position[index+1]
= cutscene_camera.y;
cutscene_position[index+2]
= cutscene_camera.z;
cutscene_position[index+3]
= cutscene_camera.pan;
cutscene_position[index+4]
= cutscene_camera.tilt;
cutscene_position[index+5]
= cutscene_camera.roll;
index += 6;
wait (1);
}
filehandle
= file_open_write("cutdata.txt");
temp = 0;
while (temp <= index)
{
file_var_write(filehandle, cutscene_position[temp]);
temp += 1;
}
file_close(filehandle);
beep;
}

First of all we reset the array to make sure that its end won't contain data from previously recorded cut scenes. I have used an array with 60,000 elements; we use 6 of them every frame (x y z pan tilt roll) so this is enough for 10,000 frames; even if your game runs with 100 fps you can record for 100 seconds (increase the number of elements if this isn't enough for you).

The recording stops when we press R again or when the array has been filled. The first instruction after the while loop opens (or creates and opens, if it doesn't exist) the file named cutdata.txt. The data in cutscene_position is written in this file; in the end a simple beep tells us that the recording is really over.

The last function opens the cutdata.txt file, reads its content and restores the cutscene_camera positions every frame:

function
play_data()
{
filehandle
= file_open_read("cutdata.txt");
index
= 0;
temp = 0;
while (key_l == 0 || temp < 59994)
{
cutscene_camera.x = file_var_read(filehandle);
cutscene_camera.y = file_var_read(filehandle);
cutscene_camera.z = file_var_read(filehandle);
cutscene_camera.pan =
file_var_read(filehandle);
cutscene_camera.tilt
= file_var_read(filehandle);
cutscene_camera.roll
= file_var_read(filehandle);
if (cutscene_camera.x
+ cutscene_camera.y + cutscene_camera.z + cutscene_camera.pan + cutscene_camera.tilt
+ cutscene_camera.roll == 0)
{
index = 0;
cutscene_camera.x = start_pos[0];
cutscene_camera.y = start_pos[1];
cutscene_camera.z = start_pos[2];
cutscene_camera.pan = start_pos[3];
cutscene_camera.tilt = start_pos[4];
cutscene_camera.roll = start_pos[5];
return;
}
temp += 1;
wait (1);
}
}

The camera stops when we have reached the end of the file (temp > 59994) or when we find the first zero elements in the array (look at the if structure - there's the test); the camera will be moved to its initial position. There are two small panels for recording (red) and playing (green) but they're way too simple.


Untitled

Darts

If your hands aren't shaking you might get a decent score but if you can't control them you will certainly get a high score! Just run the project and you will see for yourself...

Let's take a look at function main - it does a lot of things:

function
main()
{
camera.arc = 60;
fps_max = 30;
level_load (board_wmb);
wait (2);
mouse_map = mouse_pcx; //
2d dart picture
mouse_mode = 2;
mouse_pos.x = 372;
mouse_pos.y = 272;

We set the field of view to 60 degrees, we limit the frame rate to 30 fps and then we load the level. We use mouse_pcx as the mouse pointer (it is a picture of a dart taken from behind) and then we place the mouse pointer in the center of the screen; we substract 28 pixels on x and y because the mouse has a 56 x 56 pixels bitmap.

     while (1)
{
if (key_t == 1)
{
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
}
else
{
counter += 1;
if (counter == 1)
{
random_x = 20 - int(random(40));
random_y = 20 - int(random(40));
}
if (mouse_pos.x < pointer.x + random_x) {mouse_pos.x += 1;}
if (mouse_pos.x > pointer.x + random_x) {mouse_pos.x -= 1;}
if (mouse_pos.y < pointer.y + random_y) {mouse_pos.y += 1;}
if (mouse_pos.y > pointer.y + random_y) {mouse_pos.y -= 1;}
if ((mouse_pos.x == pointer.x + random_x) && (mouse_pos.y == pointer.y
+ random_y))
{
counter = 0;
}
}

If we press and hold "T" the mouse pointer can be moved without effort on the entire screen. However, this movement mode should be used only for calibration because it isn't fun to move the mouse over the center of the dartboard and throw five darts in the center. We have to make the game a little harder - that's what the rest of the code does for us.

I'm using a simple var named counter; its initial value is zero, but counter += 1 adds something to it. If counter = 1 we generate a random target that can be -20...20 pixels away from the current mouse position. The mouse will move towards the random target and when it reaches it a new random target will be set and so on. This looks like a clean, elegant shaking of the pointer (dart) on the screen and makes the game more difficult and (of course) more fun. Counter is reset when the pointer has reached its random target.

          if (mouse_left
== 1 && number_of_darts < 5)
{
while (mouse_left == 1) {wait (1);}
dart_coords.x = mouse_pos.x + 28;
dart_coords.y = mouse_pos.y + 28;
dart_coords.z = 100;
vec_for_screen(dart_coords, camera);
ent_create(dart_mdl, dart_coords, move_dart);
}
wait (1);
}
}

If we press the left mouse button (LMB) and we haven't fired all the five darts yet, we wait until we release the LMB in order to disable auto fire and we set a var - dart_coords - to the coordinates of the mouse pointer. The bitmap used for the pointer has 56 x 56 pixels so I have to add 28 pixels on x and y to get the correct coordinates. The 3d dart (the real model) will be placed 100 quants in front of the screen and vec_for_screen makes my dream come true: changes a 2d position on the screen in a 3d coordinate in the "real" world. The last line creates the dart at that position (same position with the mouse pointer on x and y, 100 quants in front of the screen - that's for those of you that have opened the tv later).

Now that we've got a real dart in the level, what should we do with it? Let's see it running function move_dart:

function
move_dart()
{
wait (1);
mouse_map = null;
my.enable_entity = on;
my.enable_block = on;
my.event = stop_dart;
my.skill1 = 0;

The mouse pointer will disappear (we have a true 3d dart now so the 2d dart aka mouse pointer is fired!); the dart will be sensitive to other entities or level blocks. If the dart collides with another entity or with one of the level blocks, its stop_dart event will run. I reset skill1 for this dart but if it hits something, skill1 will be set to 1.

Time to guess :) the speed components of the dart. You might think that a dart flies right ahead but these dangerous pieces of whatever they're made of need much more care. Why, you ask? Let's imagine this scenario: you sit in front of your computer, drinking cola and playing this little dart game. It is late now and you've been playing the game for hours, yet you get 250 points out of 250 every time. Why is that? Well, when you miss the center of the screen (which happens to be the center of the dartboard too) by a few pixels - let's say you've missed it by 3 pixels - the dart will arrive 3 pixels away from the center of the dartboard (for 50 points) but this means nothing because the entity used for the center has a diameter of 8 pixels. You would need about 80 pixels to miss the board completely - and this can't happen in the real world.

This is the problem - let's see the solution. The dart moves towards the dartboard because of the dart_speed.x. When we miss the center, we will change the speed of the dart on y and z depending on the distance between the center (the perfect shot) and the actual target. The picture below should explain what I just said even better:

That's a big foggy... but I'll just type camera.fog = 0 at the console and we can continue (Acknex humour). The bad version is bad because even if the player misses the center of the screen, the dart has big chances to hit the center of the dartboard. The good version checks if the player has missed the center and if this is true it changes the speed on y and z in order to make the dart go away from the center of the screen.

     dart_speed.x = 100;
dart_speed.y = (372 - mouse_pos.x) / 15;
dart_speed.z = (272 - mouse_pos.y)
/ 15;
dart_speed *= time;
while (my.skill1 == 0)
{
move_mode = ignore_passable;
// ignore passable entities
ent_move (nullvector,
dart_speed);
wait (1);
}
// the dart has hit something here
mouse_map = mouse_pcx;
}

Have I told you that dart_speed.x is the main component of the dart speed? It's what moves the dart toward the dartboard. The other two dart_speed components have a different story: dart_speed.y will move the dart to the left or to the right while dart_speed.z can move the dart up or down. When the center of the mouse is in the center of the screen (400, 300) the upper left corner (the useful spot) is placed at (372, 272) because we have to subtract 28 pixels on x and y. The values for dart_speed.y and dart_speed.z will be calculated by subtracting the "real" mouse position from this virtual center of the screen placed at (372, 272); the value that results from this operation will be divided with a number (15) that can be used for calibration.

Do you remember that if we press "T" the mouse stops fooling around? This allows us to calibrate the game; press and hold "T", shoot some darts as shown by the red circles in the picture below and they should hit the extremity of the dartboard; if they hit the dartboard too close to the center or if they miss the dartboard completely, change 15 until everything works ok. Please remember that if you change dart_speed.x, you will need to calibrate again.

Dart_speed is multiplied by time in order to keep the same dart speed at different frame rates; if your pc can't deliver 30 fps this line of code will help you. The dart will move inside the while loop until it hits something; when this happens, we show the mouse pointer again.

What happens when the dart hits something? Let's take a look at the function below:

function stop_dart()
{
my.skill1 = 1;
snd_play (hit_snd, 50, 0);
my.passable = on;
number_of_darts += 1;
while (number_of_darts <
5) {wait (1);}
while (camera.arc > 20)
// zoom in
{
camera.arc -= 0.2
* time;
wait (1);
}
while (key_any == 0) {wait (1);}
ent_remove (me);
while (camera.arc < 60)
// zoom out
{
camera.arc += 0.2
* time;
wait (1);
}
number_of_darts = 0;
score = 0;
}

First of all, we set skill1 to 1; this stops the movement in function move_dart(), isn't it? We play a sound and then we make the dart passable because another dart might hit the same point. We increase the number of darts and then we wait until all the five darts are fired. Now why would we want to do something like that? It is easier (and nicer) to create the code that restarts the game if we do it this way. When all the five darts are fired, we zoom in, with five functions decreasing camera.arc at the same time. We have come close to the dartboard now so we can admire what we did; as soon as we press any key or mouse button, the old darts are removed, the camera zooms out, the number of darts and the score are reset.

Do you remember that I've told you not to shoot the score board? Ok, shoot it and see what it does:

action
score_board
{
my.oriented = on;
my.enable_impact = on;
my.event = fall;
}

function
fall()
{
while (my.z > -500)
{
my.z -= 50 * time;
wait (1);
}
snd_play (fall_snd, 50, 0);
}

I'm using a simple, oriented sprite but it is sensitive to impact; if it is hit by a dart, it will run function fall(). The score board will decrease its height until it reaches a value below -500 quants on z. The player will hear a sound when the board has stopped moving; he or she will think that the board has hit the floor... which is ok because they can't see the floor :)

Time to see the dartboard code - it is really simple:

action
got_50
{
my.skill1 = 50;
my.enable_impact = on;
my.event = compute_score;
}

action
got_25
{
my.skill1 = 25;
my.enable_impact = on;
my.event = compute_score;
}

action
got_10
{
my.skill1 = 10;
my.enable_impact = on;
my.event = compute_score;
}

function
compute_score()
{
score += my.skill1;
}

These actions are attached to the entities placed over the big dartboard circle. The score for every entity is stored inside its skill1; when one of the darts hits one of these entities, the value kept in skill1 is added to the total score.


Untitled

Deadly spiders

These spooky creatures will move around in your level, avoiding the obstacles and biting any entity that collides with them (excluding their own kind, of course).

action
deadly_spider
{
var spider_speed;
var in_front;
my.enable_entity = on;
my.enable_impact = on;
my.event = hurt_them;
while(1)
{
spider_speed.x = 8 * time;
spider_speed.y = 0;
spider_speed.z = 0;
spider_speed *= time;
in_front.x = my.x + 40 * cos(my.pan);
in_front.y
= my.y + 40 * sin(my.pan);
in_front.z = my.z;
if (content(in_front) == content_solid)
{
my.skill40 = my.pan;
my.skill41 = 30 + random(90);
while (my.pan < my.skill40 + my.skill41)
// rotate 30..120 degrees
{
my.pan
+= 5 * time;
wait (1);
}
}

The spiders are sensitive to other entities; their event is function hurt_them. The movement takes place inside a while(1) loop, with the movement speed given by spider_speed.x. I have defined a local variable named in_front, and I have set its coordinates 40 quants in front of the player; if the content of in_front is solid (a wall or a map entity), we store the initial pan angle of the spider and then we add a random angle (30...120 degrees) to it using a loop that increases the value slowly. This trick will (hopefully) rotate the spider in a direction that isn't blocked by obstacles.

     else // free to move
{
ent_cycle("run", my.skill20);
// play the "run" animation
my.skill20 += 10 * time;
my.skill20 %= 100; // loop
move_mode = ignore_passable; // ignore passable entities
result = ent_move (spider_speed, nullvector);
if (result == 0) // got stuck?
{
spider_speed.x
*= -1; // then reverse the movement direction
my.skill40 = my.pan;
my.skill41 = 30 + random(90);
while (my.pan < my.skill40 + my.skill41) // rotate 30..120 degrees
{
my.pan += 5 * time;
ent_cycle("run", my.skill20); // play reversed "run" animation
my.skill20 -= 10 * time;
my.skill20 %= 100; // loop
move_mode = ignore_passable; // ignore passable entities
ent_move (spider_speed, nullvector);
wait (1);
}
spider_speed.x
*= -1; // restore the initial speed
}
}
wait (1);
}
wait
(1);
}

If the spider can move, it plays its "run" animation, ignoring the passable entities that it might encounter on its way. We store the result of its movement in "result" and then we check its value; if the spider got stuck, "result" is set to zero, so we multiply spider_speed.x by -1, which reverses its movement direction. We generate another random angle that will be added to the initial pan angle, and then we move the spider backwards, using its reversed "run" animation and ignoring the passable entities. We multiply spider_speed.x by -1 again at the end of the loop, and this restores the initial speed.

The last thing that needs to be discussed is the event function:

function
hurt_them()
{
proc_kill(4);
if (you != null)
// collided with an entity (the player, a monster, etc)?
{
snd_play (gotcha_wav, 50, 0);
you.skill9 -= 10; // if "health" for that entity is stored in its skill9
}
}

This function will run when the spider collides with something. If it collides with an entity (you != null) we play the gotcha_wav sound, and then we decrease skill9 for the entity that was bitten. If your player or enemies use another skill for storing their health you should change it as needed. Action player_walk_fight uses skill9 for health, so your player will see its death in my demo level.


Untitled

Earthquake

If you need to add an earthquake to your 1st person game, there's a simple solution: store player’s eye height (eye_height_up) from templates, then play with it and with player’s roll angle. Restore the original values when the earthquake is over and everything will work as before.

The good thing with this code is that you can't jump out of the level because you aren't moving at all (of course you can add code for that); unfortunately, your customer doesn't know this, so he will certainly fall in that lava pool because he will move in order to keep the player in a safe area :)

The earthquake starts when you run into an entity (attach it the quakegen action, make it invisible if you want). I would use a visible stair (built as a wmb entity) that leads to a rope bridge placed above the lava pool:

action
quakegen
{
my.enable_impact = on;
my.event = earthquake;
}

function
earthquake
{
my.skill9 = eye_height_up;
my.skill10 = random(100) + 20;
my.skill11 = random(100) + 100;
waitt(my.skill10);
while (my.skill11 > 0)
{
if (random(1) > 0.9)
{
play_sound eq_snd, 30;
}
if (eye_height_up >
0.2)
{
eye_height_up += 0.1 - random(2)/10;
}
player.roll += 10 - random(20);
my.skill11
-= 0.1 * time;
waitt(2);
}
eye_height_up = my.skill9;
player.roll = 0;
}

Skill10 gives the initial delay (so the player can’t tell when / if he has triggered the earthquake) and skill11 gives the duration; play with these values.


Untitled

Eclipse code

One of the new features that was presented in AUM5 is the ability to change the color for the sky and the scene map. This helps us to get smooth day / night transitions. I thought that if I write some simple day / night code, none of my readers will sit in his chair and wait for 24 hours to test if it works fine. I have decided to go for something a little more complicated but more spectacular; the eclipse happens every two minutes (with the speeds set by me in the test level) and lasts for a few seconds. If you miss the eclipse or if you don't want to wait for another two minutes to see it, press tab and type moon_speed = 1 at the console. This works fine when you start the level - if the sun has moved too much you'll have to find another value.

entity*
moon_pnt;
entity* sun_pnt;
var sun_radius;
var sun_speed;
var moon_radius;
var moon_speed;

We have to set two pointers for the moon and the sun; in fact we don't need the pointer to the sun (we can use the my pointer for that) but it makes the things easier for you. The radius for the sun and the moon (their orbits) are set automatically when you place the planets in Wed; their speeds are set with their skill1.

The action for the sun is the most complex:

action
sun
{
sun_pnt
= me;
qLensFlare = 0;
mouseview = 5;
camera.ambient
= 100;
my.unlit = on;
my.ambient = 100;
my.bright = on;
my.flare = on;
while (player == null) {wait (1);}
while (moon_pnt == null) {wait
(1);}
sun_radius = vec_dist (my.x, player.x);

After the first line is executed, the sun will be known as sun_pnt and can be accesed from any other action or function. We turn the template lens flares off (if lflare.wdl is included in your project, otherwise you'll have to comment this line). The variable that controls the mouse sensitivity is named mouseview - we increase its value. One way to control the brightness for in the whole level is to modify the camera.ambient - this will work on all the surfaces, with all the models and entities and so on. We are setting camera.ambient to 100 for now (so we start with a bright level). The flags bright and flare used in conjunction will blend the sun over the background. We wait for the player and for the moon to be created and then we get the sun radius from its position related to player's starting position in Wed.

    while (1)
{
my.x = player.x + sun_radius
* sin (sun_speed);
my.y =
player.y + sun_radius * cos (sun_speed);
my.z = sun_radius * ((moon_pnt.z
- (player.max_z - player.min_z) * 0.7) / moon_radius);

vec_set (temp, player.x);
vec_sub (temp, sun_pnt.x);
vec_to_angle (sun_pnt.pan, temp);

my.skill20 = ang(sun_pnt.pan - moon_pnt.pan);

        if (my.skill20 > -6 &&
my.skill20 < 6)
{
camera.ambient = 16.6 * abs(my.skill20); // 100:6
scene_color.red = 42 * abs(my.skill20); // 255:6
scene_color.green = 42 * abs(my.skill20);
scene_color.blue = 42 * abs(my.skill20);
sky_color.red = 42 * abs(my.skill20);
sky_color.green = 42 * abs(my.skill20);
sky_color.blue = 42 * abs(my.skill20);
}
sun_speed += my.skill1 * time;
wait (1);
}
}

The sun moves on a circle who's center is the player. If the player (blue dot) moves, the sun and the moon will move with him too. I did that because we don't want to create an enormous level and we have to fool the player that the distance between him and the planets is huge. If we wouldn't use this trick, the player would see that the distance between him and the planets gets smaller as he approaches them.

Let's take a look at this line:

sun.z = sun_radius * (moon.z - player.z) / moon_radius;

You won't find the line above in daynight.wdl - I have simplified it a little. We want to make sure that the moon can "block" the sun view regardless of player's position, so me lift (or lower) the sun depending on player's position. If you take a look at the picture in the left can you imagine what happens if the player comes closer to the moon (look at the dashed line): the eclipse can't happen because the sun would have to be lifted much higher - that's what that line does. We haven't finished with this: you can see that the player looks with his eyes (picture on the right) so we have to compute the approximate height of the eye. I have used a simple (player.max_z - player.min_z) * 0.7 that computes player's height and multiplies it by 0.7 - you should play with this value. Here's the result (remember that my is the sun):

my.z = sun_radius * ((moon_pnt.z - (player.max_z - player.min_z) * 0.7) / moon_radius);

The sun is rotated so that it always faces the player using a simple vec_to_angle instruction. We are using sun's skill20 to keep the difference between sun's and moon's pan angles, like in the picture.

If skill20 is bigger than -6 and smaller than 6 it's eclipse time! Of course that you will have to modify these values in your level depending on the size of your planets, their distance to the player and so on. We are modifying scene_color, sky_color and camera.ambient to get a good looking eclipse.

Here's the action associated to the moon:

action
moon
{
moon_pnt
= me;
my.unlit = on;
my.bright = on;
while (player == null) {wait (1);}
moon_radius = vec_dist (my.x, player.x);
while (1)
{
my.x = player.x + moon_radius
* sin (moon_speed);
my.y =
player.y + moon_radius * cos (moon_speed);

vec_set
(temp, player.x);
vec_sub
(temp, moon_pnt.x);
vec_to_angle
(moon_pnt.pan, temp);

        moon_speed += my.skill1 * time;
wait (1);
}
}

All
the lines in this action can be found (and have been explained) in action sun.


Untitled

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.


Untitled

Advanced explosion effect s

I'd like to mention from the very beginning that the harder you will work, the nicer your effects will look. However, this explosion effect consists of some simple, overlapping particle and sprite effects:

1) The explosion core:

function
explosion_core()
{
if (random(1) > 0.5)
{
my.bmap = core1_tga;
}
else
{
my.bmap = core2_tga;
}
my.transparent = on;
my.flare = on;
my.bright = on;
my.size = random(30) + 20;
my.alpha = 100;
my.function = core_fade;
}

function
core_fade()
{
if (my.alpha > 50)
{
my.size += 2 * time;
}
else
{
my.size -= 2 * time;
}
my.alpha -= 3 * time;
if(my.alpha < 0)
{
my.lifespan = 0;
}
}

The first function picks one of the two bitmaps (core1_tga or core2_tga), makes it transparent, bright and so on, and then it gives it a random size. The second function will increase the bitmap size until its alpha goes below 50, and then it will start decreasing it. The particles will be removed if their alpha is below zero.

2) The sprite ring (shockwave, whatever):

function
explosion_ring()
{
my.passable = on;
my.scale_x
= 0.1;
my.scale_y = 0.1;
my.tilt = 90;
my.oriented = on;
my.bright = on;
my.flare
= on;
my.transparent = on;
my.alpha = 100;
my.ambient = 100;
my.lightred = 255;
my.lightgreen = 200;
my.lightrange = 100;
while
(my.scale_x < 4)
{
my.alpha
-= 10 * time;
my.lightrange += 50 * time;
my.scale_x += 0.5 * time;
my.scale_y = my.scale_x;
wait(1);
}
ent_remove(my);
}

The sprite starts with a small scale and light up an area of 100 quants at first; the "while" loop will decrease its transparency factor and will increase its scale and lightrange. The ring is removed as soon as its scale is equal to or bigger than 4.

3) The rotating sprite:

function
explosion_sprite1()
{
my.passable = on;
my.facing
= on;
my.bright = on;
my.flare = on;
my.alpha = 100;
my.ambient = 100;
while
(my.scale_x < 4)
{
my.scale_x
+= 1 * time;
my.scale_y = my.scale_x;
my.alpha -= 10 * time;
my.roll += 25 * time;
wait(1);
}
ent_remove(my);
}

This sprite will face the camera all the time. We increase its scale in a "while" loop, decreasing its alpha and rotating it at the same time. When its scale is bigger than 4, the sprite is removed.

4) The animated sprite:

function
explosion_sprite2()
{
my.passable = on;
my.facing
= on;
my.bright = on;
my.flare = on;
my.ambient = 100;
my.red = 255;
my.lightrange
= 100;
while (my.frame < 20)
{
my.frame += 1 * time;
my.scale_x += 0.1 * time;
my.scale_y = my.scale_x;
my.lightrange = 300 - random(100);
wait(1);
}
ent_remove(my);
}

This sprite will go through all its animation frames, increasing its scale and having a random lightrange. The sprite is removed as soon as it has played the last animation frame.

5) The gibbit generator and its particle trails:

function
move_gibbits()
{
var gib_coords;
var gib_speed;
my.passable = on;
my.scale_x = 0.5 + random(1);
my.scale_y = my.scale_x;
my.scale_z = my.scale_x;
vec_set (gib_coords, my.x);
gib_speed.x = (10 - random(20))
* time;
gib_speed.y = (10 - random(20)) * time;
gib_speed.z = (20 + random(10)) * time;
my.skill10 = 10;
while (my.skill10 > 0)
{
if (vec_dist (my.x, gib_coords.x) > 70)
{
my.passable = off;
}
ent_move (nullvector,
gib_speed);
effect (debris_trail, 1, my.x,
nullvector);
if (bounce.z != 0)
{
gib_speed.z = -(gib_speed.z * min(0.8, random(1)));
if (gib_speed.z < 0.1)
{
gib_speed.x = 0;
gib_speed.y = 0;
gib_speed.z = 0;
}
}
gib_speed.z -= 2
* time;
my.skill10 -= 0.3 * time;
wait (1);
}
my.transparent
= on;
my.alpha = 100;
while (my.alpha > 0)
{
my.alpha -= 3 * time;
wait (1);
}
ent_remove (my);
}

The gibbets are small 3D models with a random size (0.5 ... 1.5) and random speeds on the x, y and z axis. Please note that the speed on the z axis is positive, so the giblet will fly upwards at the moment of creation. We set skill10 to 10 and then we run a while loop that will stop when skill10 goes below zero. What happens inside the while loop? First of all, the giblet becomes impassable if it has managed to get away from its starting point; this trick prevents the giblets from getting stuck into each other. Play with "70" - you'll find better values for your own projects. The giblet moves with the speed given by gib_speed and creates particles at its position every frame using the debris_trail function. If the giblet hits something (the ground, a wall, and so on) it changes its speed, picking a smaller value (up to 0.8 from the initial value) and a changed sign. If gib_speed is too small, gib_speed is reset. The last few lines inside the loop decrease gib_speed.z every frame, simulating the effect of the gravity and decrease skill10 because the loop must end at a certain moment. What happens next? The giblet model is made transparent and fades away, being removed when its alpha goes below zero.

function
debris_trail()
{
my.bmap = debristrail_tga;
my.flare = on;
my.bright = on;
my.size = 2;
my.alpha = 30;
my.function = fade_debtrail;
}

function
fade_debtrail()
{
my.size += 0.8 * time;
my.alpha
-= 1.5 * time;
if (my.alpha < 0)
{
my.lifespan = 0;
}
}

These particles simply increase their size and decrease their alpha, being removed as soon as their transparency factor goes below zero.

6) The smoke:

function
particle_smoke()
{
my.bmap = smoke_tga;
my.vel_x
= 2 - random(4);
my.vel_y = 2 - random(4);
my.vel_z
= 2 + random(1);
my.lifespan = 200;
my.flare
= on;
my.transparent = on;
my.alpha = 50;
my.move = on;
my.size = 50;
my.function
= fade_smoke;
}

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

That's your typical smoke effect, with random speeds on the x and y axis and a positive speed on the z axis. The particle increases its size as it moves upwards, decreasing its transparency.

These are all the functions that are being used for the effect, so let's see how they are assembled together:

function
start_explosion(coordinates)
{
var sound_volume;
var number_of_gibbits;
sound_volume = 30000 / (vec_dist(coordinates.x,
camera.x) + 100);
snd_play (explostart_wav, sound_volume, 0);
effect (explosion_core, 10 + random(10), coordinates, nullvector);
sleep (0.5);
ent_create (exploring_tga, coordinates, explosion_ring);
snd_play (explomid_wav, sound_volume, 0);
sleep (0.1);
ent_create (exploflare_pcx, coordinates, explosion_sprite1);
sleep (0.1);
ent_create (explosion_pcx, coordinates, explosion_sprite2);
sleep (0.1);
number_of_gibbits = random(15) + 5;
while (number_of_gibbits > 0) // while without wait!
{
ent_create (gibbit_mdl, coordinates, move_gibbits);
number_of_gibbits -= 1;
}
snd_play (exploend_wav, sound_volume, 0);
sleep (0.5);
effect (particle_smoke, random(20) + 10, coordinates, nullvector);
}

I have computed the value for sound_volume using the distance between the coordinates of the explosion and the camera; this way you will be able to play the explosion sounds anywhere in the level (not necessarily at an entity's position). We start by running the explosion_core function, with a random (10...20) number of particles, at the position given by "coordinates", which is the vector that is passed to the start_explosion() function. We wait for 0.5 seconds, and then we create the explosion ring. We wait 0.1 seconds, we create the rotating star effect, we wait another 0.1 seconds and then we create the animate sprite effect. We generate a random (15...20) number of gibbits and we create them inside a fast while loop. Finally, we wait 0.5 seconds and we create the smoke.

Feel free to experiment by changing the order, adding several identical lines that create the same effect, playing with the sleep intervals and so on. I'll see you soon!

Hold on, mister! Aren't you going to show us how to use this great effect in our projects?

Ok, let's take a look at 2 examples:

1) Create the explosion in a specific area of your level:

function explo_test()
{
start_explosion (vector(0, 0, 100));
}

on_e = explo_test;

When you press the "E" key you will generate an explosion at x = 0, y = 0, z = 100 quants in your level. Simply call explo_test() when you need it to trigger the explosion.

2) Create exploding barrels:

function destroy_me()
{
start_explosion (vector(my.x,
my.y, my.z + 20));
sleep (0.5);
my.invisible
= on;
my.passable = on;
}

action explodable_barrel
{
my.enable_impact
= on;
my.enable_entity = on;
my.enable_shoot
= on;
my.event = destroy_me;
}

The barrel is sensitive to impact with other entities as well as to "trace" and "c_trace" instructions; its event function triggers the explosion at barrel's position (ok, 20 quants higher) waits a bit and then hides the barrel. You will find both examples in the included office.wmp file, so feel free to experiment with them. Don't forget that if you run out of ammo you can run into the barrels to destroy them! :)


Untitled

Flame thrower

If you need a weapon that creates a jet of particles then you have come to the right place.

action
flame_thrower
{
my.metal = on;
while (player
== null) {wait (1);} // wait until the player is loaded
while
(vec_dist (my.x, player.x) > 60) // wait until the player has come close enough
{
my.pan += 5 * time;
wait (1);
}
snd_play(gotflames_wav,
60, 0);
my.passable = on;
while (1)
{
my.x = player.x + 20 * cos(player.pan)
+ 13 * sin(player.pan);
my.y = player.y
+ 20 * sin(player.pan) - 13 * cos(player.pan);
my.z = player.z + 12;
my.pan = camera.pan;
my.tilt = camera.tilt;
vec_for_vertex (flame_start, my, 14); // the flames will start at the coordinates
given by the 14th vertex on the gun
vec_set(flame_end,
nullvector); // reset flame_end
flame_end.x
= flame_range.x; // add 400 quants to it
vec_rotate(flame_end, camera.pan); // rotate it depending on player's angles
vec_add(flame_end.x, camera.x); // now set the
correct flame_end position in 1st person mode (use player.x instead of camera.x
for 3rd person)
if(key_ctrl == on) //
Ctrl was pressed?
{
create_flames(); // fire!
}
wait (1);
}
}

The weapon will wait until the player has come closer than 60 quants to it. As soon as this happens, the weapon becomes passable and is placed 20 quants in front of the player, 13 quants to the right and 12 quants above player's origin, regardless of player's position and / or angles. Play with 20, 13 and 12 to set a different position for the flame thrower in 1st person mode. The gun will have the same pan and tilt angles with the camera.

I have defined a variable named "flame_start" and I use vec_for_vertex to set it to the position of the 14th vertex on the flame thrower model every frame; this will be the origin for our flames. We reset the second variable named "flame_end", we add the content of "flame_range" to it (a variable that sets the range to 400 quants), and then we rotate "flame_end", creating a vector that has a magnitude of 400 quants and the orientation given by player's angles. Finally, we add camera's position to the vector; this way we have set the correct flame_end point, which is (surprisingly) the point where the flames will end.

Every time we press the "Ctrl" key, function create_flames() is run:

function
create_flames()
{
vec_sub(flame_end.x, flame_start.x);
temp = vec_length(flame_end);
flame_end.x = (flame_end.x
* 3) / temp;
flame_end.y = (flame_end.y * 3) / temp;
flame_end.z = (flame_end.z * 3) / temp;
while(temp >
0)
{
effect(particle_flames,
1, flame_start.x, nullvector);
vec_add(flame_start.x,
flame_end.x);
temp -= 3;
}
}

This function gets the distance between flame_end and flame_start, and then splits it in tiny parts, creating particles until flame_start grows to flame_end. If you would be able to get a side view to our particle effect, which nowadays, using modern technology, is possible :) you would see something like this:

I am using many more particles for the effect; the shot above was taken with "3" being replaced by "20" in function "create_flames", so the effect in the picture uses about 7 times fewer particles when compared to the original flame thrower effect.

function
particle_flames()
{
temp.x = random(2) - 1;
temp.y = random(2) - 1;
temp.z = random(2) - 1;
vec_set(my.vel_x, temp);
my.move = on;
my.bmap
= fire_tga;
my.flare = on;
my.bright = on;
my.alpha = 100;
my.lifespan = 100;
my.size
= 15;
my.function = fade_flames;
}

function
fade_flames()
{
my.alpha -= 75 * time;
my.size
+= 10 * time;
if(my.alpha < 0) {my.lifespan = 0;}
}

The rest of the code is your standard particle effect; if you think that the fire effect lags a bit you can shorten the life of the particles. Oh, and if you set flame_start to your enemy's weapon and flame_end to be player's model you will see how it feels to be on the other side of the flame thrower. For those of you that are too lazy to try my snippets I've got a low quality movie file here. You will need Media Player 9 or a newer version of it.


Untitled

Flickgen

Have you ever wondered if A5 can be used as a wdl - generator? Some of the users have created great particle generators a while ago; you set some values / drag some sliders around and after you click a button a new wdl file is generated. All you have to do is to include it in your project and everything will run great. How is this possible?

First of all you must create a room and set a decent camera position. You create a few sliders / buttons / whatever - they will change what you see. When you are happy with the result, you press a button and a wdl file is created. This file can include comments, etc but you can be sure that it will include the action(s) / function(s) needed for your effect. Sounds easy, right? Let's see how it is done.

Flickgen comes from flickering light generator - this project generates code for flickering lights. You can set the color (RGB), light range, time on and time off. Before we start, take a look at the test.wdl file in the \flickgen folder - its content was generated by Flickgen.

Every standalone project has its own main function:

function
main()
{
d3d_lightres = 1; // better dynamic lights
level_load (flickgen_wmb);
wait (2);
mouse_init();
}

The first line in main improves the quality of the dynamic lights; the test level is loaded and then we show the mouse pointer:

function
mouse_init()
{
mouse_map = arrow_pcx;
mouse_mode = 2;
while (1)
{
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
wait (1);
}
}

My test level includes a model that has this action attached to it:

action camera_init
{
my.invisible = on;
my.passable = on;
vec_set (camera.pos, my.pos);
camera.pan = my.pan;
camera.tilt = my.tilt;
}

I have used an arrow model; change its position and angles to get a better view, because the camera is tied up to this arrow model. The object that generates the flickering lights is a cube model that runs the following action:

action flicker
{
my.passable = on;
while (1)
{
my.z = height;
my.lightred = red;
my.lightgreen =
green;
my.lightblue =
blue;
my.lightrange =
distance;
waitt (interval1);
my.lightrange =
0;
waitt (interval2);
}
}

This
action is quite simple, but all the variables in it are controlled by sliders;
height is the height of the flickering light source, red green and blue are the
values for lightred, lightgreen and lightblue, distance is the value for lightrange,
interval1 and interval2 are the on / off values for the light. Let's see how these
sliders work:

panel adjust_pan // main
panel
{
bmap = panel_pcx;
layer = 20;
pos_x = 0;
pos_y = 0;
vslider = 2, 10, 100, slider_pcx,
0, 255, red;
vslider = 22, 10, 100, slider_pcx, 0, 255, green;
vslider = 42, 10, 100, slider_pcx, 0, 255, blue;
vslider = 62, 10, 100, slider_pcx,
1, 100, interval1;
vslider = 82, 10, 100, slider_pcx, 1, 100, interval2;
vslider = 102, 10, 100, slider_pcx, 0, 500, distance;
vslider = 122, 10, 100, slider_pcx,
10, 500, height;
flags = d3d, overlay, refresh, visible;
}

The first definition places the slider at (2,10) pixels, sets its vertical size to 100 pixels, uses the slider_pcx bitmap for the slider and changes the var named red between 0 and 255. The rest of the sliders work the same way.

If you have opened
and run Flickgen you have noticed that the proper RGB, etc values are displayed
on the screen. I'm using a simple panel definition for that:

panel digits_pan // shows the rgb... values
{
layer = 21; // appears over adjust_pan
pos_x = 0;
pos_y = 0;
digits = 0, 130, 3, univers_font,
1, red;
digits = 20, 140, 3, univers_font, 1, green;
digits = 40, 130, 3, univers_font, 1, blue;
digits = 60, 140, 3, univers_font,
1, interval1;
digits = 80, 130, 3, univers_font, 1, interval2;
digits = 100, 140, 3, univers_font, 1, distance;
digits = 120, 130, 3, univers_font,
1, height;
flags = d3d, overlay, refresh, visible;
}

The value for "red" appears at (0,130) pixels, using 3 digits, with univers_font, its value being multiplied by 1. Flickgen has 2 more buttons: save and quit. In fact, these buttons are 2 small panels - and their definitions are as simple as possible:

panel
quit_pan // shows the quit button
{
bmap = quit_pcx;
layer = 21; // appears over
adjust_pan
pos_x = 10;
pos_y = 190;
flags = d3d, overlay, refresh,
visible;
on_click quit;
}

panel
save_pan // shows the save button
{
bmap = save_pcx;
layer = 21; // appears over
adjust_pan
pos_x = 10;
pos_y = 160;
flags = d3d, overlay, refresh,
visible;
on_click save_file;
}

function quit()
{
exit; // exit the engine
}

When we press the "Quit" button, its associated quit function will run. You can see that the function simply shuts down the engine - but that's what we want, right? Let me throw in the definitions for 2 texts and a few more strings; we will use them right away:

text
action_txt
{
layer = 21;
pos_x = 55;
pos_y = 176;
font = albertus_font;
string = action_str;
}

text
filename_txt
{
layer = 21;
pos_x = 55;
pos_y = 204;
font = albertus_font;
string = filename_str;
}

string action_str = " "; // holds the action name
string filename_str = " "; // holds the file name
string empty_str = ""; // an empty string
string content_str; // holds the content of the file
string indent_str = " "; // 4 spaces here but you can use any other indent value
string temp_str = " "; // just a temporary string used to convert numbers to strings

If we press the "Save" button, the ugly save_file function will run:

function save_file()
{
action_txt.visible = on; // show the text
str_cpy (action_str, empty_str);
while (str_cmpi (action_str, empty_str) == 1) // make sure that the player has
typed something
{
inkey action_str;
// show the cursor -> store the input in filename_str
wait (1);
}
filename_txt.visible = on;
// show the text
str_cpy (filename_str, empty_str); // clear previous inputs
while (str_cmpi (filename_str,
empty_str) == 1) // make sure that the player has typed something
{
inkey filename_str;
// show the cursor -> store the input in filename_str
wait (1);
}
waitt (16); // wait a second
action_txt.visible = off;
// hide the text
filename_txt.visible = off; // hide the text

The action_txt text is made visible and then the action_str is cleared. We want to store the name of our new action in action_str; if we call inkey at this point, the player could press the enter key, without typing anything - and this would be bad. We can prevent this from happening by including the inkey instruction in a while loop; if action_str = "" (nothing), inkey keeps running until something is entered in the "Action name" field. The same things happens with the rest of the code that stores the file name in filename_str. The texts are visible for one more second and then they disappear.

str_cpy (content_str, empty_str); // clear the string
str_cat (content_str, "// include this file in your main c-script file\n// place any model in your level and attach it the ");
str_cat (content_str, action_str);
str_cat (content_str, " action");
str_cat (content_str, "\n\naction ");
str_cat (content_str, action_str);
str_cat (content_str, "\n{\n");
str_cat (content_str, indent_str);
str_cat (content_str, "while (1)\n");

The string content_str will be used to store all the text that will be written to our wdl file. Please open the test.wdl file that came with Flickgen or another file created by you with Flickgen, otherwise I might loose you on the way :) The string is cleared and then the commented lines are appended to the string. Our comments should include the name of our action, so we append the named stored in action_str to it. Time to add the word "action" to the 2nd comment line. I hope that you remember: \n will jump to the next line the same way you type Enter in a word processor. After 2 line feeds we begin to write "action", followed by a space and the name stored in action_str. Line feed, a winged bracket, another line feed. Now it is the time to add our indent_str, a string consisting of 4 spaces, and then the text "while (1)" and another line feed. Did you see all this stuff in the generated wdl file? Let's move on:

    str_cat (content_str, indent_str);
str_cat (content_str, "{\n");
str_cat (content_str, indent_str);
str_cat (content_str, indent_str);
str_cat (content_str, "my.lightred
= ");
red
= int(red);
str_for_num (temp_str, red);
str_cat (content_str, temp_str);

We append another indent_str to content_str, a winged bracket and a line feed, 2 indent_str strings and then we append the text: "my.lightred = ". The value set for red by its associated slider is converted to an integer, converted to string and then appended to content_str. The same things happen for green, blue and distance - trust me on that.

    str_cat (content_str, ";\n");
str_cat (content_str, indent_str);
str_cat (content_str, indent_str);
str_cat (content_str, "my.lightgreen
= ");
green
= int(green);
str_for_num (temp_str, green);
str_cat (content_str, temp_str);
str_cat (content_str, ";\n");
str_cat (content_str, indent_str);
str_cat (content_str, indent_str);
str_cat (content_str, "my.lightblue
= ");
blue
= int(blue);
str_for_num (temp_str, blue);
str_cat (content_str, temp_str);
str_cat (content_str, ";\n");
str_cat (content_str, indent_str);
str_cat (content_str, indent_str);
str_cat (content_str, "my.lightrange
= ");
distance
= int(distance);
str_for_num
(temp_str, distance);
str_cat
(content_str, temp_str);

Now it is the time to add the waitt(x) lines; we have a line feed, 2 * indent_str, the "waitt (" string, the integer value of interval1 is converted to string and appended to content_str, we add ")" and move to the next line and so on.

    str_cat (content_str, ";\n");
str_cat (content_str, indent_str);
str_cat (content_str, indent_str);
str_cat (content_str, "waitt (");
interval1 = int(interval1);
str_for_num (temp_str, interval1);
str_cat (content_str, temp_str);
str_cat (content_str, ");\n");
str_cat (content_str, indent_str);
str_cat (content_str, indent_str);
str_cat (content_str, "my.lightrange
= 0;\n");
str_cat
(content_str, indent_str);
str_cat
(content_str, indent_str);
str_cat
(content_str, "waitt (");
interval2
= int(interval2);
str_for_num
(temp_str, interval2);
str_cat
(content_str, temp_str);
str_cat
(content_str, ");\n");
str_cat
(content_str, indent_str);
str_cat
(content_str, "}\n");
str_cat
(content_str, "}");

If you are still awake you will notice that the lines below add .wdl to our file name stored in filename_str, open the file for writing, write content_str in the file and close it. The nightmare is over, can you believe it?

str_cat (filename_str, ".wdl");
filehandle = file_open_write (filename_str);
file_str_write (filehandle, content_str);
file_close (filehandle);
}

The slider named P (position) moves the light up or down. If you plan to have a flickering light on the ceiling of your room, 300 quants above the floor, you can set P to 300 and see how it will look.
Using this code snippet as a starting point you can create any c-script wizard: particle generators, script editors, etc.


Untitled

Fountain

This fountain uses particles that stop (don't pass through) when they encounter a wmb entity or a level block and create a nice looking mist when they touch the surface of the water.

action
fountain
{
my.invisible = on;
my.passable
= on;
while(1)
{
effect(particle_fountain, 15 * time, my.x, nullvector);
wait(1);
}
}

The action above must be attached to a small model; it makes it invisible and passable and then it generates particles using the particle_fountain function. The number of particles that is generated is time-corrected.

function
particle_fountain()
{
temp.x = random(6) - 3;
temp.y = random(6) - 3;
temp.z = random(10) + 10; // play with
this value
vec_set(my.vel_x,temp);
my.bmap =
drop_tga;
my.size = 1.5;
my.flare = on;
my.bright = on;
my.move = on;
my.streak
= on; // if your engine version supports it
my.gravity = 2;
my.alpha = 80;
my.lifespan = 50;
my.function
= fade_particles;
}

The particles have a random speed on x and y (-3 to 3) and a positive random speed on z (10 to 20). Please note that I am using the "streak" flag which makes the particles look much better but it might not be available in your engine version; try to use a different particle bitmap if you can't use streak. The particles have a gravity of 2 and a lifespan of 50; the function that drives them is named fade_particles.

function
fade_particles()
{
my.alpha -= 2 * time;
if(content(my.x) == content_solid) // the particle is inside a solid?
{
effect(fountain_mist, 1, my.x, nullvector);
// generate mist
my.lifespan = 0; // don't
allow the particles to continue their movement if they have hit something solid
}
if(my.alpha < 0)
{
my.lifespan = 0;
}
}

The first line of code decreases the transparency of the particles. If one of the particles ends up inside a solid (a wmb entity or a level block) it will generate a mist particle using the fountain_mist function and will disappear. For good results you should use fountains with thick walls because the water drops travel at big speeds and might penetrate the walls of the fountain if they are too thin. The last few lines in the function destroy the particle when its transparency factor goes below zero.

function
fountain_mist()
{
temp.x = random(2) - 1;
temp.y = random(2) - 1;
temp.z = random(1) + 1; // play with
this value
vec_set(my.vel_x,temp);
my.bmap =
smoke123_tga;
my.size = 5;
my.lifespan = 50;
my.flare = on;
my.bright = on;
my.move
= on;
my.gravity = 0;
my.alpha = 70;
my.function = fade_mist;
}

The mist has a random speed on x and y (-1 to 1) and a positive random speed on z (1 to 2). The function that drives them is named fade_mist:

function
fade_mist()
{
my.alpha -= 3 * time;
my.size
+= 0.5 * time;
if(my.alpha < 0)
{
my.lifespan = 0;
}
}

This function decreases the transparency of the mist particle while increasing its size. As soon as alpha goes below zero, the mist particle is removed. You can play with the temp.z value inside fountain_mist() and with alpha and size inside fade_mist() until you get a better looking mist. I'm not a fan of excessive particle use but here's an example of what you can get with a slightly different set of values:


Untitled

Force field

I like force fields! The look cool and they pump adrenaline through your veins. And they're not too hard to create, either!

First of all we need to create the part of the force field that looks nice.

action force_field
{
var number_of_particles = my.skill1;
var particle_distance = my.skill2;
if (my.skill1 == 0) {number_of_particles = 600;}
if (my.skill2 == 0) {particle_distance = 0.2;}
vec_set (temp, my.pos);
while (number_of_particles
> 0) // don't use a wait(1) here
{
temp.x += particle_distance
* cos(my.pan);
temp.y += particle_distance
* sin(my.pan);
effect(particle_barrier,
1, temp, normal);
number_of_particles
-= 1;
}
}

We define two local variables: one will store the number of the particles used for the force field (hence the length of the force field) and the second will store the distance between two consecutive particles. If you forget to set these values using skill1 and skill2 they will default to 600 and 0.2. We set temp to the position of the force field entity and then we start generating particles inside a while loop (without wait!) until all the particles set with skill1 are generated.

Time to see the particle related functions:

function
particle_barrier()
{
my.bmap = redflare_pcx;
my.flare = on;
my.bright = on;
my.size = 10;
my.function = keep_particles;
}

function keep_particles()
{
my.lifespan = 10;
my.alpha = 30 + random(70);
}

The only thing that should be observed here is the fact that lifespan is set to 10 all the time, in order to keep the particles "alive". Oh, and we set random alpha factors for the particles to show some laser activity. This part was nice indeed but how do we kill the player?

The easiest method is to use an invisible entity that is overlapping with the force field. Here's the code for it:

action
killer
{
my.invisible = on;
my.enable_impact = on;
my.enable_entity = on;
my.event = kill_them;
}

function
kill_them()
{
wait (1);
if (you != null)
{
you._health = -10;
}
}

The killer entity is a simple wmb entity that is sensitive to other entities. If it collides with another entity (you != null) it sets the health for that entity to -10, killing it instantly.

How do you set up a force field, a laser barrier like the one in my example?
1) Place a small entity in your level and attach it action force_field to create the effect. This will be the particle generator, so you should place another identical entity at the end of the particle beam, without attaching any action to it, to make it look convincing.
2) Create a convenient wmb entity that covers your force field(s), place it in your level and attach it the action named killer. This entity will do the killing.

The example level included in the zip file comes with three particle generators (three force fields) and one big killer entity.


Untitled

Forest

This snippet will generate a forest in a square sized area in your level. This should save you a lot of work and more than that, you will have a different forest every time you run the level.

define num_trees = 100;

var max_x = 1000;
var min_x = -1000;
var max_y = 1000;
var min_y = -1000;

The forest will have 100 trees; in my example the trees will be placed inside a rectangular area (-1000 quants ... 1000 quants on x and -1000 ... 1000 quants on y but you can use any other values).

The forest is generated by running this function in main:

function
generate_forest()
{
randomize(); // always generate
a different forest
while (tree_index < num_trees)
{
tree_pos.x = sign(min_x)
* random(abs(min_x)) + sign(max_x) * random(abs(max_x));
tree_pos.y = sign(min_y)
* random(abs(min_y)) + sign(max_y) * random(abs(max_y));
tree_pos.z = 1000;
// 3000 quants above the floor level
tree_index += 1;
if (random(1) >
0.5)
{
ent_create (tree1_mdl, tree_pos, create_tree);
}
else
{
ent_create (tree2_mdl, tree_pos, create_tree);
}
ifdef
fun;
waitt (8);
endif;
}
}

First of all we call the predefined function randomize(); this makes sure that we get a different forest every time we run the level. Random numbers aren't that "random"; they're usually computed using complicated functions but every time you start getting a random number, that huge "random" function delivers the same result. Randomize makes sure that random won't deliver the first function value, but a "random" function value - got it?

As long as we haven't generated all the trees (tree_index < num_trees), we generate a random tree position inside the rectangular area, 1000 quants above the ground (if the ground is set to 0 on the z axis). We add 1 to tree_index, meaning that we have generated a new tree and depending on a random value we create one of the two trees that are used for this forest.

If we run the level like this: "office.wdl -d fun" we place two trees a second, so we will be able to see how they're created and placed in the level. If we run the level without -d fun all the trees will be placed in the level way too fast - but that's what we want, isn't it?

The function that runs for every tree is:

function
create_tree()
{
wait (1);
my.pan = random(360);
vec_set (temp, my.pos);
temp.z -= 3000;
trace_mode = ignore_me +
ignore_sprites + ignore_models + use_box;
my.z -= trace (my.pos, temp) + 20;
}

You can see that the tree will have a random pan angle. We do a trace 3000 quants below the initial position and we decrease the z coordinate until the tree is placed on the ground. We add 20 quants more to make sure that the "roots" will stick in the ground even if the tree is on a slope.

Please make sure that you use low poly models or (even better) sprites for your trees. If your tree has 500 polygons and you want to have 500 trees in the forest, you won't find a 3d card that is able to render 250,000 polygons at decent frame rates.


Untitled

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!


Untitled

Planet Survivors - part 2

This month we will get to play the first P.S. level. Let's take a look at a few screenshots first:

This first level doesn't include enemies, but there are plenty asteroids that can pump adrenaline in your veins :). The main.wdl script went through some minor changes; all the interesting stuff is coded inside level1.wdl. Let's begin with some panels and a text:

panel
shield_pan
{
bmap = shield_tga;
layer = 10;
pos_x = 732;
pos_y = 555;
digits = 7, 15, 3, figures_font,
1, player.shield;
flags = overlay, refresh, visible, transparent;
}

panel ammo_pan
{
bmap = ammo_tga;
layer = 10;
pos_x = 0;
pos_y = 555;
digits = 12, 15, 3, figures_font, 1, player.ammo;
flags
= overlay, refresh, visible, transparent;
}

panel crosshair_pan
{
bmap = pointer2_tga;
layer = 20;
pos_x = 368;
pos_y = 282;
flags = transparent, overlay, refresh;
}

panel score_pan
{
bmap = score_tga;
layer = 10;
pos_x = 300;
pos_y = 5;
flags = overlay, refresh, visible;
}

text score_txt
{
pos_x = 380;
pos_y = 5;
layer = 10;
font = score_font;
string = score_str;
flags = visible;
}

These panels display the shield (in the lower right corner) the ammo (in the lower left corner) the crosshair (centered on the screen at 800x600 pixels) and the bitmap at the top of the screen that has "Score:" written on it. The text will display the score (yes, the score will be converted to a string before being displayed).

The player flies with a big speed above the surface of the planet, right? Wrong! The player is actually sitting still (excepting the cut scene) and the "planet" below it is an mdl that changes its "v" parameter in a while loop:

action
planet_level1 // attached to the "planet" (a flat mdl, really)
{
my.passable = on;
fog_color = 1;
camera.clip_far
= 100000; // works only with A6, delete this line if you own A5
// clip_range = 100000; // remove the comment if you own A5
camera.z
= -100;
camera.fog_start = 50;
camera.fog_end
= 20000;
while (ready_to_play == 0)
{
my.v += 3 * time;
wait (1);
}
my.z -= 5000;
my.ambient
-= 30;
while (player_is_dead == 0)
{
my.v += 2 * time;
wait (1);
}
}

Like I said, the planet is a plane model and it is shifting its texture for as long as player's ship isn't destroyed. The texture that was used for the model is tileable, so the "level" appears to be endless; in fact, it can't end because player's ship is standing still! Now don't go and tell that to your customers! :)

I have activated my secret weapon - the orange fog with RGB = 200, 121, 50 and I have set nice ranges for it using camera.fog_start and camera.fog_end. On a side note, I will make reasonable efforts to keep the project running with both A6 and A5, but you will have to tweak some values (replace camera.clip_far with clip_range, choose different fog ranges, and so on) if you own A5. Take a good look at the screenshots to see how the level is supposed to look like. Ready_to_play is a variable that will be set to 1 when the player is able to control the ship (just right after the cut scene at the beginning). You can see that the "planet" is moved 5000 quants below when the game starts and it is make a bit darker. The "v" texture shifting continues at t smaller speed for as long as the player isn't dead.

I have used a similar model for the sky and a "u" shift for its texture:

action
sky_level1
{
my.passable = on;
my.flare =
on;
my.ambient = -20;
while (ready_to_play ==
0)
{
my.u -= 0.3 *
time;
wait (1);
}
ent_remove (my);
}

The sky will be removed as soon as the cut scene ends. The most horrifying action you've ever seen is listed below:

action
player_level1
{
player = my;
my.ammo = 200;
my.shield = 100;
my.metal = on;
my.ambient
= 50;
my.transparent = on;
my.alpha = 100;
my.enable_impact = on;
my.enable_entity = on;
my.event = player_was_hit;
vec_set(temp, player.x);
vec_sub(temp, camera.x);
vec_to_angle(camera.pan, temp);
track_handle = media_play ("track1.wav", null, 50);
fade_factor = 1000;
my.engine = on;
plasma_jet(65, 63);

This is the action attached to the player; it sets the amount of ammo (just another name for skill21) and the shield (skill22). The ship is transparent (we will see later why) but its alpha factor is 100 (opaque) for now. The ship is sensitive to impact with other entities (the asteroids); its player_was_hit event will run if something collides with it. We use a vec_to_angle instruction to rotate the camera towards player's ship from the very beginning, in order to cut down the potential glitches, and then we play the sound track named track1.wav. I had to use a wave file because not all the editions can "media_play" mp3 or ogg files, but you should use compressed audio files.

Do you remember the particle function that is used for player's engine in the menu? Well, I have added a simple fade_factor to that function and now I can control the lifetime of the particles by setting fade_factor to a convenient value. I chose fade_factor = 1000 because the particles need to be destroyed quickly; the speed of the ship is too big for slow particles. Finally, I set the "engine" flag (flag1) and I run the function that creates the plasma jets at the vertices that are passed as parameters (function plasma_jet() from main.wdl was modified a bit too).

  while (my.x < 3000)
{
my.roll += 1.5 * time;
ent_playsound (my, engine1_wav,
300);
my.x += 52 * time;
vec_set(temp, my.x);
vec_sub(temp, camera.x);
vec_to_angle(camera.pan, temp);
wait (1);
}
ready_to_play = 1;
my.x = -10000;
my.y = 0;
my.z = -100;
my.roll = 0;

   crosshair_pan.visible = on;
camera.x = -10230;
camera.y = 0;
camera.z = -50;
camera.pan = 0;
camera.tilt = 0;
camera.fog_start = 15000;
camera.fog_end
= 50000;

fade_factor = 100;
snd_play (engage_wav, 70, 0);

The code above plays the cut scene; the ship will move from its initial position (x = -10,000) to x = 3,000 changing its roll angle and playing its engine sound. The camera follows the ship all the time (another vec_to_angle does that). As soon as the player reaches the x = 3000 quants limit, ready_to_play is set to 1 (the sky disappears) and the ship is moved back to -10,000 quants because we don't have a huge "planet" to play with and the customer won't notice the change. We show the crosshair, we set a proper position for the camera and we choose another set of fog ranges. Finally, we set fade_factor to 100 because the player is standing still now (though it appears to fly because of the planet "v" shift) and we play the engage_wav sound, to let the player know that he or she is in control now.

   while (my.shield > 0)
{
camera.tilt += 10 * mouse_force.y * time;
camera.pan
-= 10 * mouse_force.x * time;
if ((camera.pan
> 30) && (camera.pan < 330)) // don't allow the player to blow our
cover (if it looks back)
{
if (camera.pan < 180)
{
camera.pan = 30;
}
else
{
camera.pan = 330;
}
}
if (camera.tilt < -35)
{
camera.tilt = -35;
}
if (camera.tilt > 25)
{
camera.tilt = 25;
}
my.pan = camera.pan;
my.tilt = camera.tilt;

As long as player's ship isn't completely destroyed (shield > 0), the camera changes its angles when we move the mouse. Camera's pan angle can range from -30 to 30 degrees, while tilt ranges from -35 to 25 degrees. We do this because we can't allow the player to look back because he would see the level boundary. The ship inherits the same pan and tilt angles with the camera.

vec_set (temp_var, my.pos); // copy player's position
to temp_var
vec_to_screen(temp_var, camera); // now
temp_var contains the 2D position (x, y) of the ship
if ((abs(temp_var.x - 400) < 50) || (abs(temp_var.y - 300) < 40)) // the
crosshair and the ship tend to overlap?
{
my.alpha = (abs(temp_var.x - 400) + abs(temp_var.y - 300));
}
else
{
if (my.alpha < 99)
{
my.alpha += 0.5 * time;
}
}

     if ((mouse_left == 1) && (player.ammo
> 0))
{
fire_rocket();
}

The lines above convert the 3D position of the ship to a 2D screen position, and adjust the transparency of the ship if the crosshair overlaps with it; take a look at the pictures below to see how it works.

This method allows the player to see any attacker / asteroid, even if it is standing in front of the ship. The last lines of code call function fire_rocket() if the player presses the left mouse button (LMB) and if the ammo is greater than zero.

      if ((key_z == on) && (my.y <
100))
{
my.y += 10 * time;
if (my.roll
< 30)
{
my.roll += 3 * time;
}
}
if ((key_x == on) && (my.y > -100))
{
my.y -= 10 * time;
if (my.roll > -30)
{
my.roll -= 3 * time;
}
}
if (key_z + key_x == 0)
{
if (my.roll > 1)
{
my.roll -= 1 * time;
}
if (my.roll < -1)
{
my.roll += 1 * time;
}
}
wait (1);
}

Did you know that the player can move to its left and right using the "Z" and "X" keys? This will be his only chance to avoid the enemies; please note that the movement is limited in this range: -100, 100 quants. The ship changes its roll angle when it is moved, and the it recovers slowly if key_z + key_x = 0 (none of them is pressed).

   crosshair_pan.visible = off;
camera.pan =
0;
camera.tilt = -10;
while (my.z > -1990)
{
if (camera.tilt > -30)
{camera.tilt -= 0.2 * time;}
my.x += 30 * time;
my.z -= 20 * time;
my.roll += 10 * time;
wait (1);
}
player_is_dead = 1;
my.skill10 = 10;
ent_create (explo13_pcx, my.pos, explode_me);
my.tilt = -50; // choose a weird position for the ship
ent_playsound
(my, hit_wav, 5000);
while (1)
{
temp.x = 3 - random(6);
temp.y = 3 - random(6);
temp.z = 4 + random(4);
effect(particle_smoke,
10, my.pos, temp);
wait (1);
}
}

The last part of the action runs when the player is dead (shield <= 0). We set a convenient camera angle and then we can watch loosing its height and rolling, while camera.tilt is decreased slowly, in order to keep up with the ship. We set player_is_dead to 1, and this makes the "v" texture shifting stop, and then we create a big explosion sprite, attaching it the explode_me function. My.tilt = -50 sets a weird position for the ship, we hear the explosion and then we emit smoke in a while loop. R.I.P, courageous player... sniff....

Well, this level doesn't include conventional enemies, but hundreds of asteroids. Let's see what makes them tick:

action
asteroid_maker
{
var temp_var;
my.passable
= on;
my.invisible = on;
while (1)
{
temp.x = my.x;
temp.y = 5000 - random(10000); // -5,000 to 5,000
temp.z = 1000 - random(2000);
if (ready_to_play
== 0) // game not started yet?
{
if ((temp.y > -500) && (temp.y
< 500)) // the asteroid is too close to the player during the cut scene?
{
temp.y = 500; // then avoid the player until it is able to control the ship
}
}
temp_var = random(1);
if (temp_var < 0.33)
{
ent_create (asteroid1_mdl, temp, move_asteroid);
}
if ((temp_var >= 0.33) && (temp_var
< 0.66))
{
ent_create (asteroid2_mdl, temp, move_asteroid);
}
if (temp_var > 0.66)
{
ent_create (asteroid3_mdl,
temp, move_asteroid);
}
wait (2);
}
}

The model that has this action attached to it is made passable and invisible. Its x = 10,000 in Wed, so the asteroids will be generated at this x and will have random values on y (-5000 to 5000) and z (-1000 to 1000). If ready_to_play = 0 (this happens during the cut scene) the asteroids that could collide with the player are moved to y = 500 where they can't damage the ship. We generate a random number and then we store it in temp_var; choosing one of the 3 different asteroid models. The function that moves the asteroids is named move_asteroid:

function
move_asteroid()
{
var meteor_angle;
var meteor_speed;
my.enable_impact = on;
my.enable_entity = on;
my.event = destroy_asteroid;
my.scale_x = 5 + random(10);
my.scale_y = my.scale_x;
my.scale_z = my.scale_x;
my.skill10 = my.scale_x; // store the scale in skill10 - we will use it
to adjust the size of the explosion later
meteor_angle.x = 3
- random(6); // random pan speed
meteor_angle.y = 3 - random(6);
// random tilt speed
meteor_angle.z = 3 - random(6); // random
roll speed
meteor_speed.x = (-100 - random(80)) * time;
meteor_speed.y = 0;
meteor_speed.z = 0;

The asteroids are sensitive to impact with other entities (other asteroids, player's ship or player's rocket) and run their "destroy_asteroid" event function when they collide with something. We set a random scale for the asteroids, and we store it in their own skill10; later we will use this value to scale the explosion sprite accordingly, because a small asteroid should create a smaller explosion, while a huge asteroid should create a huge explosion. The asteroids rotate with random pan, tilt and roll speeds, and move using a negative speed of -100...-180 quants / frame.

  while (my.x > -21000)
{
my.pan += meteor_angle.x * time;
my.tilt +=
meteor_angle.y * time;
my.roll += meteor_angle.z
* time;
move_mode = ignore_you + ignore_passable;
ent_move (nullvector, meteor_speed);
wait (1);
}
ent_remove (my);
}

The asteroids will move toward player's ship until their x goes below -21,000 quants (if they manage to get there), rotating all the time. As soon as the asteroids get past the -21,000 quants limit, they are removed. Let's examine their event function now:

function
destroy_asteroid()
{
if (you.skill40 == 1234)
{
score_value += int(1000 / (my.skill10 + 1));
}
my.x = -30000;
wait (1);
my.event = null;
ent_create (explo13_pcx, my.pos, explode_me);
my.passable = on;
my.invisible = on;
ent_playsound (my, hit_wav, 1000);
sleep (2);
ent_remove(my);
}

If the asteroid is hit by player's bullet (its skill40 = 1234, you'll see) we increase the score, adding a bigger score for smaller asteroids, because they can't be shot that easy; the scale is stored in skill10, remember? The asteroid that was hit (it takes 2 for a tango) will be moved to x = -30,000 where it will wait for another asteroid to collide with it; this is how we get those nice explosions at the beginning of the cut scene. The event function is set to null, the explosion sprite is created and an explosion sound is played. We keep the asteroid in the level for 2 more seconds (though it is invisible and passable) because we the hit_wav sound would stop prematurely if we remove the asteroid too quickly. What comes next?

function
explode_me()
{
my.oriented = on;
my.flare
= on;
my.bright = on;
my.ambient = random(100);
my.passable = on;
my.roll = random(360);
my.skill30 = you.skill10;
while (my.frame < 14)
{
if (my.scale_x < my.skill30)
{
my.scale_x += 5 * time;
my.scale_y
= my.scale_x;
}
my.frame += 1 * time;

vec_set (temp.x, camera.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp); // turn towards the player to make sure that the explo looks great

wait (1);
}
ent_remove (me);
}

That's the function that drives the explosion sprite! The sprite is oriented, passable and has a random ambient value. More than that, it has a random roll angle because we'd like to simulate as many different explosions as we can using a single sprite, in order to save some video memory. We store you.skill10, which is the scale of the asteroid using skill30 from our explosion sprite, and then we play the animated frames, increasing the scale until it matches the one that was used when the asteroid was created. Another vec_to_angle instruction rotates the sprite towards the camera, making sure that the player can see the explosion in its full splendor. The sprite is removed as soon as the last animation frame was played.

Let's see what happens when the player presses the left mouse button (LMB):

function
fire_rocket()
{
proc_kill(4);
while (mouse_left
== 1) {wait (1);}
player.ammo -= 1;
snd_play
(rocket_wav, 50, 0);
ent_create (rocket_mdl, player.pos, move_rocket);
}

The first line of code makes sure that only one instance of the function is running; more than that, we wait until the LMB is released. Player's ammo is decreased, a sound is played and a rocket_mdl file is created at player's position, running the "move_rocket" function:

function
move_rocket()
{
my.enable_entity = on;
my.enable_block
= on;
my.event = remove_rocket;
my.pan = camera.pan;
my.tilt = camera.tilt;
my.skill40 = 1234;
my.skill10 = 3; // default explosion scale for the rocket
my.skill20
= 0;
while (my.skill20 < 500)
{
my.skill1 = 500 * time;
my.skill2 = 0;
my.skill3 = 0;
my.skill20 += 1 * time;
ent_move (my.skill1,
nullvector);

       temp.x
= my.x;
temp.y = my.y;
temp.z = my.z;
effect (particle_trail,
1, temp, normal);
wait (1);
}
remove_rocket();
}

The rocket is sensitive to other entities and level blocks; its event function is named remove_rocket. The rocket gets the same pan and tilt angles with the camera and is identified as a rocket because we set its skill40 to a weird value (1234). We set skill10 (scale of the potential explosion sprite) to 3, and we use skill20 to limit the maximum distance that can be covered by the rocket. The speed of the rocket is given by skill1, and the origin of the particle trail is given by the very coordinates of the rocket.

The speed of the rocket is too big, so we have to use the particle_trail function if we want to see something; take a look at the red laser in the picture below.

function particle_trail()
{
temp.x = 100;
temp.y = 0;
temp.z = 0;
vec_rotate (temp,
you.pan);
vec_add (my.vel_x, temp);
my.alpha
= 70 + random(30);
my.bmap = ptrail_tga;
my.size
= 7;
my.flare = on;
my.bright = on;
my.beam = on; // comment this line if your engine can't use "beam"
my.move = on;
my.lifespan = 30;
my.function
= fade_particles;
}

That's your common sense particle function. It has a big speed on x because we want to use "beam" (comment that line if your engine doesn't support it). The particles have the same orientation with their creator (the rocket) and are run by the function named "fade_particles":

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

You can see that these particles are removed quickly; it might be a good idea to decrease "100" if you can't use "beam"; this change might improve the effect. We're coming close to the end now:

function
remove_rocket()
{
my.event = null;
ent_playsound
(my, destroyed_wav, 1000);
ent_create (explo13_pcx, my.pos,
explode_me);
my.invisible = on;
sleep (2);
ent_remove(me);
}

The function above is the event for our rocket; it makes the rocket insensitive to other events, plays a destroyed_wav sound, creates the explosion sprite, and hides the rocket for 2 more seconds, allowing the destroyed_wav sound to be played correctly. The last line of code removes the rocket.

Let's see what happens when the player and one of the asteroids collide:

function
player_was_hit()
{
my.event = null; // stop reacting to events
for now
my.shield -= you.skill10 * 1.5; // bigger asteroids cause
greater damage
my.skill33 = 0;
snd_play (hit_wav,
100, 0);
if (player.shield > 0) // the game isn't over yet?
{
while (my.skill33 < 10)
{
camera.roll = 2 - random(4); // shake the player a bit
my.skill33 += 1 * time;
wait
(1);
}
player.roll
= 0; // then restore player's roll
}
my.event
= player_was_hit;
}

The player stops reacting to events for a while; its "shield" is decreased depending on the size (skill10) of the asteroids. We reset skill33 and then we use it as a counter, shaking the player a bit by choosing a random roll angle (-2...2) every frame, and then we reset player's roll angle. This happens only if "shield" is bigger than zero; the ship won't shake if the player is already dead. The last line of code makes the player sensitive to impact again.

If player.shield <= 0, the ship falls on the ground and explodes; let's see the function that creates smoke particles:

function
particle_smoke()
{
my.alpha = 40 + random(50);
my.bmap = smoke_tga;
my.bright = on;
my.flare
= on;
my.move = on;
my.beam = on; // if your
engine supports it
my.size = 30;
my.function
= particle_fade;
}

There isn't too much to talk about this function; don't forget to comment the "beam" line if your engine can't us it. Oh, and we are using the same particle_fade function that was used for the plasma jets. Time to see the last function:

starter
compute_score()
{
while (1)
{
str_for_num(temp_str, score_value); // convert the value
to string
if (str_len(temp_str) == 1)
{
str_cpy(score_str, "0000000");
str_cat(score_str, temp_str);
}
if (str_len(temp_str) == 2)
{
str_cpy(score_str, "000000");
str_cat(score_str, temp_str);
}
if (str_len(temp_str) == 3)
{
str_cpy(score_str, "00000");
str_cat(score_str, temp_str);
}
if (str_len(temp_str) == 4)
{
str_cpy(score_str, "0000");
str_cat(score_str, temp_str);
}
if (str_len(temp_str) == 5)
{
str_cpy(score_str, "000");
str_cat(score_str, temp_str);
}
if (str_len(temp_str) == 6)
{
str_cpy(score_str, "00");
str_cat(score_str, temp_str);
}
if (str_len(temp_str) == 7)
{
str_cpy(score_str, "0");
str_cat(score_str, temp_str);
}
if (str_len(temp_str) == 8)
{
str_cat(score_str, temp_str);
}
wait (1);
}
}

This starter function is pretty big, but I wanted to display the score using one of the old school techniques; a score if 1500 is displayed like this "00001500" instead of "1500". Now you can see why we need to use a string and not a "digit" for the score. We convert the numerical value (score_value) to a string, we get its length and then we add the correct number of "0" digits at its left. That's all!

I think that P.S. has some potential. At this moment I see it as an action / logical / rpg combination, but who knows what can happen with it in the near future? I want to thank Nemisis for his great ideas and his support; the gameplay would have been worse without his help. I'll see you next month!


Untitled

Planet Survivors - part 3

This month we will see the second level of the game; the ship lands on a platform and we get to play a board game, gaining more ammo and shield.

I had to make a few changes to action player_level1 but don't worry, we will discuss them all. The main addition is a variable named landing, which controls the... landing of the ship, what else? When this var is zero, the ship flies as discussed in the previous P.S. article, but when landing = 1 the ship prepares for landing.

while (players_shield
> 0)
{
if (landing == 0)
{
// old stuff from Aum37 here
}
else // landing
= 1
{
player.passable = on;
player.transparent = off;
camera.x = 500;
camera.y = -200;
camera.z = -2000;
lbase.visible = on;
crosshair_pan.visible =
off;
my.pan = 0;
my.tilt = 0;
my.roll = 0;
my.y = 0;

The "else" branch runs when landing = 1 and does the following things: makes the player passable, in order to prevent the ship from colliding with the asteroids, and sets the "transparent" flag off. We choose a certain (experimental) position for the camera, and then we make the landing base (lbase) visible. Finally, we hide the crosshair and we reset the angles and the y of the ship, because the player might have had a funny position or angles before landing.

      while (player.x < 0)
{
if (player.z >
-2270) {player.z -= 14 * time;}
player.x += (vec_dist(player.x, lbase.x) / 60 + 2) * time;
vec_set(temp, player.x);
vec_sub(temp, camera.x);
vec_to_angle(camera.pan, temp);
engine_handle = ent_playsound (my, engine1_wav, 400);
my.skill15 = vec_dist(player.x, lbase.x);
if (my.skill15 < 1000)
{
snd_tune(engine_handle, 0, (my.skill15 / 10), 0);
}
wait (1);
}

The ship must land at about x = 0, y = 0, z = -2270. We use a while loop to increase its x and decrease its z. The speed on the x axis depends on the distance between the player and the landing base (lbase); it will decrease slowly as the player approaches the landing area, with 2 * time being the minimum speed. The camera will rotate towards the ship all the time, and the engine sound will be played and tuned, depending on the distance between the player and the base. This means that the engine sound will decrease its frequency as the ship approaches the base.

sleep (2);
my.engine = off;
temp.x = -35;
temp.y = 0;
temp.z = -2285;
ent_create (human_mdl, temp, move_human);
return;
}
wait (1);
}

The ship has landed now. We wait for 2 more seconds, and then we stop the particle generators (the engines). We set x = -35, y = 0, z = -2285 (experimental value) and we create a human_mdl entity at this position, attaching it the move_human function. Let's not forget that all these things were running inside a while loop, so we have to get out of the function using a return.

These were the major changes to the code from Aum37. Let's take a look at the new stuff:

function
compute_time()
{
sleep (130);
landing =
1;
}

This tiny function is called by player_level1; it waits until the soundtrack comes close to its end and then it sets landing to 1. I didn't include track1.wav in this month's archive because I know that some of you use 56Kb modems; don't forget to overwrite the track1.wav file with the one from Aum37.

action
landing_base
{
lbase = my;
my.invisible =
on;
my.passable = on;
while (landing == 0) {wait
(1);}
my.invisible = off;
}

The action above is attached to the landing base; it makes it invisible and passable (we don't want any nasty collision detection problems) and then it waits until landing is set to 1, which makes the base visible. I had to hide the base for a while because our level is pretty small, although it appears to be infinite for the innocent player.

Let's discuss the function that drives the little human:

function
move_human()
{
my.passable = on;
human =
my;
my.pan = 270;
my.enable_touch = on;
my.event = logical_help;
while (my.y > -170)
{
my.y -= 1.4 * time;
ent_cycle("walk", my.skill1);
my.skill1
+= 5 * time;
my.skill1 %= 100;
wait (1);
}
ent_cycle ("walk", 25);

The human is passable and gets an initial pan angle of 270 degrees; it is sensitive to touching and runs the logical_help function when you place the mouse pointer above it. This little guy appears below the ship and moves with the speed given by 1.4 * time until its y is smaller than -170 quants; the speed of its walking animation is given by 5 * time. The last line chooses a convenient standing position (25 is an experimental value).

  camera.x = -40;
camera.y = -140;
camera.z
= -2100;
camera.pan = 270;
camera.tilt = -65;
camera.roll = 0;
mouse_map = logical_pcx;
mouse_mode = 2;
mouse_spot.x = 0;
mouse_spot.y
= 0;
temp_bonus = 100;
sound_vol = 20;
while (logical_ended == 0)
{
temp_bonus -= 0.1 * time;
if (temp_bonus <
0) {temp_bonus = 0;}
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
wait (1);
}

The human has approached the logical board; we choose a new set of camera coordinates and angles, we set a new mouse pointer, we restore the default mouse_spot values (they were changed for the main menu) and then we set temp_bonus to 100, which is the maximum bonus that can be gained if the player solves the logical level quickly. I didn't stop the asteroids, so they're still colliding and producing noise in the background; this can upset the player a bit, so we reduce the volume for all the wave sounds to 20%. Finally, we decrease temp_bonus in a loop and we allow the player to move the mouse until it solves the logical level (until logical_ended = 1).

  sleep (1);
sound_vol = 100;
while (my.pan
> 90)
{
my.pan -= 25 *
time;
wait (1);
}
while (my.y < -100)
{
my.y += 1.4 * time;
ent_cycle("walk", my.skill1);
my.skill1 += 5 * time;
my.skill1 %= 100;
wait (1);
}
ent_remove(my);

The logical level is solved here; we wait for a second, we restore the volume for the exploding asteroids, and then we rotate the human towards the ship with the speed given by 25 * time. We move the human inside a while loop, playing its walking animation. The human is removed before reaching its target (the ship) but the player can't see this.

  camera.x = 500;
camera.y = -200;
camera.z
= -2000;
vec_set(temp, player.x);
vec_sub(temp,
camera.x);
vec_to_angle(camera.pan, temp);
while (minimum_bonus > 0) {wait (1);}
sleep (1);
fade_factor = 1000;
player.engine = on;
my = player;
plasma_jet(65, 63);

The ship prepares for the take-off: we restore the former camera position and we rotate the camera towards the ship. We wait until the player gets the entire bonus (ammo and shield), we stop for another second, and then we set a particle fade_factor of 1000. This means that the engine particles will have a short life; we need to do this because the speed of the ship will be really big.

We set the "engine" flag, we tell the engine particle function that "my", the entity that needs the particles, is the player itself. The last line starts the plasma jets.

  while (vec_dist(camera.x, player.x) < 10000)
{
player.skill15 = min(100, vec_dist(player.x, lbase.x)
/ 10);
player.x += (player.skill15 + 1) * time;
player.z += 5 * time;
engine_handle = ent_playsound (player, engine1_wav, 1000);
if (player.skill15 < 10000)
{
snd_tune(engine_handle, 0, (player.skill15),
0);
}
vec_set(temp,
player.x);
vec_sub(temp, camera.x);
vec_to_angle(camera.pan, temp);
wait (1);
}
beep;
exit;
}

The last part of the function increases the x coordinate of the ship from 1 * time to 101 * time, depending on the distance between the player and the base. We increase the z too, because the ship is taking off, and we play the engine sound, tuning its frequency according to the speed on x. The camera follows the ship until it is 10,000 quants away; when this happens, we play a beep and then we shut down the engine; that's where we will put a level_load instruction next month.

Let's see the actions and functions that are used for our board game:

starter init_logical()
{
mouse_range = 5000;
while ((tile1 == null) || (tile2 == null) || (tile3 == null) || (tile4 == null) || (tile5 == null) || (tile6 == null) || (tile7 == null) || (tile8 == null) || (tile9 == null)) {wait (1);}

This is a starter function that makes the entities sensitive to mouse clicks even if they are 5000 quants away from the camera. We wait until all the colored tiles are created and then we compute the values for the variables named status1... status9. Here's a typical board setup:

I have created 3 different wmb entities (red, green and blue) and I have placed them as shown in the picture above. Each tile has a simple action that looks like this attached to it:

action
tile_1
{
tile1 = my;
}

Each red tile has its flag1 set, each green tile has its flag2 set and each blue tile has its flag3 set. The good news is that this setup allows you to place the tiles in any order, as long as you use 3 tiles from each color. I chose an initial setup that can be solved with only a few moves - this is the first logical level, after all!

Let's
get back to our logical function:

while (1)
{
status1 = 1 * (tile1.flag1 == on) + 2 * (tile1.flag2 == on)
+ 3 * (tile1.flag3 == on);
status2 = 1 * (tile2.flag1
== on) + 2 * (tile2.flag2 == on) + 3 * (tile2.flag3 == on);
status3 = 1 * (tile3.flag1 == on) + 2 * (tile3.flag2 == on) + 3 * (tile3.flag3
== on);
status4 = 1 * (tile4.flag1 == on) + 2 * (tile4.flag2
== on) + 3 * (tile4.flag3 == on);
status5 = 1 * (tile5.flag1
== on) + 2 * (tile5.flag2 == on) + 3 * (tile5.flag3 == on);
status6 = 1 * (tile6.flag1 == on) + 2 * (tile6.flag2 == on) + 3 * (tile6.flag3
== on);
status7 = 1 * (tile7.flag1 == on) + 2 * (tile7.flag2
== on) + 3 * (tile7.flag3 == on);
status8 = 1 * (tile8.flag1
== on) + 2 * (tile8.flag2 == on) + 3 * (tile8.flag3 == on);
status9 = 1 * (tile9.flag1 == on) + 2 * (tile9.flag2 == on) + 3 * (tile9.flag3
== on);
if ((status1 == status2) && (status2
== status3))
{
if ((status4 == status5) && (status5 == status6))
{
give_bonus();
return;
}
}
if ((status1 == status4) && (status4 ==
status7))
{
if ((status2 == status5) && (status5 == status8))
{
give_bonus();
return;
}
}
wait (1);
}
}

Those 9 vars can have a value of 1 (red tiles, flag1 = on), or 2 (green tiles, flag2 = on), or 3 (blue tiles, flag3 = on). If the variable that checks tile1, the variable that checks tile2, and the variable that checks tile3 have the same value, the first row (1-2-3) contains identical colors. The inner if branch tests if the second row contains identical colors (4-5-6). If this is also true, the player has solved the level, because the third row (7-8-9) can only contain identical colors. We call function give_bonus() and then we get out of the function.

If the variable that checks tile1, the variable that checks tile4, and the variable that checks tile7 have the same value, the first column (1-4-7) contains identical colors. The inner if branch tests if the second column (2-5-8) contains identical colors. If this is also true, the player has solved the level (3-6-9 contains identical colors for sure).

But how do we change the colors? We can use up to 6 buttons (entities): v1... v3 and h1... h3.

I have clicked the v3 entity in the example above; the tiles are pushed one step higher; the one at the top goes down to the bottom. The same thing happens with the h1... h3 buttons, but the tiles are pushed in a horizontal direction. We have 6 buttons (entities) that move the tiles, but the code for them is almost identical, so we'll discuss a single button.

action
h1
{
my.ambient = -100;
my.enable_click =
on;
my.event = h1_clicked;
}

This bright button is sensitive to mouse clicks and runs its h1_clicked event function when it is clicked.

function
h1_clicked()
{
my.ambient = 100;
sleep (0.2);
my.ambient = -100;
if (status2 == 1) // tile2 is a red
tile?
{
ent_morph(tile1, tilered_wmb);
// then change into a red tile
tile1.flag1
= off; // reset all the flags
tile1.flag2 =
off;
tile1.flag3 = off;
tile1.flag1 = on; // tile1 will be a red tile from now on
}
if (status2 == 2) // tile2 is a green tile?
{
ent_morph(tile1, tilegreen_wmb); // then
change into a green tile
tile1.flag1 = off;
// // reset all the flags
tile1.flag2 = off;
tile1.flag3 = off;
tile1.flag2 = on; // tile1 will be a green tile from now on
}
if (status2 == 3) // tile2 is a blue tile?
{
ent_morph(tile1, tileblue_wmb); // then change
into a blue tile
tile1.flag1 = off; // // reset
all the flags
tile1.flag2 = off;
tile1.flag3 = off;
tile1.flag3 = on; // tile1 will be a blue tile from now on
}

The yellow buttons will increase their ambient for 0.2 seconds if they are clicked. If status2 (which, in case you don't remember, is the variable that checks the flags for the second tile) is 1, tile2 is a red tile. This means that we have to morph the first tile into a red tile (1 <-- 2 <-- 3). We reset all the flags, and then we set the correct flag for a red tile, which is flag1. If status2 is 2, tile2 is a green tile, so we morph the first tile into a green tile (once again, 1 <-- 2 <-- 3). We reset all the flags, setting flag2 to on. Finally, status2 = 3 means that tile2 is a blue tile. The function morphs the original tile (tile1) into a blue tile.

  if (status3 == 1) // tile3 is a red tile?
{
ent_morph(tile2, tilered_wmb); // then change into a
red tile
tile2.flag1 = off; // reset all the
flags
tile2.flag2 = off;
tile2.flag3 = off;
tile2.flag1 = on; // tile2
will be a red tile from now on
}
if (status3
== 2) // tile3 is a green tile?
{
ent_morph(tile2, tilegreen_wmb); // then change into a green tile
tile2.flag1 = off; // // reset all the flags
tile2.flag2 = off;
tile2.flag3 = off;
tile2.flag2 = on; // tile2 will be a green tile from
now on
}
if (status3 == 3) // tile3 is a blue
tile?
{
ent_morph(tile2, tileblue_wmb);
// then change into a blue tile
tile2.flag1
= off; // // reset all the flags
tile2.flag2
= off;
tile2.flag3 = off;
tile2.flag3 = on; // tile2 will be a blue tile from now on
}

The things look pretty much the same here, with the third tile (status3) being checked. If status3 = 1, tile3 is a red tile, if status3 = 2, tile3 is a green tile, and if status 3 = 3, tile3 is a blue tile. The code morphs the old tiles into the new ones, resets the flags and sets the proper ones for each tile, depending on its color.

if (status1 == 1) // tile1 is a red tile?
{
ent_morph(tile3, tilered_wmb); // then change into a red tile
tile3.flag1 = off; // reset all the flags
tile3.flag2 = off;
tile3.flag3 = off;
tile3.flag1 = on; // tile3 will be a red tile from now on
}
if (status1 == 2) // tile1 is a green tile?
{
ent_morph(tile3, tilegreen_wmb); // then change into a green tile
tile3.flag1 = off; // // reset all the flags
tile3.flag2 = off;
tile3.flag3 = off;
tile3.flag2 = on; // tile3 will be a green tile from now on
}
if (status1 == 3) // tile1 is a blue tile?
{
ent_morph(tile3, tileblue_wmb); // then change into a blue tile
tile3.flag1 = off; // // reset all the flags
tile3.flag2 = off;
tile3.flag3 = off;
tile3.flag3 = on; // tile3 will be a blue tile from now on
}
}

The last part of the function checks the color of the first tile (status1) and changes it depending on the color of its neighbor (tile3, because tile3 does the jump from the right side of the screen to the left). Let's examine two more tiny functions and we are done!

function
give_bonus()
{
logical_ended = 1; // the logical level has
ended
mouse_mode = 0; // // so hide the mouse
snd_play (victory_wav, 50, 0);
sleep (2);
minimum_bonus
+= temp_bonus; // add what's left of temp_bonus to minimum_bonus
while (minimum_bonus > 0)
{
snd_play (bonus_wav, 100, 0);
players_shield
+= 2;
players_ammo += 2;
minimum_bonus -= 2;
sleep (0.1);
}
sleep (1);
}

The function above is called as soon as you solve the logical level. It hides the mouse, plays a victory sound, waits for 2 seconds, and then it adds to the minimum_bonus variable (10) what's left from temp_bonus, which is set to 100 before starting the logical level, but decreases inside the while loop as the time passes by. We play a tiny sound and we increase player's shield and ammo with a value that can range from 10 to 110.

function
logical_help()
{
logical_txt.visible = on;
sleep (5);
logical_txt.visible = off;
}

This last function runs when you move the mouse pointer over the human, and displays a helpful text for 5 seconds. I have used a fixed width font for this text, which made it look pretty ugly, but readable by the A5 engine.


Untitled

Planet Survivors - part 4

This month we will meet the first enemies: two spaceships and (can't get rid of these) quite a few asteroids. Before I begin, be warned that this project will be a cocktail of games.

I care for you, so I have decided to make a standalone script; this way you won't have to play the previous P.S. levels to get here. You'll find the code for the standalone script at the beginning of the level2.wdl file; be sure to run this file if you want to play the level.

Let's discuss the code that creates the nice starfield effect first:

starter
particle_generator
{
var particle_start[3];
wait (3); // wait until the level is loaded
camera.pan = 0; //
now set the new camera position and angles
camera.tilt = -90;
camera.roll = 0;
camera.x = 330;
camera.y
= 0;
camera.z = 1000;
while(1)
{
particle_start.x = 1000;
particle_start.y = 1000 - random(2000);
particle_start.z
= -500 + random(200);
effect (particle_init,
1, particle_start, normal);
wait (1);
}
}

We are defining a variable named particle start; this will be the origin for our particles. You can see that the camera is using a fixed position, looking down at player's ship (tilt = -90); we will give the player the illusion that the ship is moving using the starfield. By the way, the stars are generated using the particle_init function:

function
particle_init()
{
temp.x = -(2 + random (15)) * starfield_speed;
temp.y = 0;
temp.z = 0;
vec_add
(my.vel_x, temp);
my.bmap = star_tga;
my.alpha
= 40 + random(60); // play with this value
my.size = 5 + random(5);
my.bright = on;
my.move = on;
my.lifespan
= 500;
my.function = size_particles;
}

function
size_particles()
{
my.size += 0.01 * time;
}

The particles have a random, negative speed on the x axis; play with starfield_speed if you want to change the global speed. I'm using the star_tga bitmap for the stars and I have set a random transparency factor and a random size for it. Its associate function size_particles() simply increases the size of the particles during their lives.

Time to take a look at the action attached to the player:

action
player_level2
{
player = my;
my.ambient =
-100;
my.enable_impact = on;
my.enable_entity
= on;
my.event = damage_player;
var speed_factor_x;
var speed_factor_y;

The player is sensitive to impact with other entities; its "damage_player" function is triggered as soon as that happens. Finally, we are defining two local variables that will store player's movement speed on the x and y axis.

  while (players_shield > 0)
{
if (key_cul == on)
{
if (speed_factor_y < 10) {speed_factor_y += 5 * time;}
}
else
{
if (speed_factor_y > 0) {speed_factor_y
-= 2 * time;}
}
if (key_cur == on)
{
if (speed_factor_y > -10) {speed_factor_y -= 5 * time;}
}
else
{
if (speed_factor_y < 0) {speed_factor_y
+= 2 * time;}
}
if (abs(speed_factor_y) > 1)
{
if (((speed_factor_y > 0) &&
(my.y < 500)) || ((speed_factor_y < 0) && (my.y > -500)))
{
my.y += speed_factor_y;
}
}

As long as the player is alive (players_shield > 0) the player can increase speed_factor_y slowly by pressing and holding the left arrow key; speed_factor_y is decreased slowly if the player releases the key. The same thing happens when we press the right arrow key. Why are we using the speed_factor_x and speed_factor_y variables? Mainly because the ship moves much smoother this way, by increasing and decreasing its speed smoothly. If speed_factor_y is bigger than 1 or smaller than -1, we move the ship sideways using the values stored at that moment in speed_factor_y. Please note that the ship can move from -500 to 500 quants on the y axis.

     if (key_cuu == on)
{
if (speed_factor_x <
10) {speed_factor_x += 5 * time;}
}
else
{
if (speed_factor_x > 0) {speed_factor_x -= 2 * time;}
}
if (key_cud == on)
{
if (speed_factor_x >
-10) {speed_factor_x -= 5 * time;}
}
else
{
if (speed_factor_x < 0) {speed_factor_x += 2 * time;}
}
if (abs(speed_factor_x) > 1)
{
if (((speed_factor_x > 0) && (my.x < 650)) || ((speed_factor_x <
0) && (my.x > 0)))
{
my.x
+= speed_factor_x;
}
}

The same thing happens on the x axis; the player increases or decreases speed_factor_x when it presses the up or down arrow keys. If the player has its x greater than 0 and below 650 quants, it can be moved with the speed stored in speed_factor_x.

if ((key_space == on) || (mouse_left == on))
{
player_fires();
}
wait (1);
}
}

If the player presses the space key or the left mouse button, function player_fires() is called:

function
player_fires()
{
proc_kill(4);
while ((key_space
== on) || (mouse_left == on)) {wait (1);} // wait until the player releases the
keys
snd_play (fire2_wav, 40, 0);
vec_set (temp.x,
my.x);
temp.x += 70; // the laser is generated in front of the
ship
ent_create (laser2_mdl, temp, move_bullet1);
players_ammo -= 1;
}

The first line makes sure that only a copy of this function will run. We wait until the player releases the space key or the left mouse button, we play a fire2_wav sound, we compute a position 70 quants in front of the player and then we create a laser2_mdl entity, assigning it the function named move_bullet1(). The last line of code decreases players_ammo because the player has fired a bullet.

function
damage_player()
{
wait (1);
snd_play (playerhit_wav,
70, 0);
players_shield -= 10;
if (players_shield
< 0)
{
players_shield =
0;
my.invisible = on;
my.passable = on;
effect(particles_explo2,
30, my.x,nullvector);
my.event = null;
}
}

The function above is the event associated to the player. When the player is hit by a bullet, by one of the enemy ships or by an asteroid, we play a playerhit_wav sound, we decrease players_shield, and then, if players_shield is smaller than zero, we set it to zero, we make the ship invisible and passable, we set player's event to null and we create an explosion effect using particles:

function
particles_explo2()
{
my.bmap = explo2_tga;
my.bright = on;
my.transparent = on;
my.alpha
= 100;
my.move = on;
temp.x = 1 - random(2);
temp.y = 1 - random(2);
temp.z = 0;
vec_normalize
(temp.x, (1 + random(1)));
vec_add (my.vel_x, temp.x);
my.size = 2;
my.lifespan = 50;
my.function
= explode_2;
}

function
explode_2()
{
my.alpha -= 4 * time;
my.size
+= 3 * time;
if (my.alpha <= 0)
{
my.lifespan = 0;
}
}

The bitmap used for the explosion is explo2_tga; it's a bright, transparent bitmap with its alpha set to 100. The bitmap will move in a random direction on the x and y axis; its associated function (explode_2) decreases the alpha and increases the size of the bitmap, removing the particle if its alpha goes below zero.

Let's examine that action that is used by the enemy ships:

// uses follow_player
action shuttle
{
my.ambient
= -100;
while (player == null) {wait (1);}
my.z
= player.z; // set the same height for the enemy ships and the player
var shuttle_speed;
my.skill10 = 1;
my.skill40
= 1; // I'm a ship
my.enable_impact = on;
my.event
= destroy_them;
shuttle_speed.x = -2 * time;
shuttle_speed.y = 0;
shuttle_speed.z = 0;
my.skill1
= 1 + int(random(70));

The ship will wait until the player is created, and then it will set its z to player's z value. We set skill10 to 1 (this value will be used in the event function), we set skill40 to 1 (this skill stores the id of the entity), we make the ship sensitive to impact and then we associate it the destroy_them() event function. The ships move only on the x axis; here's a picture of Wed's top view that gives more information:

I couldn't show you all the level here; feel free to make it as big as possible by increasing its size on the x axis, placing more ships, asteroids, and so on. Let's get back to our code:

    while ((my.x > -300) &&
(my.skill10 == 1))
{
if (my.follow_player
== on)
{
vec_set(temp, player.x);
vec_sub(temp, my.x);
vec_to_angle(my.pan,
temp);
}
move_mode = ignore_you + ignore_passable;
ent_move
(nullvector, shuttle_speed);
if ((vec_dist
(my.x, player.x) < 800) && (total_frames % 90 == my.skill1)) // random
firing intervals
{
if (my.x > player.x) // still got the chance to hit the player?
{
snd_play (fire1_wav, 40, 0);
ent_create (laser_mdl, my.x, move_bullet1);
}
}
wait
(1);
}
ent_remove (my);
}

As long as the ships haven't moved behind the player and they haven't been destroyed (their skill10 = 1), if we have the follow_player flag set on, the ships will rotate towards player's ship. This is what makes the two types of enemy ships different: one of them fires along the x axis, hitting the player only if it is placed behind it, while the other one rotates towards the player, trying to hit it harder :). The ships move ignoring passable objects and start firing if they are closer than 800 quants to the player and if total_frames % 90 is equal to a random value that was stored in skill1. In plain English, the ship will fire every 1.5 seconds (we have limited the frame rate to 60 fps) but the initial firing moment will be different for every ship. If the x coordinate for the ship is bigger than player's x, we play a fire1_wav sound, and then we create a laser_mdl entity, associating it the move_bullet1() function:

function
move_bullet1()
{
var bullet_speed;
my.passable
= on;
my.enable_impact = on;
my.enable_entity
= on;
my.event = destroy_bullet;
my.pan = you.pan;
bullet_speed.x = 50 * time;
bullet_speed.y = 0;
bullet_speed.z = 0;
my.ambient = 100;
my.bright = on;
my.light = on;
my.red = 50;
my.green = 255;
my.blue = 50;

This function is used by player's bullet and by the enemy ships. The bullet is passable at first and it is made sensitive to impact with other entities, its event function being destroy_bullet. The bullet gets the same pan angle with its creator (the player or the enemy ships) and a speed of 50 * time. We add some colored light to the model entity that will be used for the bullet.

  while (my != null) // as long as the bullet exists
{
if (you != null)
{
if(vec_dist (my.x, you.x) > 50)
{
my.passable = off;
}
}
move_mode = ignore_you
+ ignore_passable;
ent_move (bullet_speed,
nullvector);
wait (1);
}
}

As long as the bullet exists, if its creator continues to exist and the distance between the bullet and its creator is greater than 50 quants, the bullet becomes impassable. The bullet will move ignoring its creator and all the other passable entities. The event function associated to the bullet will play a sound and then it will remove the bullet:

function
destroy_bullet()
{
wait (1);
snd_play (explo1_wav,
30, 0);
ent_remove (my);
}

Let's see the code for the asteroids now:

action
asteroid
{
var asteroid_speed;
my.metal =
on;
my.ambient = -random(100);
my.scale_x =
2 + random(5);
my.scale_y = 2 + random(5);
my.scale_z
= 2 + random(5);
my.skill40 = 2; // I'm an asteroid
while (player == null) {wait (1);}
my.z = player.z; //
set the same height for the enemy ships and the player
my.enable_impact
= on;
my.event = destroy_them;
asteroid_speed.x
= -2 * time;
asteroid_speed.y = 0;
asteroid_speed.z
= 0;
my.skill10 = 1;
my.skill1 = 4 - random(8);
my.skill2 = 4 - random(8);
my.skill3 = 4 - random(8);
while ((my.x > -300) && (my.skill10 == 1))
{
move_mode = ignore_you + ignore_passable;
ent_move (nullvector, asteroid_speed);
my.pan += my.skill1 * time;
my.tilt += my.skill2
* time;
my.roll += my.skill3 * time;
wait (1);
}
ent_remove
(my);
}

These asteroids have a random ambient and scale; their skill40 is set to 2, allowing us to see if a certain entity is an asteroid or not. The asteroids wait until the player is created, and then they set their height to player's height, making sure that they will collide with the player if they get the change :). The asteroids use the same misterious destroy_them event function, and move towards the player with the speed given by -2 * time. We generate 3 random rotation speeds for pan, tilt and roll and we store them in skill1... skill3. The asteroids will move and rotate inside a while loop that continues to run if their x is greater than -300 quants and if their skill10 continues to be set to 1 (the asteroids haven't been destroyed).

Ok, let's examine the function that can destroy player's ship, the enemy ships and the asteroids:

function
destroy_them()
{
my.skill10 = 0;
snd_play
(explo1_wav, 50, 0);
if (my.skill40 == 1) // a ship was hit?
{
score_value += 100; // get 100 points
for a ship
effect(particles_explo2, 30, my.x,nullvector);
}
if (my.skill40 == 2) // hit one of the asteriods?
{
score_value += 50; // get 50 points
for an asteroid
effect(particles_explo1, 20,
my.x, nullvector);
}

If the player, or an enemy ship, or an asteroid is hit, it sets its skill10 to 0 (this means that it is destroyed). We play the explo1_wav sound and then we test the id (skill40) of the entity: if skill40 = 1, we have hit an enemy ship, so we increase score_value by 100 and we generate some particles using the previously discussed particles_explo2 function. If skill40 = 2, we have hit an asteroid, so we increase score_value by 50 and we generate particles using the particles_explo1() function. I'm not going to discuss the code for this particle function because it's identical with function particles_explo2(), using a different bitmap and another set of numerical values. Here are the 2 particle effects in action:

I'll see you all next month!


Untitled

Planet Survivors - part5

This month the action goes on as we learn to create three new enemy types, including a boss at the end of the level. Before I begin, let's see a small list with the changes:

- Player's bullets are destroyed if their x is greater than 700 quants; this way he won't be able to kill the enemies that are far away (he has to wait until they become visible). Check function move_bullet1() inside level2.wdl to see how I did that.

- The level is bigger now (40,000 quants) because we have added the new enemies to it and there wasn't enough room for them.

Let's see the action that drives the first new enemy:

action
shuttle2
{
my.ambient = 30;
while (player
== null) {wait (1);}
my.z = player.z; // set the same height
for the enemy ships and the player
var shuttle_speed;
my.skill10 = 1;
my.skill40 = 4; // I'm a ship
my.enable_impact = on;
my.event = destroy_them;
shuttle_speed.x = -2 * time;
shuttle_speed.y = 0;
shuttle_speed.z = 0;
while ((my.x > -300) &&
(my.skill10 >= 0))
{
if
((vec_dist (my.x, player.x) < 800) && (total_frames % 90 == 1))
{
if (my.x > player.x) // still got the chance to hit the player?
{
snd_play
(fire1_wav, 40, 0);
vec_set (temp.x, my.x);
temp.x -= 30;
temp.y -= 83;
ent_create (bolt_mdl, temp.x, move_bullet2);
vec_set (temp.x, my.x);
temp.x -= 30;
temp.y += 83;
ent_create (bolt_mdl, temp.x, move_bullet2);
}
}
move_mode
= ignore_you + ignore_passable;
ent_move (nullvector,
shuttle_speed);
wait (1);
}
ent_remove (my);
}

This shuttle waits until the player appears in the level, and then it adjusts its height to player's height, making sure that it (or its bullets) can collide with the player if it gets the chance :). We set skill40 to 4 for this shuttle because we want to give it some sort of id that will be used in its destroy_them event function.

The ship will move until its x goes below -300 quants (the player can't see it anymore) if its skill10 is bigger than or equal to zero. We will need to fire 2 shots if we want to destroy one of these shuttles. If the ships are closer than 800 quants to the player, the ship could start firing every 1.5 seconds because the frame rate is set to 60 fps and we are checking total_frames % 90, which would happen every 90 / 60 = 1.5 seconds. I said that the ship "could" start firing because it will fire only if its x is bigger than player's x (its position is above player's position). If this is correct, we play the fire1_wav sound, we set a temporary position for the first laser bolt, we create it, and then we set a temporary position for the second laser bolt and we create it. These bolts are driven by the move_bullet2() function. Oh, and before I show you that function, note that the shuttles move ignoring the passable entities, and get removed if their x goes below -300 or if they are hit twice.

function
move_bullet2()
{
var bullet_speed;
my.passable
= on;
my.enable_impact = on;
my.enable_entity
= on;
my.event = destroy_bullet;
my.pan = you.pan;
bullet_speed.x = 50 * time;
bullet_speed.y = 0;
bullet_speed.z = 0;
my.ambient = 100;
my.bright = on;
my.light = on;
my.red = 50;
my.green = 255;
my.blue = 50;
while (my
!= null) // as long as the bullet exists
{
if (you != null)
{
if(vec_dist (my.x, you.x) > 50)
{
my.passable
= off;
}
}
my.roll += 200 * time;
move_mode = ignore_you + ignore_passable;
ent_move (bullet_speed, nullvector);
wait (1);
}
}

Please note that the bullet is passable at first, and becomes impassable only when the distance between it and its creator (the shuttle) grows above 50 quants. The bolts are rotating with the speed given by 200 * time; play with that value if you'd like to set a different rotation speed.

The second ship is a bit more interesting: it creates a sphere of particles that grows over and over and destroys all the entities (player, ships, asteroids) in the area. Let's examine its code:

action
shuttle3
{
my.ambient = 30;
while (player
== null) {wait (1);}
ent_create (sphere_mdl, my.x, move_sphere);
my.z = player.z; // set the same height for the enemy ships and the player
var shuttle_speed;
my.skill10 = 1;
my.skill40
= 1; // I'm a ship
my.enable_impact = on;
my.event
= destroy_them;
shuttle_speed.x = -2 * time;
shuttle_speed.y = 0;
shuttle_speed.z = 0;
while
((my.x > -300) && (my.skill10 == 1))
{
move_mode = ignore_you + ignore_passable;
ent_move
(nullvector, shuttle_speed);
wait (1);
}
ent_remove (my);
}

The second shuttle creates a sphere_mdl entity at its position and assigns it the move_sphere function. The ship will move until its x goes below -300, if it manages to live that long (if my.skill10 continues to remain set to 1).

function
move_sphere()
{
proc_late();
var index;
var vertices;
my.passable = on;
my.invisible
= on;
vertices = ent_vertices(my);
while (you
!= null)
{
vec_set (my.x,
you.x);
if (you.x < 800)
{
if (my.scale_x == 1)
{
snd_play (scanning_wav, 60, 0); // this sound will be played only once
}
my.scale_x += 1 * time;
my.scale_x
= min(30, my.scale_x); // limit the scale to 30
my.scale_y = my.scale_x;
my.scale_z = my.scale_x;
temp.pan = 360;
temp.tilt
= 360;
temp.z = my.scale_x
* 37; // 37 = experimental value, must fit the size of the particle sphere
scan_entity (camera.x, temp);
}

The sphere is invisible and passable; we get its number of vertices, and then we use a while loop that will keep running for as long as the shuttle (you) exists. The loop sets the same coordinates for the ship and for the invisible sphere. If the ship has its x below 800 quants, if the scale of the sphere is 1 (which happens only once) the creepy scanning_wav sound is played. The sphere increases its scale until it reaches 30, and performs scan_entity instructions every frame, using an increased range every frame. You will need to play with 37 - it's an experimental value, and it must fit the size of the particle sphere.

    index = 0;
while (index < vertices)
// while without wait!
{
vec_for_vertex (temp, my, index);
effect (particle_effect3, 10, temp, normal);
index += 1;
}
my.pan
+= 3 * time;
my.tilt += 3 * time;
my.roll += 3 * time;
wait (1);
}
ent_remove (my);
}

The visual effect is created by the lines above: we use a fast while loop (without wait!) to pass through all the vertices of the sphere, and we generate particles using the particle_effect function. All this time, the sphere rotates by changing its pan, tilt and roll angles.

function
particle_effect3()
{
temp.x = 5 - random (10);
temp.y = 0;
temp.z = 0;
vec_add (my.vel_x,
temp);
my.bmap = glow_tga;
my.alpha = 80;
my.size = 5;
my.bright = on;
my.move
= on;
my.lifespan = 1;
my.function = destroy_particles;
}

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

There isn't anything special here, really. The particles are destroyed as soon as their alpha goes below zero.

Ready to meet the boss at the end of the level?

action
boss_level2
{
my.ambient = -30;
while (player
== null) {wait (1);}
my.z = player.z; // set the same height
for the enemy ships and the player
var boss_speed;
my.skill10 = 1;
my.skill20 = 100;
my.skill40
= 3; // I'm the boss
boss_speed.x = -2 * time;
boss_speed.y = 0;
boss_speed.z = 0;
while (my.x
> 650)
{
move_mode = ignore_you
+ ignore_passable;
ent_move (nullvector, boss_speed);
wait (1);
}
my.enable_impact
= on;
my.event = destroy_them;
boss_speed.x
= 0;
boss_speed.z = 0;
while (my.skill20 >
0)
{
if (player.y > my.y)
{
my.y += 5 * time;
}
else
{
my.y -= 5 * time;
}

The boss will start moving towards the player until its x is smaller than 650 quants, and then it will stop, becoming sensitive to impact with player's bullets. The boss will try to set its y to player's y, moving to the left or to the right depending on the position of the player.

     if (((my.y - player.y) < 200) && (total_frames
% 40 == 1))
{
if (player.invisible == on) {break;}
vec_set (temp.x, my.x);
temp.x
-= 120;
snd_play (fire1_wav,
40, 0);
ent_create (laserboss_mdl,
temp.x, move_bullet3);
wait
(1);
vec_set (temp.x, my.x);
temp.x -= 40;
temp.y += 145;
snd_play (fire1_wav,
40, 0);
ent_create (laserboss_mdl,
temp.x, move_bullet3);
wait
(1);
vec_set (temp.x, my.x);
temp.x -= 40;
temp.y -= 145;
snd_play (fire1_wav,
40, 0);
ent_create (laserboss_mdl,
temp.x, move_bullet3);
}
wait (1);
}
ent_remove (my);

If the boss is pretty close to the player on the y axis, if will start firing every 40 frames, which makes about 40 / 60 = 0.6 seconds. If the player is invisible (it was destroyed) the boss will get out of the loop, removing itself from the level. If the player isn't dead yet, the boss will set 3 firing spots (corresponding to its 3 weapons) and will create 3 laserboss_mdl entities, attaching them the move_bullet3 function.

  sleep (3);
my = null;
level_load (level3_wmb);
wait (3);
camera.pan = 0; // set the correct camera position
and angles
camera.tilt = -90;
camera.roll =
0;
camera.x = 330;
camera.y = 0;
camera.z = 1000;
}

The boss was destroyed here,
so that's the end of the level. We wait for 3 more seconds, and then we load the
next level (you'll see it next month), we wait until it is loaded and then we
set s new position and new angles for the camera. Let's get back to the function
that drives the bullets fired by the boss:

function
move_bullet3() // used by the boss at the end of the level
{
var bullet_speed;
my.passable = on;
my.enable_impact
= on;
my.enable_entity = on;
my.event = destroy_bullet;
my.pan = you.pan;
bullet_speed.x = 35 * time;
bullet_speed.y = 0;
bullet_speed.z = 0;
my.ambient = 100;
my.bright = on;
my.light =
on;
my.red = 50;
my.green = 255;
my.blue = 50;
while (my != null) // as long as the bullet exists
{
if (you != null)
{
if(vec_dist (my.x,
you.x) > 50)
{
my.passable = off;
}
}
if (vec_dist(player.x, my.x) > 400) //
not too close to the player?
{
vec_set(temp, player.x); // rotate the bullet towards player's ship
vec_sub(temp, my.x);
vec_to_angle(my.pan, temp);
}
move_mode = ignore_you + ignore_passable;
ent_move
(bullet_speed, nullvector);
wait (1);
}
}

The only part of the code that needs explanations is the while loop, which will run as long as the bullet exists. If the creator of the bullet (the boss) is alive and the distance between the boss and the bullet is smaller than 50 quants, the bullet is passable; otherwise, it becomes impassable. If the bullets aren't too close to the player (must be more than 400 quants away) they will rotate towards player's ship, which turns them into heat seeking missiles and makes the job harder for the player.

function
destroy_them()
{
snd_play (explo1_wav, 50, 0);
if (my.skill40 == 1) // a ship was hit?
{
my.skill10 = 0;
if (you.skill40 == 99) // if
it was player's bullet
{
score_value += 100; // get 100 points for a ship
}
effect(particles_explo2, 30, my.x, nullvector);
}
if (my.skill40 == 2) // hit one of the asteriods?
{
my.skill10 = 0;
if (you.skill40 == 99) // if it was player's bullet
{
score_value
+= 50; // get 50 points for an asteroid
}
effect(particles_explo1, 20, my.x, nullvector);
}

The function that destroys them all has changed quite a bit, so I have decided to show it to you again. If skill40 is set to 1, the player has hit a ship, so skill10 is set to zero, destroyed the ship. We check if the ship was hit by one of player's bullets (you.skill40 = 99) and only if this is true we add 100 points to the score. Finally, we generate a particle effect for the explosion. The same thing happens if we hit the asteroids; the player won't get points if the asteroids are shot by one of the enemy ships.

  if (my.skill40 == 3) // hit the boss at the end of the level?
{
my.skill10 = 0;
if (you.skill40 == 99) // if it was player's bullet
{
score_value
+= 100; // get 100 points for a shot
}
my.skill20 -= 10; // decrease the shield for the boss
if (my.skill20 <= 0)
{
snd_play (bossexplo_wav,
100, 0);
effect(particles_explo3,
100, my.x, nullvector); // similar to the other particle explosions
}
}
if (my.skill40
== 4) // shuttle2 was hit?
{
my.skill10 -= 1;
if (my.skill10 < 0)
{
if (you.skill40 == 99) // if it was player's bullet
{
score_value += 200; // get 200 points for shuttle2
}
effect(particles_explo2,
50, my.x, nullvector);
}
}
}

If the player has hit the boss at the end of the level (skill40 = 3) we will get 100 points for every shot because the boss is destroyed only if the player manages to hit it 10 times. Finally, if the player manages to destroy one of the shuttle2 ships, it gets 200 points because these ships need to be hit twice.

action
player_level3
{
while (1)
{
my.roll += 4 * time;
wait (1);
}
}

The action above demonstrates that the 3rd level is loaded when we kill the boss at the end of the level; don't worry, we'll have a more useful action for the player next month, in a fresh and (hopefully) exciting environment.


Untitled

Planet Survivors - part6

This month the player crashes its ship and is forced to land on a new planet, jumps out of the ship, gets some primitive movement code (so that you can explore the level) and a gun with unlimited ammo (for now). The level was created by Nemisis, my P.S. partner :) and you will find all the maps and wads for it inside the archive. Let's examine the new actions and functions inside the level2.wdl file.

function
ship_explosion(coordinates)
{
var number_of_gibbits;
effect (explosion_core, 10 + random(10), coordinates, nullvector);
sleep (0.3);
ent_create (explosion_pcx, coordinates, explosion_sprite2);
sleep (0.1);
number_of_gibbits = random(15) + 5;
while (number_of_gibbits > 0) // while without wait!
{
ent_create (gibbit_mdl, coordinates, move_gibbits);
number_of_gibbits -= 1;
}
}

The player will run into an asteroid field and this time the metallic asteroids can't be destroyed; therefore, player's ship will be severely damaged and you will see a nice looking explosion effect. I won't tell you how it works here; I use the same effect in the "Advance explosions" article at the bottom of this page and you'll learn how to create even better looking effects if you read that article.

action
metal_asteroid // unbreakable asteroids
{
while (first_boss
!= null) {wait (1);}
sleep (3);
my.x -= 33000;
my.scale_x = 2 + random(5); // set a random scale on x, y, z
my.scale_y = 2 + random(5);
my.scale_z = 2 + random(5);
asteroid_speed.x = -15 * time; // the metal asteroids have a big speed
asteroid_speed.y = 0;
asteroid_speed.z = 0;
my.skill1 = 4 - random(8); // set random rotation (pan, tilt, roll) speeds
my.skill2 = 4 - random(8);
my .skill3 = 4 - random(8);
while (my.x > -300)
{
move_mode = ignore_you + ignore_passable;
ent_move (nullvector, asteroid_speed);
my.pan += my.skill1 * time;
my.tilt +=
my.skill2 * time;
my.roll += my.skill3
* time;
wait (1);

These asteroids will wait until the first boss (the big ship at the end of the level) is destroyed; you should comment this line if you want to jump straight to the end of the level. When the boss is destroyed the asteroids will come closer to player's ship by 33,000 quants because they were standing still and they need to cover a lot of space quickly. We set a random scale (2...7) on the x, y and z axis for each asteroid, and then we set their speed on the x axis. We choose a random rotation speed (-4...4) for the pan, tilt and roll axis and then we start moving the asteroids inside the "while" loop.

if ((vec_dist (player.x, my.x) < 100) && (ship_down == 0))
{
ship_down = 1;
players_shield = 0;
ship_explosion (vector(player.x, player.y, player.z + 20));
snd_play (explostart_wav, 100, 0);
while (player.z > -10000)
{
player.z -= 100 * time;
player.roll += 25 * time;
wait (1);
}
level_load (level3_wmb);
}
if (ship_down
== 1)
{
my.passable = on;
}
}
ent_remove (my); // remove the asteroids
as soon as they can't be seen
}

If the player has come closer than 100 quants to one of the asteroids and this has happened for the first time, the ship will be brought down and players_shield will be set to zero. This allows us to take over the control of the ship, so the player won't be able to move it. We create an explosion 20 quants above the ship, we play a sound, and then we start to move the ship downwards, changing its roll angle at the same time. The next level (level3_wmb) will be loaded as soon as the height of the ship is below -10,000 quants. All the asteroids will become passable if the ship was brought down; this way they won't get stuck into each other even if the distance between them is really small. The last line of code removes the asteroids if they can't be seen anymore (if their x is smaller than -300 quants).

action
halo_flare
{
my.ambient = 100;
my.transparent
= on;
my.bright = on;
my.light = on;
my.lightred = 255;
my.lightgreen = 255;
my.lightblue = 255;
my.lightrange = 0;
my.facing
= on;
my.passable = on;
while(1)
{
my.alpha = min(100, vec_dist(my.x,
camera.x) / 20);
my.scale_x = min(3,
my.alpha / 50);
my.scale_y = my.scale_x;
wait(1);
}
}

action
crystal_glow
{
my.transparent = on;
my.bright
= on;
my.ambient = 100;
my.light = on;
my.lightred = 255;
my.lightgreen = 255;
my.lightblue = 100;
my.lightrange = 0;
}

These two actions control the light flare sprites and the light bulbs that can be observed in the first picture from this page. I have used passable sprites that are illuminated by pure white light (RGB = 255 255 255) and face the camera all the time for the flares. We get the distance to the camera inside a "while" loop and we compute the transparency factor depending on it. The scale for these flares can increase up to 3 times. The crystal light bulbs have a yellowish glow.

starter
level3_camera()
{
while (level_number != 3) {wait (1);}
while (player == null) {wait (1);}
camera.x = 20000;
camera.y = 9600;
camera.z = -500;
camera.pan
= 110;
camera.tilt = 0;
camera.roll = 0;
}

The lines of code above set the initial camera position for the 3rd level. I have decided to use a "starter" function, so it will start to run as soon as we run the game; therefore, I am using two "while" loops that make it wait until we get to the 3rd level and until player's ship is loaded. The rest of the lines set a convenient position and angles for the camera.

action
player_level3
{
player = my;
my.ambient
= -50;
level_number = 3;
my.scale_x = 5;
my.scale_y = my.scale_x;
my.scale_z = my.scale_x;
my.x = 10000;
my.y = 28000;
my.z = 8000;
my.pan = 295; // and angles
my.tilt = -50;
my.skill1 = 15000;
while (my.skill1 > 100)
{
vec_set (temp, my.x);
temp.z -= 15000;
trace_mode = ignore_me +
ignore_you + ignore_passable;
my.skill1 =
trace (my.x, temp);
my.roll += 20 * time;
my.x += 70 * time;
my.y -= 125 * time;
my.z -= 120 * time;
effect (particle_smoke, random(10) + 5, my.x, nullvector);
wait (1);
}

This action is attached to the falling ship in the 3rd level. We set a convenient position and angles for the ship, we set its skill1 to a huge value (15,000) and then we move it downwards until the distance between it and the terrain goes below 100 quants. This is a simple, effective method that allows us to land the ship in a certain area without using predefined paths or other methods that could work badly because of the big movement speeds. We rotate the ship by increasing its roll angle and we generate smoke using the particle_smoke function from explosions.wdl; read the "Advanced Explosions" article for more information.

   snd_play (explomid_wav, 20, 0);
my.skill1
= 0;
while (my.skill1 < 100)
{
my.skill1 += 10 * time;
camera.roll =
2 - random(4);
wait (1);
}
sleep (3);
ent_create (myhero_mdl, my.x, move_player);
while (player_1st == null) {wait (1);}
while
(vec_dist (player_1st.x, my.x) < 2000)
{
effect (particle_smoke, random(3) + 2, vector(my.x, my.y + 80, my.z + 200), nullvector);
wait (1);
}
}

The ship has landed, so we play an explomid_wav sound and then we reset skill1, because we will use it as a counter inside a loop that shakes the camera a bit by changing its roll angle every frame. We wait for 3 more seconds, we create the 1st person player model and we attach it the "move_player" function. We have to wait until the player appears in the level, and then we create thinner smoke until the player has moved at least 2000 quants away from the ship.

Let's examine the code that moves the player:

function
move_player()
{
var players_speed;
var track2_playing
= 0;
player_1st = my;
player = null;
my.invisible = on;
players_shield = 25;
players_ammo = 30;
my.x = 14830;
my.y = 19420;
my.z = -80;
my.pan = 300;
my.tilt = -50;
sleep (1);
camera.x = my.x;
camera.y
= my.y;
camera.z = my.z;
camera.pan = my.pan;
camera.tilt = my.tilt;
snd_loop (wind3_wav, 15, 0);
snd_play (smoked_wav, 100, 0);
sleep (3);

Our player has changed from a ship to a human, right? We will use the new player_1st pointer for the player from now on. The player is invisible (we don't need to see it in 1st person mode) and has 25 shield units and 30 bullets; he was injured during the landing but I bet that he'll find some health packs next month :). We choose a convenient position and angle for the player, close to the top of the ship, because we want to give me (and you) the illusion that the player will get out of the ship. This position and angles (copied to the camera) will hopefully trick us that we are in a smoky cockpit.

We start to play the wind3_wav sound in a loop using the snd_loop instruction that loops a sound perfectly, and then we play a "cough cough" sound that lasts for a few seconds.

   while (my.x < 15000)
{
my.x += 20 * time;
my.y -= 40 * time;
my.z -= 25 * time;
my.tilt += 2 * time;
camera.x = my.x;
camera.y = my.y;
camera.z = my.z;
camera.tilt = my.tilt;
wait (1);
}
snd_play (jump_wav, 80, 0);
while (my.tilt < 0)
{
my.tilt += 3 * time;
camera.tilt = my.tilt;
wait (1);
}
while (players_shield > 0)
{
players_speed.x = 15 * (key_w - key_s)
* time;
players_speed.y = 10 * (key_a
- key_d) * time;
vec_set (temp, my.x);
temp.z -= 5000;
trace_mode = ignore_me + ignore_passable + use_box;
players_speed.z = -trace (my.x, temp);
my.pan -= 5 * mouse_force.x;
my.tilt += 5
* mouse_force.y;

The player will jump on the ground by decreasing its z; we play the jump_wav sound and then we increase the tilt angle until the camera looks good. As long as the player is alive, we can move it using the "W", "S", "A", "D" keys and we keep it on the ground by tracing 5,000 quants below its feet and adjusting the height accordingly. Finally, we can look around using the mouse.

       if (mouse_left == 1)
{
players_bullet();
// then fire a bullet!
}
move_mode = ignore_passable + ignore_passents + glide;
ent_move(players_speed, nullvector);
camera.x
= my.x;
camera.y = my.y;
camera.z = my.z + 30;
camera.pan = my.pan;
camera.tilt = my.tilt;
if (((my.z <
-750) || (mouse_left == 1)) && (track2_playing == 0))
{
track2_playing
= 1;
media_loop("track2.wav",
null, 100);
}
wait (1);
}
my.skill46 = 0;
while (my.skill46 < 80)
{
ent_cycle("death", my.skill46);
my.skill46
+= 2 * time;
wait (1);
}
}

If the player has pressed the left mouse button (LMB) we run the function named "players_bullet". The camera will be placed 30 quants above player's origin and will use the same pan and tilt values with the player. If the height of the player goes below -750 quants (the player has come close to the first base) or if the player fires a bullet, we start playing the "track2.wav" soundtrack in a loop. The lines at the end of the action will run only if the player has died, which isn't the case for now; they simply play the "death" animation in a loop.

function
players_bullet()
{
proc_kill(4);
while (mouse_left
== 1) {wait (1);}
snd_play (bullet_wav, 50, 0);
ent_create (pbullet_mdl, player_1st.x, move_players_bullet); // create player's
bullet
}

The function above runs every time we press the LMB. We make sure that only one instance of the function is running, and then we wait until the player has released the LMB. We play a bullet_wav sound, and then we create the bullet model, attaching it the "move_players_bullet" function.

function
move_players_bullet()
{
my.pan = you.pan; // the bullet and
the player have the same pan angle
my.tilt = you.tilt; // and
tilt angle
my.passable = on; // at first
my.enable_entity
= on; // the bullet is sensitive to other entities
my.enable_impact
= on;
my.enable_block = on; // and to level blocks
my.event = remove_bullet;
my.ambient = 100;
my.light
= on;
my.lightred = 150;
my.lightgreen = 150;
my.lightblue = 255;
while (my != null)
{
if (vec_dist (player_1st.x, my.x) > 30) {my.passable = off;}
my.skill1 = 100 * time; // bullet speed
my.skill2
= 0;
my.skill3 = 0;
move_mode = ignore_you + ignore_passable;
ent_move
(my.skill1, nullvector);
my.roll += 100 * time;
wait (1);
}
}

The bullet will have the same pan and tilt angles with its creator (the player). We will make the bullet passable at first, though it is sensitive to impact with other entities or level blocks. Its event function is named "remove_bullet()". The bullet will move using a "while" loop, becoming impassable as long as the distance between it and the player is over 30 quants. Oh, and the bullet rotates by changing its "roll" angle.

function
remove_bullet()
{
my.event = null;
ent_playsound
(my, phit_wav, 1000); // player's bullet hits something
sleep
(0.4);
ent_remove (me);
}

This is the event function for the bullet: it sets its event to null, plays a sound at the impact position, waits a bit and then removes the bullet for good.

function
toggle_passable()
{
player.passable = (player.passable ==
off);
if (player.passable == on)
{
player.enable_scan = off;
player.transparent = on;
player.alpha
= 30;
}
else
{
player.enable_scan = on;
player.transparent
= off;
player.alpha = 100;
}
}

on_t = toggle_passable;

This function allows you to make player's ship invincible during the 2nd level, allowing you to get to the 3rd level without problems. Press the "T" keys as soon as the level is loaded and the player model will become passable, so all the enemy ships, bullets, as well as the asteroids will pass through it. Let's not forget that some of the enemies use "scan_entity" to hurt the player, so we have to disable player's enable_scan too. You will notice that the player is also transparent if you have made it transparent; Press "T" again to make the player vulnerable / opaque.


Untitled

Planet Survivors - part7

This month the player will get to move using a slightly modified action that includes inertia and friction, enjoys some health packs and ammo packs, and then meets the first enemy on the planet: a giant ant that fires deadly projectiles. I won't discuss the changes to the movement code because they are really simple and I have used similar snippets in some of my previous magazines.

action
health_pack
{
my.passable = on;
my.metal
= on;
while (player_1st == null) {wait (1);}
while (vec_dist(player_1st.x, my.x) > 70)
{
my.pan += 3 * time;
wait (1);
}
snd_play (pickedup_wav, 50, 0);
ent_remove
(my);
players_shield += 20;
players_shield
= min(200, players_shield);
}

action
ammo_pack
{
my.passable = on;
my.metal = on;
while (player_1st == null) {wait (1);}
while (vec_dist(player_1st.x,
my.x) > 70)
{
my.pan +=
3 * time;
wait (1);
}
snd_play (pickedup_wav, 50, 0);
ent_remove (my);
players_ammo += 50;
players_ammo = min(500, players_ammo);
}

The actions for the health packs and ammo packs are similar; we will discuss code the latter action. The ammo is passable and has its "metal" flag set. The ammo box will wait until the player_1st (the first person player) is created, and then it will wait until the player comes closer than 70 quants to the box, changing its pan angle (rotating) inside a loop until the player is close. As soon as the player approaches the box, a pickedup_wav sound is played, the box disappears and players_ammo increases by 50 bullets. The last line of code limits the ammo to 500 bullets. On a side note, players_shield is limited to 200 units.

Let's examine the code that drives the huge ant:

action
ant1
{
var ant_speed;
var ant_gun1;
var ant_gun2;
my.skill20 = random(100); // choose a random
"stand" frame for each ant
my.lightred = 0; // my.red for A6,
etc
my.lightgreen = 255;
my.lightblue = 0;
my.light = on;
my.lightrange = 0;
my.skill48
= 100; // the ant has 100 health points
my.enable_impact = on;
my.enable_entity = on;
my.event = damage_ant;
while (player_1st == null) {wait (1);}

The ant chooses a random my.skill20 value from the very beginning; skill20 controls the animation frames, so if we place 10 ants in our level, their "stand" animations won't be in sync, which is a good thing for us. Each ant glows green and has 100 health points, which are stored in their own skill48. The ants are sensitive to impact with other entities and run their damage_ant event function every time they collide with another entity. The last line of code will instruct the ants to wait until the player is created.

  while (my.skill48 > 0) // as long as I'm alive
{
if ((vec_dist (my.x, player_1st.x) < 5000) &&
(players_shield > 0))
{
vec_set(temp, player_1st.x);
vec_sub(temp, my.x);
vec_to_angle(my.pan,
temp); // the ant rotates toward the player
temp.tilt = 0;
temp.roll
= 0;
temp.pan = -my.pan;
vec_rotate(normal, temp);
temp.tilt = -asin(normal.x);
temp.roll = -asin(normal.y);
my.tilt += 0.1 * ang(temp.tilt - my.tilt);
// play with 0.1
my.roll
+= 0.1 * ang(temp.roll - my.roll); // play with 0.1

As long as the ant is alive, it checks if the player has come closer than 5000 quants to it and if the player is alive. If both statements are true, the ant rotates toward the player, and then we rotate the normal in a direction that is the opposite of ant's pan angle. This allows us to compute the slopes for the tilt and roll angle; we use those values to keep a correct orientation for the ant on the terrain. You will note that tilt and roll are increased or decreased slowly; decrease "0.1" to get a smoother angle adjustment effect if you want to.

        ant_speed.x = 25 * time;
ant_speed.y = 0;
vec_set (temp, my.x);
temp.z
-= 1000;
trace_mode = ignore_me
+ use_box;
ant_speed.z =
-trace (my.x, temp);
ent_move(ant_speed,
nullvector);
ent_cycle("walk",
my.skill19); // play "walk"
my.skill19 += 8 * time; // "walk" animation speed
my.skill19 %= 100; // loop the animation

The ant will move towards the player with the speed given by 25 * time and it will adjust its height depending on the height of the terrain below its feet. The "walk" animation speed is given by 8 * time.

        if (vec_dist (my.x, player_1st.x) <
2000) // if the player is closer than 2000 quants attack him
{
my.skill20 = 0; // reset the "attack" frames
while (my.skill20 < 100)
{
ent_cycle("attack", my.skill20); // play "attack"
my.skill20 += 10 * time; // "attack" animation speed
if
((my.skill20 > 40) && ((total_frames % 10) == 1))
{
vec_for_vertex (ant_gun1, my, 273);
ent_create (antsbullet_mdl, ant_gun1, ant_hits_player);
}
if ((my.skill20 > 40) && ((total_frames % 10) == 5))
{
vec_for_vertex (ant_gun2, my, 282);
ent_create (antsbullet_mdl, ant_gun2, ant_hits_player);
}
wait (1);
}
}
}

If the player comes closer than 200 quants to the ant we reset the attacking frames (skill20) and then we play the attack animation by increasing skill20. If the ant has played more than 40% of its "attack" animation frames, it fires 6 bullets a second from each one of its guns, alternatively (ant_gun1 fires after 1, 11, 21, 31, ... frames and ant_gun2 fires after 5, 15, 25, 35, .... frames).

  else // the player is farther than 5000 quants away
{
ent_cycle("stand", my.skill20); // play "stand"
my.skill20 += 2 * time; // "stand" animation speed
my.skill21 %= 100;
}
wait (1);
}
my.skill20 = 0;
while (my.skill20
< 90) // the enemy is dead
{
ent_cycle("death",
my.skill20); // play "death"
my.skill20 += 7 * time; //
"death" animation speed
wait (1);
}
my.alpha = 100;
my.transparent = on;
while (my.alpha >
0)
{
my.alpha -= 2 * time;
wait
(1);
}
ent_remove (my);
}

If the player got farther than 5000 quants away, the ant will switch to "stand", playing this animation in a loop. If we get to the line that resets skill20 again, the ant is dead, so we play its "death" animation and then we fade away its transparent body. The last line removes the ant model from our level.

function damage_ant()
{
if (you.skill47 == 1) // if this is a bullet fired by player's gun
{
my.skill48 -= 10; // lose 10 health points
ent_playsound (my, antangry_wav, 3000);
}
}

The function above will run if one of player's bullets hits the ant. The first line checks if the ant has collided with a bullet or not (it could also collide with another ant, player's model, and so on) and if this is true, it subtracts 10 health points and plays an antangry_wav sound.

function
ant_hits_player()
{
my.skill47 = 5; // that's an bullet fired
by the ant
my.pan = you.pan; // the bullet and the ant have the
same pan
my.tilt = you.tilt; // and tilt
my.enable_entity
= on; // the bullet is sensitive to other entities
my.enable_impact
= on;
my.enable_block = on; // and to level blocks
my.event = remove_bullet;
my.skill30 = 0;
while (my != null)
{
my.skill1
= 100 * time; // bullet speed
my.skill2 = 0;
if (my.skill30 < 1500)
{
my.skill3 = 0; // keep
the bullet running in a straight line at the beginning
}
else
{
my.skill3 -= 1 * time; // make it descend
after a while
}
move_mode = ignore_you + ignore_passable + ignore_push; // ignores the ant (its
creator = you)
my.skill30 += ent_move(my.skill1,
nullvector); // store the distance covered by ant's bullet in skill30
wait (1);
}
}

This function moves ant's bullet towards the player. I decided to allow the ants to miss most of the time because they have quite an impressive fire rate; this is why the bullets have the same tilt angle with ant's tilt angle. The bullets are sensitive to impact with other entities and level blocks / terrain and their associated event function is named remove_bullet. We reset skill30, and then we enter inside the while loop that keeps running for as long as the bullet exists. The bullet moves with the speed given by 100 * time; if it has covered less than 1500 quants, it keeps moving in a straight line, otherwise it will get a small negative speed on the z axis, which will make it impact with the ground. The bullet ignores its creator (the ant) and stores the result of its movement in its own skill30, used as a distance tracker inside the same loop.

function
remove_antsbullet()
{
my.event = null;
ent_playsound
(my, anthit_wav, 1000);
sleep (0.4);
my.passable
= on;
my.alpha = 100;
my.transparent = on;
while (my.alpha > 0)
{
my.alpha -= 2 * time;
wait (1);
}
ent_remove (my);
}

The function above is the event for ant's bullets. It stops the bullet from reacting to other events, plays a anthit_wav sound, wait for a bit, and then makes the bullet passable and transparent, decreases its alpha in a loop and removes the bullet.

function
hurt_player1st()
{
if (you.skill47 == 5)
{
my.event = null;
snd_play(playerhurt_wav, 50, 0);
players_shield
-= 5;
sleep (0.5);
my.event = hurt_player1st;
}
players_shield
= max(0, players_shield); // don't allow players_shield to go below 0
while (players_shield <= 0)
{
if (camera.roll < 70)
{
camera.roll += 5 * time;
}
wait (1);
}
}

The last function hurts the player if one of the bullets fired by the ant hits him or her. First of all, we check to see if the entity that has impacted with the player is an enemy bullet or not (skill47 = 5). If this is true we play a playerhurt_wav sound, we decrease players_shield, we make the player insensitive to other bullets for 0.5 seconds and then we set the event function again. We aren't allowing players_shield to go below zero (health = -15 wouldn't look to nice on a panel, right?) and then we change the roll angle of the camera inside a loop, in order to make the player a bit uncomfortable and let him know that he's dead now.


Untitled

Planet Survivors - part8

This month I have added a new enemy (the alien gunner), a new weapon (the sniper rifle), I have improved the movement code and a few more things.

action
alien_gunner
{
var gun_pos;
while (player_1st
== null) {wait (1);}
my.skill20 = 1; // the alien that controls
this turret isn't dead
ent_create (blastshield_mdl, my.x, stick_with_parent);
ent_create (alien_mdl, my.x, stick_with_gun);
while (my.skill20 == 1)
{
vec_set(temp, player_1st.x); // rotate the gun towards the player
vec_sub(temp, my.x);
vec_to_angle(my.pan,
temp);
if (my.tilt < -35) {my.tilt = -35;}
// the alien gunner has got a firing range of 15,000
quants
// fire every 1.5 seconds if the player
is in range and visible
if ((vec_dist (player_1st.x,
my.x) < 17000) && (total_frames % 90 == 1))
{
vec_set (temp, my.x);
temp.z += 150; // the alien uses its
head position to "see" (trace) the player
trace_mode = ignore_me + ignore_passable + use_box;
trace_distance = trace (temp.x, player_1st.x);
if ((trace_distance > 100) && (trace_distance < 15000)) // fire
only if the player is visible
{
vec_for_vertex
(gun_pos, my, 1); // fire from the first vertex of the gun
ent_create (projectile_mdl, gun_pos.x, move_projectile);
}
}
wait
(1);
}
while (my.tilt < 35)
{
my.tilt += 3 * time;
wait (1);
}
}

The code above is attached to the cannon model; it creates a shield and the alien_mdl models. As long as skill20 = 1 the cannon will rotate towards the player, with its lower tilt limit being set to -35 degrees. If the player has come closer than 17,000 quants, every 1.5 seconds (total_frames % 90 = 1 at 60 fps) we will trace from a position that is placed 150 quants above the canon to the player. If the player is visible we get the correct position of the firing vertex, and then we create the projectile. When skill20 != 1 the alien gunner is dead so the cannon tilts upwards.

function
stick_with_parent()
{
my.scale_x = 0.7;
my.scale_y
= my.scale_x;
my.scale_z = my.scale_x;
my.light
= on;
my.lightred = 100;
my.lightgreen = 100;
my.lightblue = 200;
my.lightrange = 0;
my.bright = on;
my.metal = on;
my.transparent
= on;
my.alpha = 30;
while (1)
{
my.x = you.x;
my.y = you.y;
my.z = you.z;
my.pan = you.pan;
my.tilt = you.tilt;
my.roll = you.roll;
my.lightblue = 100 + random(150);
wait (1);
}
}

The function above is attached to the shield, keeping it in sync with the cannon. The shield will have a blueish random glow.

function
stick_with_gun()
{
my.frame = 2; // needed by the alien model
my.skill1 = 100;
my.enable_impact = on;
my.enable_entity = on;
my.event = gunner_is_hit;
while (my.skill1 > 0) // not dead yet?
{
my.x = you.x;
my.y = you.y;
my.z = you.z;
my.pan = you.pan;
my.tilt = you.tilt;
my.roll = you.roll;
wait (1);
}
// the
alien was hit here (it is dead)
snd_play (aliendies_wav, 50,
0);
you.skill20 = 0; // stop the turret
while
(my.frame < 7)
{
my.frame
+= 0.3 * time;
wait (1);
}
}

function
gunner_is_hit()
{
wait (1);
my.skill1 -= 100;
}

The functions above are driving the alien: the first one keeps it in sync with the cannon model if it isn't dead and stops the turret when the alien is hit. The second function is the event that kills the alien if the player manages to shoot it.

function
move_projectile()
{
my.skill47 = 123; // to identify a bullet
fired by the alien gunner
my.pan = you.pan; // the bullet and
the weapon have the same pan
my.tilt = you.tilt; // and tilt
angle
my.enable_entity = on; // the bullet is sensitive to other
entities
my.enable_impact = on;
my.enable_block
= on; // and to level blocks
my.event = projectile_explodes;
my.ambient = 100;
my.light = on;
my.lightred
= 250;
my.lightgreen = 150;
my.lightblue = 55;
while (my.passable == off)
{
my.skill1 = 1000 * time; // bullet speed
my.skill2
= 0;
my.skill3 = 0;
move_mode = ignore_you + ignore_passable;
ent_playsound
(my, rocketflying_wav, 3000);
ent_move (my.skill1,
nullvector);
wait (1);
}
}

The projectile fired by the alien gunner moves with 1000 quants * time / frame and plays a 3D sound while it is moving.

function
projectile_explodes()
{
my.event = null;
sleep (0.1);
my.passable = on;
my.invisible
= on;
if (vec_dist (player_1st.x, my.x) < 300)
{
players_shield -= 1000 / (vec_dist(player_1st.x,
my.x) + 10);
}
ent_playsound (my, pexplo_wav,
2000); // the projectile hits something
effect (explosion_core,
10 + random(10), my.x, nullvector);
sleep (0.1);
ent_create (explosion_pcx, my.x, explosion_sprite2);
sleep (0.1);
ent_create (explosion_pcx, my.x, explosion_sprite2);
sleep
(0.1);
ent_create (explosion_pcx, my.x, explosion_sprite2);
sleep (0.1);
ent_create (explosion_pcx, my.x, explosion_sprite2);
sleep (0.1);
ent_create (explosion_pcx, my.x, explosion_sprite2);
sleep (0.1);
ent_create (explosion_pcx, my.x, explosion_sprite2);
}

If the projectile hits something, it checks if the player is closer than 300 quants to it and if this is true it damages the player. We play an explosion sound and several explosion effects from my explosions.wdl file.

action
sniper_weapon
{
my.passable = on;
while (player_1st
== null) {wait (1);}
while (vec_dist (my.x, player_1st.x) >
100) {wait (1);}
snd_play (gotsniper_wav, 50, 0);
my.invisible = on;
got_sniper_rifle = 1;
}

The action above is attached to the sniper rifle; it waits until the player comes close to it and then it sets got_sniper_rifle to 1 and disappears. Btw, the rifle is placed at the entrance of the first base.

function
toggle_sniper()
{
if (level_number != 3) {return;} // don't
show the crosshair if the player hasn't reached the 3rd level
if (got_sniper_rifle == 0) {return;}
sniper_pan.visible = (sniper_pan.visible
== off);
camera.arc = 70 * (sniper_pan.visible == off) + 10 *
(sniper_pan.visible == on);
}

on_mouse_right = toggle_sniper;

The rifle can be used with or without zooming in; press the right mouse button to toggle the sniper mode on and off. The rifle will only work if we have reached the 3rd level (this level) and only if the player has picked up the rifle. The zooming factor will range from 1 (camera.arc = 70) to 7 (camera.arc = 10).

The alien gunners are pretty tough to beat; therefore, I have added a cheat that is enabled if you press the F5 key:

function
cheater()
{
got_sniper_rifle = 1;
players_ammo
= 999;
players_shield = 999;
}

on_f5 = cheater;

If you can't finish the level with 999 shield points and 999 bullets you can make those values even bigger. Please note that we've got 3 aliens and 2 ants in the level; you should add more enemies but we are interested in making it to the getaway ship:

action
ship_takes_off
{
while (player_1st == null) {wait (1);}
while (vec_dist (player_1st.x, my.x) > 350) {wait (1);}
end_level3 = 1; // free the camera
var ship_dist;
ship_dist.y = 0;
snd_play (shipout_wav, 50, 0);
while (my.y < 25000)
{
ship_dist.x = 800 * time;
ship_dist.z = 130
* time;
ent_move(ship_dist, nullvector);
vec_set(temp, my.x); // rotate the camera towards player's
ship
vec_sub(temp, camera.x);
vec_to_angle(camera.pan, temp);
wait (1);
}
exit;
}

This ship waits until the player comes close to it, and then it moves away until its y is bigger than 25,000 quants. The game ends by calling the "exit" instruction.

I have changed the movement code as well; now you can fall smoothly and (more important) you can fly by pointing your nose upwards. This way you can jump over obstacles and get to the end of this big level in only a few minutes.


Untitled

Planet Survivors - récap

This month I have wrapped up all the levels so that they can be played in a sequence, I have added more code to make the pieces work together, I have fixed some bugs and so on. There's a lot of fresh code under the hood, but let's review some of the gameplay-related elements.

The action starts with a 3D space shooter where you have to shoot some asteroids:

A few minutes later, the ship lands on a platform:

The player has to solve a small logical game (a puzzle) in order to move on to the next level; it has to create 3 lines or 3 rows of identical colors:

If the player solves the puzzle it receives ammo and shield points and the ship takes off, resuming player's mission:

The action moves to a 2D top view space arcade with 3 different enemies and a boss at the end of the level:

The player moves into a field of asteroids that can't be destroyed, so its ship is severely damaged:

The ship crashes into the soft soil of a planet:

The player manages to get away from the ship, gets a sniper riffle and is prepared to meet two new enemy types: some green, gigantic ants and some alien rocket shooters:

The player manages to get to a new ship and takes off, ending the game. You can make the player fly in the shooter level by pointing its nose upwards; this way you will be able to move over the hills and get to the end of the level in a few minutes (if you can kill the aliens, of course).

Ok, I'll answer your most asked question about this project: how do I solve the puzzle level using a scientific method (not by trial and error)?

I'll use the very example that comes with the project but this method will work with any other combination.

1) Create a 2x2 square with properly aligned colors:

The good news is that you'll only have to play with the (red) row and column that don't include our aligned square from now on:

Let's examine the color sequence of the first two cells in the upper row: it is blue followed by green. This means that the lower row must start with blue as well and green should follow it. We push the blue cell from the lower row one step to the left:

Now we have to bring the green cell down:

We've got the same color sequence (blue, green) on the lower row; let's push the two cells to the left:

That's it! The level is solved and the player can continue the game; we have aligned two columns / two rows and the third row / column got aligned automatically. Don't forget that you get more bullets and shield if you solve the puzzle quickly. Next month we will take care of the level loading and saving.


Untitled

Planet Survivors - the end

Believe it or not, this series is over! I needed quite a few months to create it but now we've got a project that includes four completely different games under a single umbrella. This month I have fixed some minor bugs and I have created the load / save system; we'll concentrate our efforts in this direction.

string name1_str = " "; // up to 39 characters, feel free to increase the limit
string name2_str = " "; // up to 39 characters, feel free to increase the limit
string name3_str = " "; // up to 39 characters, feel free to increase the limit
string name4_str = " "; // up to 39 characters, feel free to increase the limit

text
slot1_txt
{
pos_x = 205;
pos_y = 180;
layer
= 150;
font = system_font;
string = name1_str;
}

.............................

text
slot4_txt
{
pos_x = 205;
pos_y = 480;
layer
= 150;
font = system_font;
string = name4_str;
}

I have decided to create four saving slots; name1_str will store the name given to the first saving slot, name2_str will store the name from the second slot and so on. I have also defined four texts that will display the names on the "Load game" and "Save game" panels.

panel blackpanel_pan
{
bmap = blackpanel_pcx;
layer = 2;
pos_x = 0;
pos_y = 0;
flags = overlay,
refresh;
}

panel savegame_pan
{
bmap = savegame_pcx;
layer
= 100;
pos_x = 0;
pos_y = 0;
button = 198, 158,
slot2_pcx, slot1_pcx, slot2_pcx, ps_save1, null, mouse_over;
button
= 198, 258, slot2_pcx, slot1_pcx, slot2_pcx, ps_save2, null, mouse_over;
button = 198, 358, slot2_pcx, slot1_pcx, slot2_pcx, ps_save3, null, mouse_over;
button = 198, 458, slot2_pcx, slot1_pcx, slot2_pcx, ps_save4, null, mouse_over;
flags = overlay, refresh;
on_click = game_was_saved;
}

panel loadgame_pan
{
bmap = loadgame_pcx;
layer
= 100;
pos_x = 0;
pos_y = 0;
button = 198, 158,
slot2_pcx, slot1_pcx, slot2_pcx, ps_load1, null, mouse_over;
button
= 198, 258, slot2_pcx, slot1_pcx, slot2_pcx, ps_load2, null, mouse_over;
button = 198, 358, slot2_pcx, slot1_pcx, slot2_pcx, ps_load3, null, mouse_over;
button = 198, 458, slot2_pcx, slot1_pcx, slot2_pcx, ps_load4, null, mouse_over;
flags = overlay, refresh;
on_click = cancel_loading;
}

I have defined a panel with a black bitmap; I'm using it to hide the game window when the player presses Esc in order to bring on the main menu. The following panels are used for saving and loading; each one of them has four buttons, which are our save / load slots. Whenever we click the savegame_pan panel, its associated game_was_saved() funtion is run; the same thing happens with loadgame_pan and function cancel_loading().

What happens when you press the Esc key during the game? This key used to shut down the game instantly, but now it has got a different role - it runs the function below:

function
return_to_main()
{
if (main_pan.visible == on) {return;}
freeze_mode = 1;
blackpanel_pan.visible = on;
main_pan.visible
= on;
mouse_mode = 2;
while (freeze_mode == 1)
{
mouse_map = pointer1_tga;
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
wait (1);
}
}

If the main panel is already visible, the function returns; otherwise, all the actions in the level are stopped and the black panel that hides what's happening in the level is displayed. We show the main panel as well; this panel has a bigger layer when compared to the black panel, so it will appear over it. We show the mouse pointer and then we allow the mouse to move for as long as the game is freezed.

function
save_game()
{
loadgame_pan.visible = off;
savegame_pan.visible
= on;
save_handle = file_open_read("save1.dat"); // open the file
if (save_handle != 0) // if the file exists
{
file_str_read(save_handle,
name1_str);
slot1_txt.string = name1_str;
slot1_txt.pos_x
= 205 + (39 - str_len(name1_str)) * 5; // center the text, allow up to 39 characters,
5 = 10 / 2 (character width / 2)
slot1_txt.visible = on;
file_close(save_handle);
}
.....................................................
save_handle = file_open_read("save4.dat"); // open the file
if (save_handle
!= 0) // if the file exists
{
file_str_read(save_handle,
name4_str);
slot4_txt.string = name4_str;
slot4_txt.pos_x
= 205 + (39 - str_len(name4_str)) * 5; // center the text, allow up to 39 characters,
5 = 10 / 2 (character width / 2)
slot4_txt.visible = on;
file_close(save_handle);
}
}

The function above will run whenever the player presses the "Save" button on the main menu panel. This function will hide the "Load game" panel if it was visible and will display the "Save game" panel. We open the file named save1.dat (if it exists) and we read the name of the saving slot from the file, assigning it to the slot1_txt text. We center the text on the saving slot, and then we make it visible. Finally, we close the file and the process repeats for all the four saving slots.

If you are wondering what's with those save1.dat... save4.dat you are not alone; I have asked myself why I used them as well... Hold on, now I remember: we need to save the name of the slot (the save game name) in a separate file because we want to display it on the "Load game" or "Save game" slots before loading / saving a new game. My demo comes with saved games named level1... level4 (guess why) but you can use any name for your save, as long as you make it shorter than or equal to 39 characters. How did I come up with this value? A character in my font is 10 pixels wide and the button has 400 pixels, which means that I can safely use 39 * 10 = 390 pixels out of those 400 without coming close to the boundaries.

function
load_game()
{
media_stop(media_handle); // stop the soundtrack
savegame_pan.visible = off;
loadgame_pan.visible = on;
save_handle
= file_open_read("save1.dat"); // open the file
if (save_handle != 0)
// if the file exists
{
file_str_read(save_handle,
name1_str);
slot1_txt.string = name1_str;
slot1_txt.pos_x
= 205 + (39 - str_len(name1_str)) * 5; // center the text, allow up to 39 characters,
5 = 10 / 2 (character width / 2)
slot1_txt.visible = on;
file_close(save_handle);
save1_exists = 1;
}
else
{
save1_exists = 0;
}

  ...................................................
save_handle = file_open_read("save4.dat"); // open the file
if (save_handle
!= 0) // if the file exists
{
file_str_read(save_handle,
name4_str);
slot4_txt.string = name4_str;
slot4_txt.pos_x
= 205 + (39 - str_len(name4_str)) * 5; // center the text, allow up to 39 characters,
5 = 10 / 2 (character width / 2)
slot4_txt.visible = on;
file_close(save_handle);
save4_exists = 1;
}
else
{
save4_exists = 0;
}
}

Function load_game() will run each and every time you press the "Load" button on the main menu. It stops any soundtrack that might be playing at the moment, hides the "Save game" panel, displays the "Load game" panel and reads the data from save1.dat (if the file exists), storing the name of the saved game in name1_str. The string is passed to the slot1_txt text, which gets centered on the button and made visible. If the file exists, the variable save1_exists is set to 1; otherwise, it will be set to zero. We will use this variable a bit later; we need it because we wouldn't want our customers to click and load a saved game from a slot in which they didn't save anything, right? The same thing happens with the rest of the slots.

function
ps_save1()
{
if (inkey_active == on) {return;}
if (players_shield
<= 0) {return;}
slot1_txt.visible = on;
str_cpy(name1_str,
"");
while (str_cmpi(name1_str, "") == 1)
{
inkey(name1_str);
wait (1);
}
slot1_txt.string
= name1_str;
slot1_txt.pos_x = 205 + (39 - str_len(name1_str)) * 5;
save_handle = file_open_write("save1.dat");
file_str_write (save_handle,
name1_str);
file_close(save_handle); // close the file
game_save("save",
1, sv_all - sv_info);
game_was_saved();
}

This is the function that runs when the player presses the first button on the "Save game" panel; trust me when I tell you that the other function are similar. The first line gets out of the function if inkey is already active (if the player has pressed the same button twice). The same thing happens if the player wants to save a game in which the player is dead. We display the text that will contain the name of the saved game, we reset its associate name1_str string, and then we wait inside the while loop until we are sure that the player has typed something in, storing the input inside name1_str.

Moving outside of the while loop now; we center the text on the screen and then we create and open the file named save1.dat, write the name of the save inside it. Finally, we save the game (using Conitec's "game_save" instruction) inside the save1.sav file. The last line runs the game_was_saved() function which hides some panels, makes sandwiches, and so on.

function
ps_load1()
{
if (save1_exists == 0) // the slot is empty?
{
return; // get out of here!
}
game_load("save",
1);
game_was_loaded();
if (level_number == 1)
{
media_play ("track1.wav", null, 50); // play the first soundtrack
(3D space + logical level)
}
if (level_number == 3)
{
media_loop("track2.wav", null, 100); // play the second soundtrack
(shooter leve)
}
}

The function above (as well as its other three sisters) runs when you press a button on the "Load game" panel. It checks is the saved game exists or not; if it does exist, it loads the data that was stored inside the save1.sav file, and then runs the game_was_loaded() function that does some cleanup. If the save was taken from the first level, we play the first soundtrack; otherwise, if the save was taken from the third (shooter) level, we play the second sound track in a loop. Why did we do that? Well, we have stopped the soundtracks and they can't be resumed that easily (unless you are using several smaller-sized melody slices or more complicated code) so we simply start the tracks again when we load the 1st or the 3rd level.

function
game_was_saved()
{
if (players_shield <= 0)
{
savegame_pan.visible = off;
return;
}
else
{
sleep (2);
slot1_txt.visible
= off;
slot2_txt.visible = off;
slot3_txt.visible
= off;
slot4_txt.visible = off;
savegame_pan.visible
= off;
blackpanel_pan.visible = off;
main_pan.visible
= off;
if (level_number == 1)
{
if (human == null)
{
mouse_mode = 0;
mouse_map = pointer1_tga;
}
else
{
mouse_mode = 2;
mouse_map
= logical_pcx;
}
}
if (level_number == 2)
{
mouse_mode
= 0;
}
if (level_number == 3)
{
mouse_mode = 0;
}
sleep
(1);
freeze_mode = 0;
}
}

This function runs at the end of the saving process, making various tweaks, depending on the level number. If the player is dead, the function hides the "Save game" panel and exits; otherwise, it waits for 2 seconds, and then it hides the texts that hold the names of the saved games, as well as the main panel. If we are at the first level and the "human" pointer is null, this means that we were still playing the " 3D space game" part of the first level, so we hide the mouse pointer (the code that is used for the ship will display it) and then we set the proper pointer1_tga mouse pointer. If "human" isn't null, we are playing the "logical game" part of the first level, because "human" is the little guy that comes out of the ship and is created as soon as the ship lands. We show the mouse pointer and we set for it the logical_pcx bitmap.

If we are playing the second level, we hide the mouse pointer because it isn't needed. The same thing happens if we are playing the third level. We give the player a second to remember where he / she was when she has decided to save the game, and then we unfreeze it.

function
cancel_loading()
{
loadgame_pan.visible = off;
slot1_txt.visible
= off;
slot2_txt.visible = off;
slot3_txt.visible = off;
slot4_txt.visible = off;
}

This function will run if the player clicks the "Load game" panel. I have written "click the bottom of the screen if you have got here by mistake" on the panel, but the truth is that you can click any area of the panel, excluding the buttons, if you have clicked "Load" on the main panel by mistake. If that has happened, the loading panel and its associated texts will be made invisible.

function
game_was_loaded()
{
loadgame_pan.visible = off;
savegame_pan.visible
= off;
slot1_txt.visible = off;
slot2_txt.visible = off;
slot3_txt.visible = off;
slot4_txt.visible = off;
main_pan.visible
= off;
sleep (1);
freeze_mode = 0;
}

The last function will clean up the mess after loading a saved game; it hides some panels and texts, waits for a second and then resumes the game.

Please allow me to close this series with a few advices that are applicable whenever you create a full game:

- Try to avoid "starter" functions; they will make your life harder in the end;

- Try to use as many local variables as possible. Avoid as many global variables as you can;

- Do as many initializations as possible inside function main; you are going to restart the game by running main() anyway (unless you are using some strange, more complicated method).


Untitled

Grenades

Many of you have asked this question at the forum: where can I find some good grenade throwing code? Look no further, for this article will tell you how to create the code for a grenade that explodes on impact, destroying all the entities in its range. Interested in seeing some chained explosions? I have set up a demo level especially for you!

Let's begin with a starter function:

starter init_grenades()
{
while (player == null) {wait
(1);}
while (1)
{
if (mouse_left == 1)
{
ent_create (grenade_mdl, player.x, move_grenade);
while (mouse_left == 1) {wait (1);} // wait until the LMB is released
}
sleep (0.1);
}
}

This function waits until the player is created, and then starts a while (1) loop. Every time we press the left mouse button, a grenade is created and it starts moving as instructed by function move_grenade:

function
move_grenade()
{
my.passable = on;
my.pan
= camera.pan;
my.tilt = camera.tilt;
my.enable_entity
= on;
my.enable_impact = on;
my.enable_block
= on;
my.event = grenade_event;
while (my !=
null)
{
if (vec_dist (my.x,
player.x) > 50) {my.passable = off;}
my.skill1
= 50 * time; // movement speed
my.skill2 =
0; // speed on y
if (my.skill40 < 100) //
covered at least 100 quants?
{
my.skill3 = 0; // if not, don't apply gravity yet
}
else // moved away from the player
{
my.skill3 = -5 * (my.skill40 / 100) * time;
}
move_mode = ignore_you + ignore_passable;
my.skill30 = ent_move (my.skill1, nullvector); // get
the distance that was covered this frame in skill30
my.skill40 += my.skill30; // store the distance covered so far in skill40
wait (1);
}
}

This function is similar to the one that was used for the flares in "Beginner's corner"; the grenade is sensitive to impassable entities and to level blocks, and runs its "grenade_event" function as soon as one of these collisions takes place. The grenade will become impassable when the distance between it and the player grows above 50 quants. We apply gravity to the grenade if it has covered at least 100 quants, just like in the picture below; play with "-5" if you want to adjust the gravity. Oh, and we are using skill40 to store the total distance covered by the grenade, skill30 being set to the distance that is covered every frame.

What happens when the grenade hits something? Let's examine its event function:

function
grenade_event()
{
vec_set (temp, my.pos);
temp.z += 10; // create the explosion sprite 10 quants above the ground
ent_create(explosion_pcx, temp, animated_explosion);
ent_remove (me);
}

We store the current position of the grenade in temp, we add 10 quants to the z coordinate, and then we create a sprite named explosion_pcx at this position, attaching it the function named animated_explosion. The last line of code removes the grenade.

function
animated_explosion()
{
my.passable = on;
my.flare = on;
my.ambient = 100;
ent_playsound
(my, explosion_wav, 500);
while (my.frame < 7)
{
my.frame += 1 * time;
my.scale_x += 2 * time;
my.scale_y = my.scale_x;
my.scale_z = my.scale_x;
player.roll = 2 - random(4); // shake the player a bit
wait (1);
}
player.roll = 0;
my.invisible = on;
temp.pan = 360;
temp.tilt
= 180;
temp.z = 400;
scan_entity (my.x, temp);
sleep (2);
ent_remove (me);
}

The explosion sprite is passable, has its flare flag set and its ambient is 100. We play an explosion_wav sound at the impact coordinates, and then we play all the animation frames for our sprite in a while loop.

The same while loop increases the scale for the explosion sprite every frame for a more dramatic effect and shakes the player a bit; in fact, it simply chooses a random value from -2 to 2 for camera.roll. As soon as we get out of the while loop we reset camera.roll, we hide the explosion sprite; we can't remove it yet because the explosion sound associated to it continues to play.

The time has come for us to cause some damage in the area, so we set a scanning range of 360 degrees horizontally, 180 degrees vertically and a range of 400 quants, and then we run scan_entity. This means that every entity that has its enable_scan flag set will trigger its event. The last few lines of code make sure that we wait until the explosion sound has finished, and then the sprite is removed.

How about some entities that can be destroyed by our grenade? See for yourself how simple it is to create them:

action barrel
{
my.enable_scan = on;
my.event = barrel_explodes;
}

These barrels are sensitive to scanning and run their barrel_explodes event function as soon as they are scanned.

function
barrel_explodes()
{
my.event = null;
wait
(1 + random(20)); // wait up to 20 frames
ent_morph (my, barrel_pcx);
my.flare = on;
my.ambient = 100;
ent_playsound (my, explosion_wav, 200);

The first line sets the event function to null; this way, a barrel can't trigger its event more than once. We wait a random number (1..20) frames because chained explosions look better this way, and then we morph the barrel into an explosion sprite with its flare flag set. We need a bright explosion, so we set the ambient of the sprite to 100. Finally, we play an explosion sound.

   while (my.frame < 10)
{
my.frame += 1 * time;
wait (1);
}
my.invisible = on;
temp.pan = 360;
temp.tilt = 180;
temp.z =
150;
scan_entity (my.x, temp);
sleep (2);
ent_remove (me);
}

The explosion sprite plays its animation frames in a while loop, then it becomes invisible and performs a scan on a smaller range (150 quants) because a barrel can (and should) cause some turbulence in the Force ;) by scanning (triggering the events) of the other entities nearby that have their enable_scan set. We wait for 2 more seconds because we want to make sure that the barrel explosion sound has ended, and then we remove the invisible explosion sprite.

Load my office.wmp demo level, throw a grenade and then sit back, grab some popcorn and enjoy the chained barrel explosion show.


Untitled

Heat seeking missiles

This month I have come up with a standalone project that should show you how to add heat seeking missiles to your projects.

Let's start with function main:

function
main()
{
on_d = null;
level_load (heatseek_wmb);
}

There's nothing special here. I have disabled the debug key ("D" for A5) because I will use WSAD to move the player.

action
player_moves
{
player = me;
camera.pan = player.pan;
my.invisible = on;
while (1)
{
vec_set (camera.pos,
my.pos);
camera.tilt +=
20 * mouse_force.y * time;
camera.pan -= 20
* mouse_force.x * time;
my.pan = camera.pan;
my.tilt = camera.tilt;
player_speed.x
= 15 * (key_w - key_s) * time;
player_speed.y
= 10 * (key_a - key_d) * time;
vec_set (temp,
my.x);
temp.z -= 1000;
trace_mode = ignore_me
+ use_box;
player_speed.z
= -trace (my.x, temp);
move_mode = ignore_you
+ ignore_passable;
ent_move(player_speed,
nullvector);
if (mouse_left
== 1)
{
fire_rocket();
}
wait (1);
}
}

The player is made invisible because we are using a first person camera. We can rotate the camera using the mouse and we move using the "W", "S", "A" and "D" keys. We use trace to keep the player with its feet on the ground all the time. If we press the left mouse button (LMB) function fire_rocket is called:

function fire_rocket()
{
proc_kill(4); // only one instance of this function is running
while (mouse_left == 1) {wait
(1);}
ent_create (rocket_mdl, player.pos,
shoot_rocket);
snd_play (rocket_wav, 50, 0);
}

Only one instance of the function should run; we wait for the LMB to be released and then we create a rocket and we play a sound. The rocket runs its shoot_rocket function:

function
shoot_rocket()
{
wait (1);
my.enable_entity = on;
my.enable_block = on;
my.event = remove_rocket;
my.pan = camera.pan;
my.tilt = camera.tilt;
my.skill20 = 0;
my.skill1 = 10;
my.skill2 = 0;
my.skill3 = 0;
my.skill1 *= time;
my.skill10 = 0;

The rocket is sensitive to entities and level blocks; its event function is remove_rocket. The rocket has the same pan and tilt angles with the camera at its creation. Skills 1...3 are used as a single var that gives the speed of the rocket.

     while (my.skill20 < 500)
{
temp.x = 40; //
horizontal scanning angle
temp.y = 60; //
vertical scanning angle
temp.z = 500; //
scanning range
if ((my.skill10
== 0) && (vec_dist (my.x, player.x) > 100))
{
scan_entity (my.x, temp);
}
my.skill20 += 1
* time;
ent_move (my.skill1,
nullvector);
temp.x = my.x -
6 * cos(my.pan);
temp.y = my.y -
6 * sin(my.pan);
temp.z = my.z;
effect (particle_trail,
10, temp, normal);
wait (1);
}
remove_rocket();
}

As long as the rocket continues to exist (skill20 < 500), we set its scanning angles and range: 40 degrees horizontally, 60 degrees vertically and a range of 500 quants. If the distance between the player and the rocket is over 100 quants the rocket will start to scan the environment, looking for entities with their enable_scan flag set. The rocket continues to move all this time and we create a smoke trail effect 6 quants behind the origin of the rocket model.

function particle_trail()
{
temp.x = random(2) - 1;
temp.y = random(2) - 1;
temp.z = random(2) - 1;
vec_rotate (temp, my.pan);
vec_normalize (temp, 3);
vec_add (my.vel_x, temp);
my.alpha = 40 + random(30);
my.bmap = ptrail_map;
my.size = random(2) + 5;
my.flare = on;
my.bright = on;
// my.beam = on;
my.move = on;
my.lifespan = 20;
my.function = fade_trail;
}

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

That's a typical particle function, with random speeds in all the directions, variable alpha and size and a simple function that fades out the older particles. Try using my.beam = on if you own A5 pro or A6 pro - it will make the smoke trail more interesting.

Let's see what happens when the rocket hits something:

function remove_rocket()
{
wait (1);
my.event = null;
ent_playsound (my, destroyed_wav, 1000); // play the explosion sound
my.invisible = on; // hide the rocket, keep ent_playsound playing
sleep (1.5); // wait for 1.5 seconds
ent_remove(me); // now remove it
}

It looks like its event is set to null so the rocket can't react to other events and then it plays an explosion sound. We hide the rocket for 1.5 seconds because we want the explosion sound to continue to play and then we remove the rocket.

Time to take care of our enemies!

action
enemy
{
my.enable_scan = on;
my.event = enemy_event;
}

All these guys need to do is to make sure that they have their enable_scan flag set. As soon as the rocket scans one of them, function enemy_event will start to run:

function enemy_event()
{
if (event_type == event_scan)
{
you.skill10 = 1;
// stop scanning
while (you != null)
// as long as the rocket exists
{
vec_set (temp.x, my.x);
vec_sub (temp.x, you.x);
vec_to_angle (you.pan, temp.x);
wait (1);
}
}
}

You.skill10 = 1 will make the rocket to stop scanning in order to preserve those precious computer resources; take a look at function shoot_rocket if you don't believe me. As long as the rocket exists (you != null) the rocket will rotate towards this enemy. My enemies are static but even if they would be moving targets they wouldn't have any chance if the speed of the rocket would be bigger than their own movement speed.


Untitled

Intelligent cameras

Many of you have asked me about the best method to create some cameras that will act like the ones in Resident Evil. I came up with a simple solution that has the advantage that the cameras are constantly looking at the player so they can’t miss the hero if you place a reasonable number of cameras in your level.

This example was created for 50 cameras but you can have as many as you want. To test the code, include recam.wdl and call fixed_cameras() in main. Place some dummy models in your level (I have used fish.mdl) set their pan, tilt and roll angles in Wed and attach the recam action to them. Don’t forget to set skill1for these models to 1..50; you don’t have to set it in a particular order; just make sure that you don’t have two cameras with the same skill1 value set in Wed. The values set for skill1 are used by the wdl to select the camera with the best view at a certain moment. If you want, you can load and run my modified office.wmp to see an example.

I don’t want to modify the templates so I have to disable the current view and enable the new re_camera view. Let’s take a look at the recam action first:

action recam
{
my.invisible = on;
my.passable = on;
re_camera.tilt = my.tilt;
re_camera.roll = my.roll;
if (my.skill1 == 0)
{remove me; beep; beep; return;}
while (1)
{
camera_position[my.skill1]
= vec_dist(my.x, player.x);
if (winner
> camera_position[my.skill1] - 50)
{
re_camera.x = my.x;
re_camera.y = my.y;
re_camera.z = my.z;

vec_set(temp, player.x); // re_camera's pan
vec_sub(temp, my.x);
vec_to_angle(re_camera.pan, temp);
}
wait (1);
}
}

The dummy models are made invisible and passable; their orientation (tilt, roll) is used by re_camera. Pan is computed every frame to make the cameras look at the player without missing it for a second.

I’m using the array camera_position[50] to store the distance between every camera and the player; camera_position[1] holds the distance between the player and camera that has its skill1 set to 1 in Wed, camera_position[35] holds the distance between the player and the camera that has its skill1 set to 35 and so on. I’m using a simple vec_dist(my.x, player.x) to compute the distance between the player and the camera. I have declared a variable named winner – this is the smallest distance between the player and any camera. If we find a winner, we set re_camera to the new (fish) coordinates and we constantly rotate it towards the player using a simple vec_to_angle instruction.

function
fixed_cameras()
{
camera.visible = off;
re_camera.visible =
on;
while (1)
{
temp_counter
+= 1;
if (temp_counter
== 50)
{
temp_counter -= 49;
}
if (camera_position[temp_counter]
< winner && camera_position[temp_counter] > 0)
{
winner = camera_position[temp_counter];
found_camera = temp_counter;
}
else
{
winner = camera_position[found_camera];
}
wait (1);
}
}

Function fixed_cameras includes a while loop that constantly looks for winners. If the distance between the camera and the player is smaller than winner, this camera will be activated. We have to make sure that camera_position[temp_counter] is bigger than zero because our level could use less than 50 cameras – all the unused values in the array are zero but this shouldn't make them winners.

I hope that you will have fun with this camera code.


Untitled

Intelligent elevators

I'm pretty sure that when you read the title of this article you said something like: "Yeah, right... how can you associate elevators with intelligence?" The elevator code in this article will start to work if the player is approaching a switch. The "intelligent" idea is that the switch and its corresponding elevator aren't connected at all! You can use many elevators and many switches and they will know how to work together without using pointers, handles, etc. You will use the same action for every button and for every platform and the code will do the rest! Now that I've got your attention, let's see how it is done.

The idea is (once again) simple: if you create a normal level, the distance between the elevators placed inside the level isn't that small. I am using a button that scans around it and if it finds an elevator entity nearby it starts to use it. Here's the code associated to the elevator button:

action
elevator_button
{
while (1)
{
while (vec_dist
(player.x, my.x) > 50) {wait (1);}
temp.pan = 360;
// scan on a sphere
temp.tilt = 360;
// because the elevator can be anywhere around this button
temp.z = 500; //
the elevator should be closer than 500 quants all the time
scan_entity (my.x,
temp);
sleep (0.4); //
scan 2 times a second (sleep (0.4 + 0.1))
my.ambient = 100;
sleep (0.1);
my.ambient = 0;
}
}

If the player approaches
the button, the button will start to scan around it on a 500 quants radius. The
button will flash because we are changing its ambient.

The action associated to the elevator entity is:

action elevator_entity
{
if (my.elevator_speed == 0) // if we forget to set skill1 in Wed
{
my.elevator_speed = 5; // set the default speed value
}
if (my.elevator_height == 0) // if we forget to set skill2 in Wed
{
beep; beep; exit; // shut down the engine
}
my.enable_scan = on;
my.event = move_elevator;
}

The elevator has a default speed value; elevator_speed = 5 if we forget to fill in skill1 in Wed. Skill2 is the destination point height (elevator_height) and should be filled every time. I wanted to make sure that you don't forget to do that so the engine will beep two times and then it will shut down if you forget about skill2. If the destination point really needs to be zero, set skill2 to 0.001 or so.

The elevator is sensitive to scanning; when this happens (the player comes close to a button placed close to the elevator), the event will be triggered:

function
move_elevator()
{
proc_kill(1);
my.enable_scan = off;

We kill all the other instance of this function then we make the elevator insensitive to other scans.

     if (my.z < you.z)
{
while (my.z <
(player.z - (player.max_z - player.min_z) / 2))
{
my.z += my.elevator_speed * time;
wait (1);
}
}

If the elevator is placed behind the button, it must go up until it reaches the height that corresponds to player's feet.

     if (my.z > you.z)
{
while (my.z >
(player.z - (player.max_z - player.min_z) / 2))
{
my.z -= my.elevator_speed * time;
wait (1);
}
}

Same thing here; the elevator goes down until its height is below player's feet.

my.z = player.z - (player.max_z - player.min_z) / 2;

We set the height of the elevator at player's feet (it was a little smaller or bigger).

while (vec_dist (player.x, my.x) > 50) {wait (1);}
my.enable_scan = on;
vec_set (player_strength, strength); // store player's speed on x, y, z
vec_set (strength, nullvector); // stop the player

We wait until the player has come close enough to the center of the platform and then we enable scanning again. We store player's speed (strength) and then we reset strength because we want to stop the player for now (we allow him to rotate).

     while (my.z < my.elevator_height)
{
my.z += my.elevator_speed
* time;
player.x = my.x;
player.y = my.y;
player.z = my.z
+ (player.max_z - player.min_z) / 2;
wait (1);
}

As long as elevator's z coordinate is below the value set in skill2, its z is increased and the player is kept in the center (and on top) of the platform by changing its x y z coords.

     while (my.z > my.elevator_height)
{
my.z -= my.elevator_speed
* time;
player.x = my.x;
player.y = my.y;
player.z = my.z
+ (player.max_z - player.min_z) / 2;
wait (1);
}

Same thing here, if the elevator needs to go down.

vec_set (strength, player_strength);
}

When the elevator his reached its destination, we allow the player to move again by restoring strength.

I have provided an example level with two elevators and two buttons. Of course that you can add as many elevators and buttons as you want, as long as they aren't too close to each other. Play with temp.z = 500 in elevator_button to set different scanning ranges; the distance between my elevators is over 500 quants.


Untitled

Inventory

This standalone project teaches you how to create an inventory. The player will be able to pick up a mace, a shield, body armor and the ring of fire. He can let them stay in the inventory or he can use them in order to improve its abilities. Let's take a look at the main function:

function
main()
{
level_load (inventory_wmb);
wait (2);
clip_size = 0;
on_d = null; // disable the
debug panel (key "D") because it is used for movement
fps_max = 40;
check_inventory();
}

I have disabled the "D" key because I'm using WSAD to move the player. Before I throw in the frightening check_inventory function, let's see some defines:

define items skill30;
define armour skill41;
define attack skill42;
define strength skill43;

define shield
1;
define mace 2;
define armor 4;
define ring 8;

When the player picks up an item, its corresponding value is added to player.items (just another name for skill30); if the player collects a shield and a ring, player.items will be 1 (shield) + 8 (ring) = 9, got it? Let's see the function:

function check_inventory()
{
while (1)
{
if (player.items
> 0)
{

If the player has got at least an item (player.items > 0)

if (player.items == 1) // 1 + 0 + 0 + 0
{
shield_pan.visible = on;
mace_pan.visible = off;
armor_pan.visible = off;
ring_pan.visible = off;
}

If player.items = 1, the player has got the shield; only shield_pan is made visible.

if (player.items == 2) // 0 + 2 + 0 + 0
{
shield_pan.visible = off;
mace_pan.visible = on;
armor_pan.visible = off;
ring_pan.visible = off;
}

If player.items = 2, the player has got the mace; only mace_pan is made visible.

if (player.items == 3) // 1 + 2 + 0 + 0
{
shield_pan.visible = on;
mace_pan.visible = on;
armor_pan.visible = off;
ring_pan.visible = off;
}

If player.items = 3, the player has got the shield and the mace; shield_pan and mace_pan are made visible.

...................................

The things repeat for player.items = 1...15

if (player.items == 15) // 1 + 2 + 4 + 8
{
shield_pan.visible = on;
mace_pan.visible = on;
armor_pan.visible = on;
ring_pan.visible = on;
}
}

If player.items = 15, the player has got all the items so all the panels are made visible.

else
{
shield_pan.visible = off;
mace_pan.visible = off;
armor_pan.visible = off;
ring_pan.visible = off;
}
mouse_map = cursor_pcx;
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;

If player.items = 0, all the panels are invisible; we set the mouse pointer to be cursor_pcx; the position of the mouse will be calculated every frame.

          if (mouse_right
== 1)
{
mouse_mode = (mouse_mode + 2) % 4;
while (mouse_right == 1) {wait (1);}
}
wait (1);
}
}

If we press the right mouse button, mouse mode will be set to 0 or 2, depending on how many times we press it. This shows / hides the mouse pointer, allowing us to click the items in the inventory in order to use them. The while loop waits until we release the right mouse button otherwise the pointer would appear / disappear every frame.

I use 5 panels: the main inventory panel and another 4 panels for the items.

panel main_pan
// the main panel for the items in the inventory
{
bmap = main_pcx;
pos_x = 700;
pos_y = 0;
layer = 10;
hbar = 7, 488, 85, blue_pcx,
1, player.armour;
hbar = 7, 518, 85, red_pcx, 1, player.attack;
hbar = 7, 550, 85, green_pcx, 1, player.strength;
flags = refresh, d3d, visible;
}

The main panel includes 3 hbars that show player's armor (I have used armour because armor is used), attack and strength.

panel
shield_pan // displays the shield on the panel
{
bmap = shield_pcx;
pos_x = 715;
pos_y = 30;
layer = 11;
flags = overlay, refresh,
d3d;
on_click use_shield;
}

panel
mace_pan // displays the mace on the panel
{
bmap = mace_pcx;
pos_x = 730;
pos_y = 145;
layer = 11;
flags = overlay, refresh,
d3d;
on_click use_mace;
}

panel
armor_pan // displays the armor on the panel
{
bmap = armor_pcx;
pos_x = 710;
pos_y = 255;
layer = 11;
flags = overlay, refresh,
d3d;
on_click use_armor;
}

panel
ring_pan // displays the ring on the panel
{
bmap = ring_pcx;
pos_x = 710;
pos_y = 365;
layer = 11;
flags = overlay, refresh,
d3d;
on_click use_ring;
}

The panels used for the items will appear over the main panel if they are picked up; before I forget, I have created the bitmaps for these panels by placing them inside a black room and taking screenshots. Every item panel has a simple function that will be executed if we click the panel:

function use_shield()
{
player.items -= shield; // remove the shield from the panel
ent_create(shield_mdl, player.pos, attach_item);
player.strength += 30; // bigger hbar
}

We substract shield (1) from player.items; function check_inventory will remove its associated panel from the inventory. We create the "real" shield and we give it to the player, and then we increase strength; the corresponding hbar on the main panel will grow bigger.

function
use_mace()
{
player.items -= mace; // remove the mace from the panel
ent_create(mace_mdl, player.pos, attach_item);
player.attack += 50; // bigger
hbar
}

We substract mace (2) from player.items; function check_inventory will take care of the rest. We create the mace and give it to the player; the attack hbar will grow.

function
use_armor()
{
player.items -= armor; // remove the armor from the panel
ent_create(armor_mdl, player.pos,
attach_item);
player.armour += 40; // bigger hbar
}

We substract armor (4) from player.items; function check_inventory removes the panel from the inventory. We create the armor and give it to the player; the armour hbar will grow.

function
attach_item()
{
proc_late();
my.passable = on;
my.metal = on;
my.albedo = 0;
while(player != null)
{
vec_set(my.x, player.x);
vec_set(my.pan,
player.pan);
my.frame = player.frame;
my.next_frame
= player.next_frame;
wait(1);
}
}

These 3 items are glued to the player because of this function:

function attach_item()
{
proc_late();
my.passable = on;
my.metal = on;
my.albedo = 0;
while(player != null)
{
vec_set(my.x, player.x);
vec_set(my.pan,
player.pan);
my.frame = player.frame;
my.next_frame =
player.next_frame;
wait(1);
}
}

The mace, the shield and the armor were animated together with the player and saved as separate models, so they use the same animation frames all the time. I have created the armor model by deleting all the triangles that weren't needed; I have scaled it up a little to make sure that it looks good on the player.

As long as the player exists, these items are placed at player's position, using its angles and frames all the time.

The ring is a special inventory item - let's see the code:

function
use_ring()
{
player.items -= ring;
while (1)
{
if (mouse_left
== 1 && mouse_mode == 0 && number_of_flames == 0) // the mouse
pointer is invisible and not too many entities are visible
{
temp_angle = 0;
while (temp_angle < 360)
{
temp_angle += 3;
temp.x = player.x + 100 * cos(temp_angle);
temp.y = player.y + 100 * sin(temp_angle);
temp.z = player.z;
ent_create (fire_pcx, temp, fire_action);
}
}
while (mouse_left
== 1) {wait (1);}
wait(1);
}
}

We substract ring (8) from player.items and then we enter inside a while (1) loop. If we press the left mouse button and the mouse pointer is invisible and the number of flames is zero, we create 120 flames around the player, 100 quants away from the player and then we wait until we release the left mouse button.

function fire_action()
{
number_of_flames += 1; // add another flame entity to this var
my.passable = on;
my.flare = on;
my.bright = on;
my.oriented = on;
my.pan = random(360);
while (my.pan < 1500)
{
my.pan += (5 +
random(3)) * time;
my.z -= 0.4 * time;
wait (1);
}
number_of_flames -= 1;
ent_remove (me);
}

We don't want to use autofire on the ring of fire so number_of_flames is increased with every flame that is created; this way we make sure that a single ring of fire can be generated and we keep a decent frame rate. The flames are passable and oriented by a random pan angle; they will rotate until their pan will be 1500 (it works :) and they will decrease their z at the same time. When all the flames have been removed, number_fo_flames is zero and function use_ring can create another ring of fire.

Time to take a look at the actions associated to the items that can be picked up:

action shield_init
{
my.passable = on;
while (player == null) {wait (1);}
while (vec_dist (my.x, player.x) > 50)
{
ent_cycle("idle",
my.skill20); // play stand frames animation
my.skill20 += 3
* time; // "idle" animation speed
my.skill20 %= 100;
wait (1);
}
player.items += shield;
ent_remove (me);
}

The shield is passable and plays its "idle" animation until the player comes closer than 50 quants to pick it up. When this happens, player.items is increased with shield (1) and the shield is removed. All the other actions are similar so I'll let you look at them:

action
mace_init
{
my.passable = on;
while (player == null) {wait (1);}
while (vec_dist (my.x, player.x) > 50)
{
ent_cycle("idle",
my.skill20); // play stand frames animation
my.skill20 += 3
* time; // "idle" animation speed
my.skill20 %= 100;
wait (1);
}
player.items += mace;
ent_remove (me);
}

action
armor_init
{
my.passable = on;
while (player == null) {wait (1);}
while (vec_dist (my.x, player.x) > 50)
{
ent_cycle("stand",
my.skill20); // play stand frames animation
my.skill20 += 2
* time; // "stand" animation speed
my.skill20 %= 100;
wait (1);
}
player.items += armor;
ent_remove (me);
}

action
ring_init
{
my.passable = on;
while (player == null) {wait (1);}
while (vec_dist (my.x, player.x) > 50)
{
ent_cycle("frame",
my.skill20); // play "frame" animation
my.skill20 += 2
* time; // "frame" animation speed
my.skill20 %= 100;
wait (1);
}
player.items += ring;
ent_remove (me);
}

Ready for the final action? We are talking about player_moves, the action associated to the player:

action
player_moves // attached to the player
{
player = me; // I'm the player
player.items = 0;
player.armour = 20;
player.attack = 30;
player.strength = 35;

The player starts the game without any item and some values for armour, attack and strength.

     while (1)
{
camera.x = player.x
- 200 * cos(player.pan);
camera.y = player.y
- 200 * sin(player.pan);
camera.z = player.z
+ 200;
camera.pan = player.pan;
camera.tilt = -30;

The camera is placed 200 quants behind the player and above the player, looking down at it.

my.pan += 4 * (key_a - key_d) * time - 20 * mouse_force.x * time;
player_speed.x = 10 * (key_w - key_s) * time;
player_speed.y = 0;
player_speed.z = 0;

The player can rotate using the keys "A" and "D" or the mouse; it can move forward / back with the keys "W" and "S"

          if ((key_w + key_s)
!= 0)
{
ent_cycle("walk", my.skill20);
my.skill20 += 4 * (key_w - key_s) * time;
my.skill20 %= 100;
}

If the player is walking, the model will play its "walk" animation; the animation will be reversed if the player moves backwards.

else
{
if (mouse_left != 1)
{
ent_cycle("stand", my.skill20); // play stand frames animation
my.skill20 += 2 * time; // "stand" animation speed
my.skill20 %= 100;
}

If the left mouse button (LMB) isn't pressed, the player is standing so the model will play its "stand" animation.

else // the player has pressed the left mouse button
{
if (ring_pan.visible != on && mouse_mode == 0 && number_of_flames
== 0) // the ring has been used
{
my.skill20 = 0;
ent_cycle("attack", my.skill20); // play stand frames animation
if (my.skill20 < 99)
{
my.skill20 += 0.5 * time; // "attack" animation played once (no loop)
}
}
while (mouse_left == 1) {wait (1);}
}
}

If LMB is pressed and the ring of fire was used and the mouse pointer is invisible and number_of_flames = 0, the model will play its "attack" animation frames once and then it will freeze at the last frame until we release the LMB.

move_mode = ignore_passable;
ent_move(player_speed, nullvector);
wait (1);
}
}

Finally, move_mode = ignore_passable allows the player to carry the items without being slowed down by them and the ent_move instruction produces the movement.

Ok, but I would like to kill my enemies using the ring of fire! How can I do that?
The easiest method would be (imo) to make the player scan 100 quants around its current position as long as number_of_flames != 0. Use enemies' events to decrease their health when event_type = event_scan. This way you don't need to perform many time - consuming ent_scan instructions for every flame.


Untitled

John Wayne

Ever wanted to be like John Wayne? I'm pretty sure that he has learned to use the gun that well by shooting at small sized, cheap plates. With this standalone project you'll be able to do that too, using two pistols with stereo sound!

Function main is always simple; please note that I have disabled the debug panel because I use WSAD for movement.

function
main()
{
on_d
= null;
level_load (johnwayne_wmb);
wait (2);
game_init();
}

But what's with this game_init function?

function game_init()

{
crosshair_pan.pos_x
= screen_size.x / 2 - 8;
crosshair_pan.pos_y
= screen_size.y / 2 - 8;
while (1)
{
if
(mouse_left == 1)
{
crosshair_pan.visible = on;
if (got_gun1 == 1 && got_gun2 == 0)
{
animate_right();
snd_play (shoot_snd, 70, -100);
ent_create (bullet_mdl, player.pos, move_bullet);
}
if (got_gun1 == 0 && got_gun2 == 1)
{
animate_left();
snd_play (shoot_snd, 70, 100);
ent_create (bullet_mdl, player.pos, move_bullet);
}
if (got_gun1 == 1 && got_gun2 == 1)
{
animate_right();
snd_play (shoot_snd, 70, -100); // right speaker
ent_create (bullet_mdl, player.pos, move_bullet);
waitt (3);
animate_left();
snd_play (shoot_snd, 70, 100); // left speaker
ent_create (bullet_mdl, player.pos, move_bullet);
}
while (mouse_left == 1) {wait (1);}
}
wait
(1);
}
}

First of all, game_init calculates the crosshair position; the bitmap for it has 16x16 pixels so we're substracting 8 pixels from the center of the screen. If the left mouse button (LMB) is clicked, the crosshair is made visible. I have associated 2 variables with the 2 pistols: got_gun1 = 1 means that I have got the right pistol and got_gun2 = 1 means that I have got the left pistol. It is pretty obvious what's happening when you fire one of the weapons; if you have picked up both pistols, the guns are shot using the new snd_play instruction which produces a nice stereophonic effect.

We don't want the player to be able to use autofire so we wait until the LMB is released.

This is a standalone project so I proudly present you world's shortest camera code:

action john_moves
{
player = me;
my.invisible = on;
while (1)
{
vec_set (camera.pos, my.pos);
camera.tilt += 20 * mouse_force.y * time;
my.pan += 4 * (key_a - key_d) * time - 20 * mouse_force.x * time;
camera.pan = my.pan;
player_dist.x = 10 * (key_w - key_s) * time;
player_dist.y = 0;
player_dist.z = 0;
ent_move(player_dist, nullvector);
wait (1);
}
}

John Wayne is played by (who else?) guard.mdl so I have decided to make it invisible. Use the keys WSAD to move the player and the mouse to look around. I know that this camera code hasn't got inertia so the movement is pretty crappy but I've told you that this is world's shortest camera code and more than that - who says that John Wayne had inertia at all?

Let's take a look at the code that is attached to the pistol models; I will explain what's happening with the pistol on the right because the code for the left pistol is the same:

action
pistol1
{
my.enable_impact
= me;
my.event = get_pistol1;
while (my != null)
// the gun wasn't picked up yet
{
my.pan
+= 3 * time;
wait
(1);
}
}

function get_pistol1()
{
wait
(1);
got_gun1 = 1;
snd_play (gotgun_snd, 80, 0);
ent_remove (me);
right_pistol.visible = on;
}

The pistol is sensitive to impact and will rotate as long as it hasn't been picked up. When we impact with the pistol, its function get_pistol1 sets got_gun1, plays a sound, removes the pistol from the ground and enables the right_pistol entity:

entity right_pistol
{
type
= <eagled.mdl>;
layer = 10;
view = camera;
x = 25;
y = -10;
z = -10;
}

This is a typical entity definition; left_pistol has y = 10 instead of y = -10 so it will appear on the other side of the screen. The pistols are animated using two simple while loops:

function animate_right()

{
while (right_pistol.tilt < 5)
{
right_pistol.tilt += 5 * time;
wait (1);
}
while (right_pistol.tilt > 0)
{
right_pistol.tilt -= 5 * time;
wait (1);
}
right_pistol.tilt = 0;
}

When we fire one of the guns, the bullet starts from the origin of the player and moves towards the target using the move_bullet function:

function
move_bullet()
{
wait
(1);
my.invisible = on;
my.enable_entity = on;
my.enable_block = on;
my.event = remove_me;
my.passable = on;
my.pan = camera.pan;
my.tilt = camera.tilt;
my.skill1 = 0;
bullet_speed.x = 200;
bullet_speed.y = 0;
bullet_speed.z = 0;
bullet_speed *= time;
while (my.skill1 < 50)
{
if
(my == null) {return;}
if
(my.skill1 < 0.1)  // don't collide with the player
{
my.passable = on;
}
else
{
my.passable = off;
}
my.skill1
+= 0.1 * time;
ent_move
(bullet_speed, nullvector);
wait
(1);
}
remove
me;
}

The bullet can collide with other entities or with the walls and it will be removed if it hits something. The bullet is passable at the moment of creation so it won't hit John's body, but after a few frames its passable flag will be reset. This baby travels with 200 quants / tick!

Finally, the code for that weird stove - the plate_generator:

action
plate_generator
{
my.passable
= on;
while (1)
{
if
(random(1) > 0.3)
{
ent_create (plate_mdl, my.pos, move_plate);
}
waitt
(32);
}
}

If random(1) > 0.3, a new plate is generated every two seconds. Let's take a look at the function that moves the plate:

function
move_plate()
{
wait
(1);
my.enable_impact = on;
my.enable_block = on;
my.event = destroy_plate;
my.skill12 = (1 - random(2))
/ 5;
my.skill13 = (1 - random(2))
/ 5;
my.skill14 = 2 + random(1);
plate_speed *= time;
while (me != null)
{
my.roll
+= 1 * time; // uncomment this line to see some weird stuff
ent_move
(my.skill12, nullvector);
my.skill14
-= 0.05 * time;
wait
(1);
}
}

You can see that the plate will be removed when it is hit by a bullet or collides with a wall. I'm using a special technique here so please pay attention: my.skill12..14 are used as if they were a single variable with its xyz components. All you need to do is make sure that you start with the right skill (0, 3, 6, 9, 12, etc) and that you refer to the same skill when you use it as if it were a standard variable with 3 components (the way I did with the ent_move instruction). This is extremely useful because the skills are here and they'll use memory even if you don't use them. More than that, if you have 100 plates running in the level, you won't like to have 100 separate variables and 100 slightly modified actions.

function destroy_plate()

{
wait (1);
if (event_type == event_impact) // hit
by a bullet
{
my.transparent
= on;
my.alpha
= 100;
while
(my.roll < 1440) // 4 rotations
{
my.roll += 10 * time;
my.alpha -= 0.7 * time;
wait (1);
}
}
ent_remove
(me); // remove the plate (without tilting) if it hit the ground (event_block)
}

If the plate hits a wall or the ground, it is simply removed. If the plate is hit by a bullet, its roll angle is increased while its transparency is decreased until the plate disappears.


Untitled

Lazer eyes

This article will teach you how to create particle rays and how to attach them to the eyes of your models.

I know I wouldn't want to meet this guy on a dark alley... I'm talking about the red little guy, of course :)

The model plays its "stand" animation and these two laser rays that come from its eyes move continuously as it rotates the head.

action
night_monster
{
while (1)
{
ray_size = 2;
ray_width = 10;
ent_cycle("stand",
my.skill1); // play "stand" frames animation
my.skill1 += 0.4
* time; // animation speed
my.skill1 %= 100;
// loop animation
vec_for_vertex
(left_eye, my, 235); // get the vertex coords for the left eye
vec_for_vertex
(right_eye, my, 247); // get the vertex coords for the right eye
effect (attach_rays,
1, left_eye, normal);
effect (attach_rays,
1, right_eye, normal);
while (ray_size
> 0)
{
vec_for_normal (temp_left, my, 235);
vec_for_normal (temp_right, my, 247);
vec_normalize (temp_left, ray_size);
vec_normalize (temp_right, ray_size);
vec_add (left_eye, temp_left);
vec_add (right_eye, temp_right);
effect (attach_rays, 1, left_eye, normal);
effect (attach_rays, 1, right_eye, normal);
ray_size -= 0.01;
}
wait (1);
}
}

The model plays its "stand" animation in a loop. We get the vertex coordinates for the left eye (vertex #235) in left_eye and we store the coordinates for the right eye (vertex #247) in right_eye. We create two flares using left_eye and right_eye as their starting points. Those of you that don't need more than a pair of eyes glowing in the dark should comment the following while loop.

action
night_monster
{
while (1)
{
ray_size = 2;
ray_width = 10;
ent_cycle("stand", my.skill1); // play
"stand" frames animation
my.skill1 += 0.4 * time; // animation speed
my.skill1 %= 100; //
loop animation
vec_for_vertex (left_eye,
my, 235); // get the vertex coords for the left eye
vec_for_vertex (right_eye,
my, 247); // get the vertex coords for the right eye
effect (attach_rays,
1, left_eye, normal);
effect (attach_rays, 1, right_eye, normal);
//  while (ray_size > 0)
//  {
//
vec_for_normal (temp_left, my, 235);
//
vec_for_normal (temp_right, my, 247);
//
vec_normalize (temp_left, ray_size);
//
vec_normalize (temp_right, ray_size);
//
vec_add (left_eye, temp_left);
//
vec_add (right_eye, temp_right);
//
effect (attach_rays, 1, left_eye, normal);
//
effect (attach_rays, 1, right_eye, normal);
//
ray_size -= 0.01;
//  }
wait (1);
}
}

And now, for the rest of us that want to use the full effect let's take another look at the while loop:

          while (ray_size
> 0)
{
vec_for_normal (temp_left, my, 235);
vec_for_normal (temp_right, my, 247);
vec_normalize (temp_left, ray_size);
vec_normalize (temp_right, ray_size);
vec_add (left_eye, temp_left);
vec_add (right_eye, temp_right);
effect (attach_rays, 1, left_eye, normal);
effect (attach_rays, 1, right_eye, normal);
ray_size -= 0.01;
}

We get the resulting normals for the faces that surround the left and the right eye, we set their length to ray_size and then we add them to the original left_eye and right_eye, generating particles for as long as ray_size is above 0. Let's see a picture that is explains what's happening:

I have used fewer particles in this example to make the things easier for you; the first particle is created when ray_size = 2, the second particle after the first subtraction inside the while loop (ray_size = 1.99) and so on. The process stops when ray_size = 0.

And now let's see the functions associated to the particles:

function attach_rays()
{
my.bmap = flare_pcx;
my.flare = on;
my.bright = on;
my.size = ray_width;
my.function = remove_flares;
ray_width -= 0.01;
}

function
remove_flares()
{
my.lifespan = 0; // remove the flare particle
}

The particles live for a single frame; the only thing that needs to be mentioned here is that the size of the particles is variable; the first particles (the ones that are away from the eyes) are the biggest ones while the later particles (placed close to the eye) get smaller and smaller. This effect is achieved by decreasing ray_width with 0.01 for every particle that runs the function. Ray_width is the variable that controls the size of the particles so that's all we need to do in order to achieve the effect below.

I encourage you to play with the initial values for ray_size, ray_width and to change the bitmap if you want to create a plethora of interesting effects. Don't forget to edit the numerical value inside the while loops (0.01 in my example).

These lazer rays can't kill anybody but if I were you I would combine them with my "vision cone" code in Aum28 (use a narrow cone for every eye). Set enable_scan = on for the player and when the event is triggered hurt the player. That's what I would do to create a good looking boss monster for my game!


Untitled

Mani(a)c Miner - the little man returns in full 3D

I remember myself playing those good old games on my Sinclair Spectrum computer; 32Kb of Ram were enough to write a great looking game. One of my favorites was Manic Miner - a game where you play the little man that has to collect some stuff in order to open the door to the next level. It's a classic, but I simply loved it.

By the end of this article, you'll learn how to create any type of platform / arcade 3D game, including Manic Miner. But first, let's give them a warm welcome:

guard.mdl aka 'Maniac Miner'
oldone.mdl aka 'Rock'
curtain.mdl aka 'Curtain'
cola.mdl aka 'Cola'

and finally

evilplnt.mdl also known as "The Evil Plant"! Is this scary or what?

Function main is so simple - there's nothing to explain here. Every object has its own action and we know that these actions start when we run the level without doing anything special. Before I forget: I'm using simplified actions / functions from time to time to make all this stuff easier for you - look at the wdl files for extra spices.

Action player_start is the corresponding action for guard.mdl:

action
player_start
{
start_coords.x = my.x;
start_coords.y = my.y;
start_coords.z = my.z;
my.enable_entity = on;
my.enable_push = on;
my.enable_impact = on;
my.event = maniac_event;
points = 0;
maniac_moves();
}

First of all,
we store maniac's (player's) coordinates because it will be returned to this position
if it dies. If the maniac collides with any entity, its maniac_event will be triggered.
Finally, we set score to 0 and we call the function that takes care of maniac's
movement:

function maniac_moves()
{
while (1)
{
maniac_gravity;
if (key_force.x
!= 0)
{
if (key_force.x > 0)
{
player.pan = 0;
}
else
{
player.pan = 180;
}
walk_speed.x = maniac_speed;
ent_cycle("walk", my.skill9);
my.skill9 += animation_speed * time;
if (my.skill9 > 100) {my.skill9 = 0;}
}
else
{
ent_cycle("stand", my.skill10);
my.skill10 += stand_speed * time;
if (my.skill10 > 100) {my.skill10 = 0;}
walk_speed.x = 0;
}
if (key_space == 1)
{
maniac_jump();
}
move_mode = ignore_you
+ ignore_passable;
ent_move
(walk_speed, nullvector);
wait (1);
}
}

There is a call to maniac_gravity and then some strange key_force.x stuff. When you press the left or right arrow keys on your keyboard, key_force.x changes from 0 to -1 or 1, depending on what key you have pressed. The instruction ent_move (walk_speed, nullvector); will move the maniac in the direction that it is facing so if we make the maniac change its pan angle, it will be enough to make it go in that direction. Because its move_mode = ignore_you + ignore_passable, the maniac can pass through passable entities.

ent_cycle("walk", my.skill9);
my.skill9 += animation_speed * time;
if (my.skill9 > 100) {my.skill9 = 0;}

I have explained how ent_cycle works in Aum2 - here's a short version: at game start, my.skill9 = 0, so ent_cycle("walk", my.skill9); sets guard's frame to the first walk frame (if guard.mdl has some animation frames named 'walk'); my.skill9 increases because of the 2nd line of code - therefore the walking frames are changing. If we reach the last walking frame (this means my.skill9 = 100), we are looping the walking animation using the last line of code.

Now let's get back to maniac_moves(). If we don't press one of the arrow keys, maniac will "stand" and its speed on x will be reset. If we press space, the maniac will jump:

function maniac_jump()
{
while (jump_time > -1)
{
if (key_space == 0) {return;}
walk_speed.z = jump_height * time * jump_time;
jump_time -= 0.04 * time;
wait (1);
}
while (key_space == 1) {wait (1);}
jump_time = 1;
}

When the maniac is on the ground, jump_time = 1. The idea is to give the maniac a positive walk_speed.z for a limited time, then make it negative - otherwise the maniac would jump up in the sky and we don't want that! When we press space, jump_time decreases until it is smaller than -1 (the red line in the picture above). When jump_time = -1 the jump is finished; we wait for key_space to be released then we allow the maniac to jump again.

If we press space and then we release it while we're still in the while(jump_time > -1) loop, the following instruction will be executed: if (key_space == 0) {return;}
This allows us to have longer or shorter jumps, depending on how much time "space" has been pressed because as long as jump_time > 0, the jump is getting higher. If we release space quickly and jump_time has only decreased to 0.8 the jump is shorter than the one that is performed for jump_time = 0.1

Function maniac_gravity() takes care that the maniac stands on the ground (if it isn't jumping):

function
maniac_gravity()
{
vec_set (temp, my.x);
temp.z -= 2000;
trace_mode = ignore_me
+ ignore_sprites + ignore_models + use_box;
my.skill11 = trace(my.x, temp);
if (my.skill11 >
2)
{
walk_speed.z
-= 2 * time;
}
else
{
walk_speed.z
= 0;
jump_time
= 1;
}
if (my.skill11 < 0)
{
walk_speed.z
+= 5 * time;
}
}

We're using trace to get maniac's height above the ground and we're storing it in skill11. While we're in the air (skill11 > 2) we use a negative speed on z to pull the maniac down. If skill11 <= 2, we assume that the maniac is on the ground, so we reset its speed on z and we allow him to jump again by setting jump_time to 1. It is possible to see the maniac getting stuck with its feet in the ground, so we pull it out a little by giving him a positive z speed boost if my.skill11 < 0.

If the maniac collides with something, its event is triggered - you already know that. Here's what happens:

function
maniac_event()
{
if (event_type == event_entity || event_type == event_push || event_type == event_impact)
{
wait (1);
if (you.skill9
== 1)
{
player.x = start_coords.x;
player.y = start_coords.y;
player.z = start_coords.z;
points = 0; // reset score
beep;
}
}
}

This function checks if the entity that collided with
the maniac has its skill9 set to 1 (this is correct for all the "bad" entities).
If this is correct, the maniac has to die, so it will be warped to its starting
position (remember we've stored it at game start?), the score will be reset and
so on.

The maniac has to be afraid by two entities: the rock and the evil plant. The rock has a simple action:

action rock
{
my.y = player.y;
my.skill9 = 1;
}

The first line makes sure that even if we place the rock (in wed) in a wrong position, it will match player's y coordinate at game start - this way we make sure that the collision can happen. The rock is "bad", so it has its skill9 set to 1. The evil plant has a bigger action:

action evil_plant
{
plant_speed.y = 0;
plant_speed.z = 0;
my.y = player.y;
my.skill9 = 1;
while (1)
{
move_mode = ignore_you + ignore_passable;
ent_move (plant_speed, nullvector);
my.skill20 += plant_speed.x;
if (my.skill20 == my.skill1 || my.skill20 == my.skill1 * -1)
{
plant_speed.x *= -1;
}
wait (1);
}
}

The plant moves only on x (from left to right and back), so we're setting its speed on y and z to 0; The plant moves all the time using ent_move in a while loop; we're storing the distance covered by the plant in skill20. On the other hand, we have set the maximum distance in wed (skill1). If the distance is smaller than -skill1 or bigger than skill1, the plant will change its direction. This action has a few more lines of code; one of them is

my.skill1 *= plant_speed;

Can you guess what's happening here?

Finally, the good stuff:

action
eat_dots
{
my.y
= player.y;
my.enable_impact = on;
my.event = dot_action;
}

You can eat cola dots - they react on impact, triggering the following function:

function dot_action()
{
if (event_type == event_impact)
{
my.invisible = on;
my.passable
= on;
play_sound
eatdot_snd,70;
points += 10;
}
}

I don't want to "remove" the dots because there are other actions that might need the my synonym. I prefer to make the dots invisible and passable - they won't exist for the player anymore.

If the maniac has managed the get to the top of the level, the curtain waits for the impact. If it happens, a nice zoom out effect is applied, allowing us to see the whole level.

There's something more to explain: the score panel. We start by defining a font and a string (to write "Score:" at the bottom of the screen):

font comic_font, <comic20.pcx>, 34, 38;
string score_string, "Score:";

We define a panel that is always visible:

panel
score_panel
{
pos_x
= 0;
pos_y = 0;
digits 650,550,4,comic_font,1,points;
flags
= refresh, visible;
}

digits 650,550,4,comic_font,1,points; = "display the variable points using comic_font, with four digits, multiplied by 1, at (650,550) pixels from the upper left corner".

We define a text that is always visible to show "Score:"

text
score_text
{
pos_x
= 470;
pos_y = 550;
font = comic_font;
string
= score_string;
flags = visible;
}

Before I forget, Manic Miner (PC version) is freely available for download at

Manic Miner's Homepage:
http://www.andyn.demon.co.uk

If you think that Manic Miner is too easy to play, wait until you meet the Kong Beast!


Untitled

Menu code

Many of you have requested some menu code so I have decided to create a simple, fully functional menu example. This menu has the following buttons: New Game, Sound Volume, Video Resolution, Quit. I know that you might need Save, Load, etc but all these functions are implemented in the template menu and you can add them (and their respective buttons) to my code.

I wanted to give you the chance to play with the menu in a standalone project so I have created a simple "game" - a level where you get to stare at some eye-check sprite. If you're confused, please think what must have been inside my head when I have decided to pick up an example like that! The good thing is that you can start the "game", adjust the sound level (and hear the difference), change the resolution (and see it being changed) and quit the game.

The game starts running at 800x600 pixels, in 16 bit mode.

var video_mode = 7; // 800x600
var video_depth = 16; // 16 bit mode

First of all, we need to define some bmaps; we are using two bitmaps for every button and a single bitmap for every panel. The names are pretty suggestive so I won't make any comments here.

bmap
main_pcx = <main.pcx>;
bmap new1_pcx = <new1.pcx>;
bmap new2_pcx = <new2.pcx>;
bmap sound1_pcx = <sound1.pcx>;
bmap sound2_pcx = <sound2.pcx>;
bmap video1_pcx = <video1.pcx>;
bmap video2_pcx = <video2.pcx>;
bmap quit1_pcx = <quit1.pcx>;
bmap quit2_pcx = <quit2.pcx>;
bmap maincursor_pcx = <maincurs.pcx>;
bmap sound_pcx = <sound.pcx>;
bmap video_pcx = <video.pcx>;
bmap slider_pcx = <slider.pcx>;

I'm using two levels: introlev.wmb is a dummy level, a simple hollowed block with some colored lights inside it and level1.wmb is the first (and only) game level.

string introlevel_wmb = <introlev.wmb>;
string level1_wmb = <level1.wmb>;

The panel definitions are similar; main_pan has four buttons on it; sound_pan and video_pan don't have buttons, but sliders.

panel
main_pan
{
bmap
= main_pcx;
layer = 20;
pos_x = 220;
pos_y
= 200;
button = 50, 65, new1_pcx, new1_pcx,
new2_pcx, null, start_game, mouse_over;
button
= 50, 115, sound1_pcx, sound1_pcx, sound2_pcx, null, set_volume, mouse_over;
button = 50, 165, video1_pcx, video1_pcx,
video2_pcx, null, set_resolution, mouse_over;
button = 50, 215, quit1_pcx, quit1_pcx, quit2_pcx,
null, quit_game, mouse_over;
flags
= d3d, overlay, refresh;
}

panel sound_pan
{
bmap
= sound_pcx;
layer = 20;
pos_x = 160;
pos_y
= 240;
hslider = 60, 95, 200, slider_pcx,
0, 100, sound_vol;
flags
= d3d, overlay, refresh;
}

panel video_pan
{
bmap
= video_pcx;
layer = 20;
pos_x = 160;
pos_y
= 240;
hslider = 60, 95, 200, slider_pcx,
4, 8, video_value;
flags
= d3d, overlay, refresh;
}

If you don't understand what I'm doing with the buttons and sliders, please read their definitions in the manual and compare what I did with the manual.

Let's take a look at:

function
main()
{
on_esc
= null;
level_load (introlevel_wmb);
wait (2);
game_init(); // checks the Esc key state and changes
the panels if needed
main_pan.visible = on; // show
the main panel
mainmenu_mouse(); // and give me
a cursor to play with
vec_set (camera.pos, nullvector);
// move camera to the origin
while
(game_started == 0) // we haven't pressed New yet
{
camera.pan += 0.3 * time; //
rotate the camera gently
camera.tilt += 0.1 * time;
camera.roll
+= 0.05 * time;
wait (1);
}
}

If you're starting a new project from scratch Esc will exit the engine. We want to use Esc to bring the main menu on, so the first line in main deactivates the built-in (exit) Esc function. We load the dummy level, we wait two frames to make sure that it has loaded and then we run the game_init() function (more on that a little later). Main_pan is made visible, mainmenu_mouse() give us a cursor to play with, the camera is moved in the origin and starts to rotate by changing its pan, tilt and roll angles.

function
mainmenu_mouse ()
{
mouse_mode
= 2;
mouse_map = maincursor_pcx;
while (game_started == 0)
{
mouse_pos.x = pointer.x;
mouse_pos.y
= pointer.y;
wait (1);
}
}

The first lines in mainmenu_mouse() show the cursor and change it to our maincursor_pcx. We can move the cursor as long as the game hasn't started using the predefined pointer.x and pointer.y.

Let's get back to:

function
game_init()
{
while
(1)
{
main_pan.pos_x = (screen_size.x
- bmap_width(main_pcx)) / 2;
main_pan.pos_y = (screen_size.y
- bmap_height(main_pcx)) / 2;
sound_pan.pos_x = (screen_size.x
- bmap_width(sound_pcx)) / 2;
sound_pan.pos_y = (screen_size.y
- bmap_height(sound_pcx)) / 2;
video_pan.pos_x = (screen_size.x
- bmap_width(video_pcx)) / 2;
video_pan.pos_y = (screen_size.y
- bmap_height(video_pcx)) / 2;

Every panel can be moved using its pos_x and pos_y parameters. We have provided some initial pos_x and pos_y values to our panels, but the panel needs to change its position to make sure that it is positioned in the center of the screen at any resolution. Take a look at the picture below to see how this works:

        if (key_esc == 1)
{
while
(key_esc == 1) {wait (1);}
if
(main_pan.visible == on)
{
main_pan.visible = off;
mouse_mode = 0;
}
else
{
if (sound_pan.visible == off && video_pan.visible == off)
{
main_pan.visible = on;
game_started = 0;
mainmenu_mouse();
}
}
if (sound_pan.visible
== on)
{
sound_pan.visible = off;
main_pan.visible = on;
}
if (video_pan.visible
== on)
{
video_pan.visible = off;
main_pan.visible = on;
if (int (video_value != video_mode))
{
switch_video (video_value, 16, 1);
}
}
}
wait (1);
}
}

If we press Escape, we have to wait for its release (while loop) because we don't want 50-100 menu changes a second (the changes triggered by Esc would be executed every frame). If main_pan is visible, we remove it and we hide the cursor. If main_pan isn't visible we check to see if sound_pan or video_pan are visible. If none of them is visible (there's no panel on screen) we make main_pan visible, we stop the game and we show the cursor. If sound_pan is visible, we hide it and show main_pan; if video_pan is visible, we do the same thing with it BUT if the resolution has changed we switch the video mode. Our video slider can deliver non-integer values, so we use int (video_value).

I'm using a small action: eye_checker to keep the sprite oriented and a small function mouse_over() to play a sound when the mouse is over one of the buttons:

action eye_checker
{
my.oriented = on;
}

function mouse_over()
{
play_sound (mouse_snd, 70);
}

The long awaited moment has finally arrived: we get to see the functions associated to the menu buttons:

function
start_game()
{
main_pan.visible
= off;
game_started = 1;
mouse_mode = 0;
level_load (level1_wmb);
wait (2);
camera.z = 220;
camera.tilt = -90;
}

Function start_game() will be executed every time we press the New button. This function hides the main_pan, sets game_started to 1, shows the cursor, loads the game level and then sets the camera.z position and tilt angle.

function
set_volume()
{
main_pan.visible
= off;
sound_pan.visible = on;
}

Function set_volume() hides main_pan and shows sound_pan. If you take a look at the hslider definition in sound_pan, you will see that by changing the slider position we modify sound_vol; this is a predefined variable that changes the volume for all the wav sounds in the game. Move the slider, press Esc and you will hear the difference when you move the mouse over the main_pan buttons.

function
set_resolution()
{
main_pan.visible
= off;
video_pan.visible = on;
}

Function set_resolution hides main_pan and shows video_pan. When you move the slider, video_value is changing and when you press Esc the resolution will be changed - see the code in game_init()

function
quit_game()
{
exit;
}

This function will exit the engine right away.


Untitled

Context menu

This article will teach you how to use the right mouse button for popup menus. The example changes the video resolution, but you can use its skeleton and turn it into a fully working context menu, with several options that appear under certain circumstances, and so on.

We need to define a panel that includes 3 buttons:

panel
context_pan
{
bmap = menu_pcx;
pos_x = 0;
pos_y = 0;
layer = 10;
button = 2, 2,
b1_640_pcx, b2_640_pcx, b1_640_pcx, change_resolution, null, null;
button = 2, 22, b1_800_pcx, b2_800_pcx, b1_800_pcx, change_resolution,
null, null;
button = 2, 42, b1_1024_pcx, b2_1024_pcx, b1_1024_pcx,
change_resolution, null, null;
flags = overlay, refresh;
}

Please note that the panel isn't visible at the moment; the buttons use the same change_resolution function. Function main loads the level, sets the bitmap that will be used for the mouse pointer and allows the mouse to move using a while loop.

function
main()
{
level_load (context_wmb);
mouse_map
= pointer_pcx;
while (1)
{
mouse_pos.x = pointer.x;
mouse_pos.y
= pointer.y;
wait (1);
}
}

Let's take a look at the function that runs when we click one of the 3 buttons on to the menu:

function
change_resolution(button_number)
{
if (button_number == 1)
// 640 x 480
{
video_switch
(6, 0, 0);
}
if (button_number == 2) // 800
x 600
{
video_switch (7, 0,
0);
}
if (button_number == 3) // 1024 x 768
{
video_switch (8, 0, 0);
}
mouse_mode = 0; // now hide the pointer
}

If
the player presses the first button, the engine will switch to 640 x 480 pixels,
keeping the same color depth. The same things happen when the player presses the
2nd or the 3rd button (the resolution will be different, of course). The last
line of code hides the mouse pointer as soon as one of the buttons is clicked.

The menu is made visible when we press the right mouse button:

function
show_menu()
{
mouse_mode = 2;
context_pan.pos_x
= pointer.x;
context_pan.pos_y = pointer.y;
context_pan.visible = on;
}

on_mouse_right = show_menu;

You can see that function show_menu is run as soon as the player presses the right mouse button; the function makes the mouse pointer visible, sets the correct pos_x and pos_y parameters for the panel and then makes the panel visible.

How do we get rid of the menu? Take a look at the function below:

function
hide_menu()
{
context_pan.visible = off;
wait (1);
mouse_mode = 0;
}

on_mouse_left = hide_menu;

This last function hides the panel, waits for a frame and then hides the mouse pointer too.


Untitled

Fake mirrors

The ability to create mirrors is a feature available only to the "professional" engine users; however, you can create decent looking mirror effects even if you are using A5 or A6 standard.

How do we create a mirror effect that can be used on the floor of a room? Here's a step by step explanation:

1) Add a large cube in Wed, set its size and texture it;

2) Hollow the cube and then delete its "floor":

3) Save the level and then create a separate wmb entity that will be used as the floor for your room; texture it using "checker1" from standard.wad:

4) Load the level again and then add the entity used for the floor:

5) Select all the level geometry excepting the floor entity and group it; use the "Back" or "Side" view for that:

6) Duplicate the room, rotate it and move it until it is flipped upside down:

7) Add more furniture, props, etc (normal and flipped). This is the image of the room used in my demo:

8) Add a player model and attach it the "player1" action. Attach action "transparent_floor" to the floor entity. That's it!

You've got it: the player can see through the transparent floor entity, and it sees the same things on both sides of the floor. But what happens with the player model? It changes its position, animations, etc all the time, so it must have some code attached to it!

action
player1
{
player = me;
while (1)
{
camera.x = player.x
- 100 * cos(player.pan);
camera.y = player.y
- 100 * sin(player.pan);
camera.z = player.z
+ 80;
camera.pan = player.pan;
camera.tilt +=
10 * mouse_force.y * time;
player.pan += 4
* (key_a - key_d) * time;
player_distance.x
= 10 * (key_w - key_s) * time;
player_distance.y
= 0;
player_distance.z
= 0;

The camera is placed 100 quants behind the player and 80 quants above its origin; it has the same pan angle with the player and changes its tilt angle depending on the movement of the mouse on the y axis. The player can change its pan angle using "A" and "D" and moves forward and backward with "W" and "S".

          if ((key_w == 1)
|| (key_s == 1)) // the player is walking
{
ent_cycle("walk", my.skill20); // play "walk" frames animation
my.skill20 += 4 * time; // "walk" animation speed
my.skill20 %= 100; // loop the animation
}
else // the player
is standing
{
ent_cycle("stand", my.skill20); // play "stand" frames animation
my.skill20 += 2 * time; // "stand" animation speed
my.skill20 %= 100; // loop the animation
}
move_mode = ignore_passable;
ent_move (player_distance,
nullvector);
wait (1);
}
}

If the player is walking ("W" or "S" are pressed) the player will display its "walk" animation in a loop; else, if the player is standing, it will play its "stand" animation in a loop. The player will be moved with the speed given by player_distance, passing through all the passable objects.

I can't see any mirror code here; let's check a new function:

starter init_mirrors()
{
while (player == null) {wait (1);}
ent_create (fakeplayer_mdl, nullvector, mirror_player);
}

This starter function waits until the player entity is loaded in the level and then it creates player's reflection model (fakeplayer_mdl) in the origin, and associates it the function named mirror_player():

function
mirror_player
{
my.passable = on;
while (1)
{
vec_set(temp, player.x);
temp.z -= 500;
// trace 500 quants below the player
trace_mode = ignore_me
+ ignore_passable + ignore_models + ignore_sprites + scan_texture;
trace (player.x,
temp); // get tex_name
my.frame = player.frame;
my.next_frame =
player.next_frame;
if (str_cmpi ("checker1",
tex_name))
{
my.invisible = off; // show the fake player image
my.pan = player.pan;
my.roll = player.roll + 180; // flip it upside down
my.x = player.x;
my.y = player.y;
my.z = player.z - 60;
}
else
{
my.invisible = on;
}
wait (1);
}
}

The model used as player's reflection in the mirror is passable; we trace 500 quants below the "real" player in order to get the name of the texture used for the floor and then we set the same frame and next_frame for the player and its reflection. If "trace" sees the texture named "checker1" below player's feet, it displays the reflection model, using the same pan angle and a different roll angle (180 degrees will flip the reflection model upside down, which is good for us). The reflection will have the same x and y with the player and it will be placed 60 quants below player's origin (play with this value).

If "trace" can't find the texture named "checker1" below player's feet, the reflection model is hidden. This way you can create normal rooms, with floors that don't reflect anything, and rooms that have mirrored floors. I have created a room of each type in my demo.

The action used for the transparent floor is really simple; change "85" to alter the transparency factor for the mirror.

action
transparent_floor
{
my.transparent = on;
my.alpha = 85;
}

If you plan to have enemies in a room that includes a mirrored floor, you will need to create pointers and functions that look like mirror_player() for them.

You can use a similar method to create vertical mirrors; if you want me to write an article about that, post a message at Conitec's forum, under "User Contributions" inside the Aum31 thread. Happy mirroring!


Untitled

Model viewer

This tool allows you to see the models in their full splendor. Just copy the models inside the same folder with the viewer, type their name, choose some start / end frames, set the ambient and / or the animation speed and enjoy the view!

Let's see some string definitions first:

string modelname_str = " "; // 20 chars, stores the model name
string start_str = " "; // holds the start frame
string end_str = " "; // holds the end frame
string empty_str = ""; // an empty string

The main panel includes 2 sliders that control the animation speed and the ambient light:

panel main_pan
{
bmap = main_pcx;
pos_x = 824;
pos_y = 0;
layer = 10;

vslider = 35, 340, 200, slider_pcx, 1, 50, frame_speed;
vslider = 133, 340, 200, slider_pcx, 0, 100,
model_ambient;

     digits = 95, 155, 3, arial_font, 1, frame_speed;
digits = 85, 210, 4, arial_font,
1, player.frame;

on_click input_data;

flags = refresh, d3d, visible;
}

If we click this panel, function input_data() will run. We will use 3 texts to store and display the model name, the starting animation frame and the end animation frame:

text
modelname_txt
{
layer = 30;
pos_x = 857;
pos_y = 628;
font = arial_font;
string = modelname_str;
}

text
start_txt
{
layer = 30;
pos_x = 900;
pos_y = 45;
font = arial_font;
string = start_str;
}

text
end_txt
{
layer = 30;
pos_x = 900;
pos_y = 100;
font = arial_font;
string = end_str;
}

Let's see the code inside function main:

function
main()
{
level_load (mview_wmb);
wait (2);
camera.x = -150;
camera.y = 0;
camera.z = 30;
clip_size = 0; // show all
the triangles for all the models
on_d = null; // disable the debug panel (key "D")
fps_max = 40; // lock the frame rate to 40 fps
mouse_init();
while (1)
{
if ((mouse_pos.x
< 824) || (mouse_mode == 0)) // if the pointer isn't placed over the panel
{
camera.x += 10 * cos(camera.pan) * (mouse_right - mouse_left) * time;
camera.y += 10 * sin(camera.pan) * (mouse_right - mouse_left) * time;
camera.z += 30 * sin(camera.tilt) * (mouse_right - mouse_left) * time;
camera.pan -= 5 * mouse_force.x * time;
camera.tilt += 3 * mouse_force.y * time;
}
wait (1);
}
}

We load the level (a simple hollowed cube), we wait for it to be loaded and then we set the initial camera position behind the model. We make sure that we will see all the triangles for all the models, we disable the debug panel and then we lock the frame rate to 40 fps.

If the mouse pointer isn't placed over the main panel, it is used to move the camera, with help from the 2 mouse buttons. If you took a good look at function main you might have noticed that I "forgot" to talk about mouse_init(). In fact, I wanted to show you its code here:

function
mouse_init()
{
mouse_map = cursor_pcx;
mouse_mode = 2;
while (1)
{
if (key_space ==
1) {mouse_mode = 0;}
else {mouse_mode
= 2;}
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
wait (1);
}
}

This function sets the mouse pointer bitmap, makes it visible and moves it on the screen. If we press and hold the "space" key, the mouse pointer will disappear and we will be able to move inside the cube, in order to get a better view of our model.

Do you remember the main panel? Please take a look at its bitmap: main.pcx. If we click it this function will run:

function
input_data()
{
if ((mouse_pos.x > 855) && (mouse_pos.x < 985) && (mouse_pos.y
> 630) && (mouse_pos.y < 650))
{
modelname_txt.visible
= on;
str_cpy (modelname_str,
empty_str);
while (str_cmpi
(modelname_str, empty_str) == 1)
{
inkey modelname_str;
wait (1);
}
str_cat(modelname_str,
".mdl");
if (file_open_read(modelname_str)
== 0)
{
beep; beep;
}
else // the model
exists
{
if (player != null)
{
ent_remove (player);
}
ent_create(modelname_str, nullvector, model_driver);
}
}

If we have clicked somewhere near the "Model name" text box, modelname_txt is made visible and its string is cleared. The while loop makes sure that we have typed a name for the model; the name of the model is stored in modelname_str. You don't have to type guard.mdl because the code adds .mdl to your input string, so use "guard", "warlock", etc (without quotes) for your model names.

If the model doesn't exist (you have typed a wrong name or you have forgotten to copy it in model viewer's folder), the engine will beep twice as an error message. If the model can be found in the current folder and its name was typed correctly, we remove the previously loaded model (if any) and we create the new model in the origin, attaching it the model_driver() function - more about this function a little later.

if ((mouse_pos.x > 895) && (mouse_pos.x < 955) && (mouse_pos.y > 45) && (mouse_pos.y < 65))
{
start_txt.visible = on; // show the text
str_cpy (start_str, empty_str);
while (str_cmpi (start_str, empty_str) == 1) // make sure that the player has typed something
{
inkey start_str; // show the cursor -> store the input in start_str
wait (1);
}
start_frame = str_to_num(start_str);
}
if ((mouse_pos.x > 895) && (mouse_pos.x < 955) && (mouse_pos.y > 100) && (mouse_pos.y < 120))
{
end_txt.visible = on; // show the text
str_cpy (end_str, empty_str);
while (str_cmpi (end_str, empty_str) == 1) // make sure that the player has typed something
{
inkey end_str; // show the cursor -> store the input in start_str
wait (1);
}
end_frame = str_to_num(end_str);
}
}

If we click somewhere near the "Start frame" or "End frame" boxes, we show the corresponding text and our input will be stored in start_str and end_str. Let's get back to model_driver():

function
model_driver()
{
my.albedo = 0;
while (1)
{
camera.ambient
= model_ambient;
my.frame += 0.03
* frame_speed * time;
if (my.frame >
end_frame)
{
my.next_frame = start_frame;
}
else
{
my.next_frame = 0;
}
if (my.frame >=
end_frame + 1)
{
my.frame -= (end_frame - start_frame + 1);
}
wait (1);
}
}

This is the function associated to our model. I have set albedo to zero because I want the model to look as crisp as possible; camera.ambient gets model's ambient. The ambient and the animation speed are set using the 2 sliders on main_pan, remember?

The model loops between start_frame and end_frame set by us, using smooth frame interpolation. Here's an example that uses figures; it animates a model using start_frame = 8 and end_frame = 11.

if
(my.frame > 11)
{
my.next_frame = 8;
}
else
{
my.next_frame = 0;
}
if (my.frame >= 12)
{
my.frame -= 4;
}

I have included two models with this project: guard and warlock.


Untitled

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.


Untitled

Music

The good news about "regular" music is that many of its constituent parts are repeating several times during the song, wasting space and increasing the size of the game. More than that, you might have a few quality music loops and you want to create a great song with them. If you were looking for code to achieve at least one of these goals, stop looking: you have discovered music.wdl.

My example uses 4 wav loops (for a total of 1Mb) and it is used to create a song that would have needed about 3Mb. Imagine what you can create with 8 loops (2Mb) and how much space you can save! The idea is (once again) simple: we create a small file in any text editor and we write some numbers inside it; here is the content of my music.dat file (you can use any other name):

1 1 4 4 3 3 1 2 2 3 3

I have told you that I'm using only 4 loops; the data inside the file tells the engine to play "1" twice, "4" twice, "3" twice, "1" once, "2" twice, "3" twice. As soon as one of the loops has stopped, the one that follows it in music.dat will start to play. If the numbers repeat a lot you will save a lot of space, get it?

Let's see the wonderful piece of code that changes the Acknex engine into a sequencer:

var number_of_pieces = 11; // will play 11 music pieces (defined in music.dat)
var file_handle;
var music_handle = 0;
var piece_number;

function start_music()
{
file_handle = file_open_read("music.dat"); // open the file
while (number_of_pieces >
0)
{
piece_number =
file_var_read (file_handle);
if (piece_number
== 1)
{
music_handle = snd_play (luke1_snd, 40, 0);
}
if (piece_number
== 2)
{
music_handle = snd_play (luke2_snd, 40, 0);
}
if (piece_number
== 3)
{
music_handle = snd_play (luke3_snd, 40, 0);
}
if (piece_number
== 4)
{
music_handle = snd_play (luke4_snd, 40, 0);
}
number_of_pieces
-= 1;
while (snd_playing
(music_handle) != 0) {wait (1);}
wait (1);
}
file_close (file_handle);
}

Is this simple or what? First we set the number of musical pieces to 11 (it is the number of figures in music.dat), then we open the file and we read the first number. If piece_number = 1 we will play the 1st loop, it piece_number = 2 we play the second loop and so on. This line:

while (snd_playing (music_handle) != 0) {wait (1);}

makes sure that a single loop is playing at a time. The loops will be played in the order indicated by the figures in music.dat and when number_of_pieces = 0 the song will stop and the file will be closed.

But what should I do if I want to use let's say... 8 loops?
Define the wav files, add if (piece_number == 5..8) etc inside the start_music function and use numbers from 1 to 8 inside the music.dat file. If you want, you can generate random piece_number values and torture your neighbors with the music that is created this way.


Untitled

New car

I don't remember how many of you have requested this piece of code at the forum but it worked - I have decided to create the wdl for it. What I'm trying to do here is: move the player in 1st person mode -> touch a car model -> get in the car and drive it in 1st person -> press space to get out of the car and continue to walk. If I approach the car again, I'll start driving it again and so on.

This might look like a difficult task, but it isn't; all I have to do is to switch the functions player_to_car and car_to_player. We place a car model in the level and we attach it the following action:

action my_car
{
my.enable_impact = on;
my.event = player_to_car;
}

You can see that the car will react if I touch it; let's see what happens:

function
player_to_car()
{
wait (1);
ent_remove
(me);
player.shadow = off;
player._MOVEMODE
= _MODE_DRIVING;
player._FORCE = 1.5;
player._BANKING = 0.1;
player.__SLOPES = on;
player.__WHEELS = on; // rotate only when moving
player.__JUMP
= off; // the car can't jump
player.__STRAFE = off; // can't
strafe
player.__TRIGGER = on;
astrength.pan
= 1.5; // decrease rotation speed (pan)
car_entity.visible
= on;
while (key_space == 0) {wait (1);} // press space
to switch from car to player
temp.x = player.x - 100 *
sin(player.pan); // check to see if there is an empty space (near the car door)
to deploy the player
temp.y = player.y + 100 * cos(player.pan)
;
temp.z = player.z;
ent_create (car_mdl,
player.pos, create_car);
vec_set (player.pos, temp);
car_entity.visible = off;
car_to_player();
}

entity car_entity
{
type = <rallycar.mdl>;
layer = 10;
view = camera;
x = 20;
y = 0;
z = -60;
}

The car is removed when the player impacts with it but the player is transformed into a car (you'll see most of these parameters in action player_drive). I have defined a car_entity that looks exactly like the car that has disappeared and I make it visible; now the player can drive as long as he wants to. If we press space, the player is moved near the door and the car model is created again:

function
create_car()
{
wait (1);
vec_set(temp,
my.x);
my.pan = player.pan;
temp.z
-= 2000; // trace 2000 quants below the car
trace_mode =
ignore_me + ignore_passable + ignore_models + ignore_sprites;
my.z -= trace (my.x, temp); // place the car at ground level
my_car(); // start the action again
}

I'm using "trace" to make sure that the car is placed at the ground level; action my_car is called again so the things start over.

A minor detail: we teach the player to behave like a human, not like a car; this is done in:

function
car_to_player()
{
player._MOVEMODE = _MODE_WALKING;
player._FORCE = 0.75;
player._BANKING = -0.1;
player.__SLOPES = off;
player.__WHEELS = off;
player.__JUMP = on;
player.__DUCK = on;
player.__STRAFE = on;
player.__BOB = on;
player.__TRIGGER = on;
astrength.pan = 7; // restore the
original value in move.wdl
}

This snippet does the job but of course that it can be improved. How about checking the content of the space near the door to make sure that the player can get out and won't get stuck in a wall?


Untitled

Pacmen

I remember myself playing Pacman on a Sinclair Spectrum computer. These were the days! If you were a programmer, you had to make sure that your game has less than 48kb - or it wouldn't fit in the memory! Pacmen uses a "little" more memory - it is a Pacmen (several men :) not Pacman.

Function main has nothing special in it; let's take a look at the action that drives our hero:

action
pacman
{
player = me;
my.enable_entity = on;
my.enable_push = on;
my.enable_impact = on;
my.event = kill_me; // here's
what happens on collision with enemies
camera.diameter = 0; // camera can pass through walls
camera.tilt = -90; // looks down
camera.z = my.z + 1000; //
1000 quants above the player
pacman_speed.y = 0;
pacman_speed.z = 0;

Pacman is sensitive to other entities; if it collides / is run over by enemies, its kill_me event is triggered. The camera can pass through walls, looks down, moves with the player and is placed 1000 quants above the player.

Pacman and its enemy are using invisible "antennas" (markers) to detect their environment and act like smart creatures. I have used this technique in my first AI demo in AUM. In this picture you can see the pink antennas (run the game with pacmen -d markers if you want to see them too):

We are using four markers for pacman and four markers for the enemy, like below:

Let's see what's with these antennas; I'll explain what's happening with the left marker (the code for the other markers is the same):

while (player != null)
{
motion_type = 0;

As long as the player is alive (player != null) we reset motion_type (more on that later). We are using rotated coords for all the markers because the player and enemy are rotated models, too.

          player_left.x =
my.x;
player_left.y =
my.y + width;
player_left.z =
my.z;
result = content(player_left);
if (result == content_solid)
{motion_type += 1;}
...............................................................................................

We check the content of the player_left marker and if it is in solid, we add 1 to motion_type. If player_right == content_solid -> motion_type += 2, player_down == content_solid -> motion_type += 4,
player_up == content_solid -> motion_type += 8. I chose these binary values (1, 2, 4, 8) to make sure that they represent unique combinations of numbers. If the markers for right and up are in solid, motion_type will be 2 + 8 = 10 and this is the only combination who's result will be equal to 10. Look at the table below to see all the combinations:

It is clear that motion_type = 15 can never happen, but I wanted to show you all the possibilities.

So we have four markers, we check their content and depending on the result, we allow them (pacman and enemy) to move only if they choose a direction where the road is "clear".

   if
(motion_type == 0)
{
if (key_cuu == 1) {my.pan = 0;}
if (key_cud ==
1) {my.pan = 180;}
if (key_cur ==
1) {my.pan = 270;}
if (key_cul ==
1) {my.pan = 90;}
}

if (motion_type == 1)
{
if (key_cuu == 1) {my.pan = 0;}
if (key_cud ==
1) {my.pan = 180;}
if (key_cur ==
1) {my.pan = 270;}
}

if (motion_type == 2)
{
if (key_cuu == 1) {my.pan = 0;}
if (key_cud ==
1) {my.pan = 180;}
if (key_cul ==
1) {my.pan = 90;}
}

if (motion_type == 3)
{
if (key_cuu == 1) {my.pan = 0;}
if (key_cud ==
1) {my.pan = 180;}
}

if (motion_type == 4)
{
if (key_cuu == 1) {my.pan = 0;}
if (key_cur ==
1) {my.pan = 270;}
if (key_cul ==
1) {my.pan = 90;}
}

if (motion_type == 5)
{
if (key_cuu == 1) {my.pan = 0;}
if (key_cur ==
1) {my.pan = 270;}
}

if (motion_type == 6)
{
if (key_cuu == 1) {my.pan = 0;}
if (key_cul ==
1) {my.pan = 90;}
}

if (motion_type == 7)
{
if (key_cuu == 1) {my.pan = 0;}
}

if (motion_type == 8)
{
if (key_cud == 1) {my.pan = 180;}
if (key_cur ==
1) {my.pan = 270;}
if (key_cul ==
1) {my.pan = 90;}
}

if (motion_type == 9)
{
if (key_cud == 1) {my.pan = 180;}
if (key_cur ==
1) {my.pan = 270;}
}

if (motion_type == 10)
{
if (key_cud == 1) {my.pan = 180;}
if (key_cul ==
1) {my.pan = 90;}
}

if (motion_type == 11)
{
if (key_cud == 1) {my.pan = 180;}
}

if (motion_type == 12)
{
if (key_cur == 1) {my.pan = 270;}
if (key_cul ==
1) {my.pan = 90;}
}

if (motion_type == 13)
{
if (key_cur == 1) {my.pan = 270;}
}

if (motion_type == 14)
{
if (key_cul == 1) {my.pan = 90;}
}

Now if the following line won't give you a headache then nothing else will:

if ((my.pan == 0 && motion_type < 8) || ((my.pan == 90) && (motion_type % 2 != 1)) ||
(my.pan == 180 && motion_type != 4 && motion_type != 5 && motion_type != 6 && motion_type != 7 &&
motion_type != 12 && motion_type != 13 && motion_type != 14) || (my.pan == 270 && motion_type != 2 &&
motion_type != 3 && motion_type != 6 && motion_type != 7 && motion_type != 10 && motion_type != 11 &&
motion_type != 14))
{
ent_move (pacman_speed, nullvector);
}

Well, this line is pretty simple: it stops the movement when the markers are in solid because we don't want Pacman to stick its nose into the wall, isn't it? If Pacman is pointing its "nose" in the right direction (the road is clear) ent_move is executed so Pacman moves. I have used several barbarian techniques in this instruction: motion_type % 2 != 1 replaces motion_type != 1 && motion_type != 3 .... && motion_type != 13 (all the odd numbers between 1 and 13).

The action attached to the enemy is similar; the only big difference is that we generate a random number and depending on its value we set the direction for the enemy.

If you look at the first example, the enemy is coming from right to the left; the available ("free") directions are up and right. The enemy came from the right so it wouldn't look that clever if it would bounce off the wall and return where it came from so this possibility has been disabled in enemy's code. In this case, the enemy will go up. In the second example, the enemy is coming going up and it won't return down, but move to the left because I set this limit in its code for this situation.

The enemy can pass through dots so I am using this line in its action (the dots are passable):

move_mode = ignore_passents;

Here's the action attached to every dot:

action
dot
{
my.push = -1;
my.z = player.z;
my.passable = on;
while (my != null &&
player != null)
{
if (vec_dist (my.x, player.x)
< 30)
{
play_sound (eatdot_snd, 100);
ent_remove (my);
score += 10;
}
wait (1);
}
}

The dots are aligned to player's z; this way I don't need to make sure that I place them at the proper height in Wed. If the player comes closer than 30 quants, the dot will be eaten and the score will be increased by 10.

The function that kills the player is simple; player's scale is lowered 10 times and then the player is removed.

function
kill_me()
{
exclusive_global;
play_sound gotme_snd, 100;
while (my.scale_x > 0.1)
{
my.scale_x -= 0.1 * time;
my.scale_y
= my.scale_x;
my.pan += 1 / my.scale_x;
wait
(1);
}
waitt (2);
ent_remove (me);
}

If you want to move to the next level, check the score in a loop; score = 600 can take you to the next level if you have 60 dots in the level.

What about "real" AI? This enemy of yours does nothing but moves correctly in the level! This part is easy and is left as an exercise for you. Pacmen has four different enemies - I've coded the dumb one. Don't worry - I'll explain how to code the most Pac-thirsty enemy right here:

The enemy must make sure that its x and y coordinates are equal with player's x, y coords (this means that the enemy has "got" the player). When the enemy reaches the 1st red point, it can't come closer to the player - it must move to the right, towards the 2nd point. When it arrives there its motion type has changed; the enemy knows that by moving upwards the distance between it and the player is getting smaller (on y) so it will move towards the 3rd point (we aren't using any random value here). By choosing to move towards the 4th point (again, not a "random" decision) the enemy knows that the distance between it and the player (on x) will get smaller. The 5th point can be a random decision or not, depending on player's position.

And you thought that creating a Pacman clone is easy!


Untitled

Pong

It all started with Pong! Ok, there were some text based adventures before Pong, but this one the first (as I like to call it) "real game". I wanted to recreate it because I'm a nostalgic guy (sniff) but also to show you how to use 2D graphics, collision detection in 2D and many more things!

Let's start with some panel definitions and with the picture that shows them all in action:

panel
main_pan // the main panel
{
bmap = main_pcx;
layer = 10;
pos_x = 0;
pos_y = 0;
digits = 330, 40, 2, digital_font,
1, player_score;
digits = 395, 40, 2, digital_font, 1, computer_score;
flags = refresh, d3d, visible;
}

panel
player_pan
{
bmap = player_pcx;
layer = 11;
pos_x = 60;
pos_y = 250;
flags = overlay, refresh,
d3d, visible;
}

panel
computer_pan
{
bmap = computer_pcx;
layer = 11;
pos_x = 715;
pos_y = 250;
flags = overlay, refresh,
d3d, visible;
}

panel
ball_pan
{
bmap = ball_pcx;
layer = 11;
pos_x = 504;
pos_y = 344;
flags = overlay, refresh,
d3d, visible;
}

This time I have used a big main function; the good news is that function main is the only function in this game and we will read it line by line:

function main()
{
fps_max = 80; // limit the frame rate to keep the game playable on faster computers
level_load (dummy_wmb);
wait (2);
speed_x = 3 - 6 * (sys_seconds
% 2);
speed_y = 3 - random(6);

We limit the frame rate to 80 fps because the game should be played at the same speed on a P5 at 5GHz. We load a dummy level, we wait for the level to be loaded and then we generate 2 random values:
1) The direction of the ball at game start (towards the player or the computer) = -3 or 3, depending on sys_seconds (odd or even).
2) The vertical speed of the ball = -3...3.

     while ((computer_score != 15) && (player_score
!= 15))
{
if (key_cuu ==
1) {player_pan.pos_y -= 8;}
if (key_cud ==
1) {player_pan.pos_y += 8;}
player_pan.pos_y
= min(470, player_pan.pos_y);
player_pan.pos_y
= max(35, player_pan.pos_y);

The game continues to run as long as the score (for the player or for the computer) is below 15. If we press the cursor up (cuu) arrow key, player_pan is moved upwards because its y gets smaller and smaller (until it reaches 35). If we press the cursor down arrow key, player_pan is moved downwards (its y can increase up to 470).

ball_pan.pos_x += speed_x;
ball_pan.pos_y += speed_y;

The ball will change its position because of speed_x and speed_y. Here's a picture that shows the movement of the ball:

You know that the ball changes its x and y position at the same time, so its movement looks something like this:

          if (ball_pan.pos_y
> 555)
{
speed_y = -3 - random(3);
snd_play (beep1_wav, 50, 0);
}
if (ball_pan.pos_y
< 32)
{
speed_y = 3 + random(3);
snd_play (beep1_wav, 50, 0);
}

If the ball goes below 32 pixels or over 555 pixels on y, it bounces back by changing its speed and plays the beep1_wav sound.

          if (ball_pan.pos_x
> 740) // player scores!
{
snd_play (goal_wav, 70, 0);
speed_x = -3 - random(3);
player_score += 1;
ball_pan.pos_x = 740;
}

If ball_pan.pos_x > 740, the player has scored! We play the goal_wav sound, we change the speed of the ball to make bounce back to the player, we increase player's score and then we move the ball to 740 pixels on x. Now why is that?

Well, the ball could go to up to 741, 742, 743, ... pixels before being bounced back and this would register several goals for a single shoot. The solution is to move the ball outside the dangerous area and to change ball's speed from positive to negative. This way the code will register a single goal for a single shoot.

          if (ball_pan.pos_x
< 40) // computer scores!
{
snd_play (goal_wav, 70, 0);
speed_x = 3 + random(3);
computer_score += 1;
ball_pan.pos_x = 40;
}

If the computer scores we apply the same scenario.

          if ((ball_pan.pos_y
> player_pan.pos_y - 12) && (ball_pan.pos_y < player_pan.pos_y +
96) && (ball_pan.pos_x > 60) && (ball_pan.pos_x < 72))
{
// the player has blocked the ball
snd_play (beep2_wav, 70, 0);
speed_x = 3 + random(3);
speed_y = 3 - random(3);
}

If the player has blocked the ball we play the beep2_wav sound and we bounce the ball back.

Who wants a perfect computer opponent? I know I don't want one.

          if ((ball_pan.pos_y
> 100) && (ball_pan.pos_y < 490))
{
computer_pan.pos_y = ball_pan.pos_y - 42;
}

We want the computer to make mistakes so we limit its movement on the y axis, instead of letting him go from 35 to 470, like the player does. This way you have a fair chance to beat the computer. Please note that the computer uses an offset of 42 pixels on y to follow the ball; this way it will hit the center of the ball with the center of its paddle (if it can reach the ball, of course :).

          if ((ball_pan.pos_y
> computer_pan.pos_y - 12) && (ball_pan.pos_y < computer_pan.pos_y
+ 96) && (ball_pan.pos_x > 703) && (ball_pan.pos_x < 715))
{
// the computer has blocked the ball
snd_play (beep2_wav, 70, 0);
speed_x = -3 - random(3);
speed_y = 3 - random(3);
}

If the computer has blocked the ball we play a sound and then we bounce the ball back.

Ok, this isn't beginner's corner, but how does that collision detection system work? How can you tell if the ball has hit the paddle?
I'll explain how the system works for the computer; I'm using exactly the same method for the player so you shouldn't have any problem with it. The sad truth is that there isn't any "true" collision detection system in any 2D application, so it must be faked. Let's use this example:

Could you tell that the ball will hit the paddle? It looks like it will, isn't it? First of all, the height of the ball is similar to the height of the paddle. In fact, I'm pretty sure that the ball would hit the paddle if it would be placed in any of the positions from the picture below:

Ok, so we have to make sure that the bottom of the ball is placed below the top of the paddle (ball #1). Don't forget that any panel has the origin in its upper left corner, so the first statement will look this way:

if (ball_pan.pos_y > computer_pan.pos_y - 12)

The ball has 12 x 12 pixels, get it? How would we write the statement for ball #7? If the top of the ball is placed above the bottom of the paddle, isn't it?

if (ball_pan.pos_y < computer_pan.pos_y + 96)

That's the correct code because the paddle has 96 pixels on y.

This solves the collision detection on y; let's move to the x axis then.

How can we be sure that the ball will not pass through the paddle? Well, we must make sure that the right side of the ball has touched the left side of the paddle, in other words:

if (ball_pan.pos_x > 703)

because the computer paddle is placed at 715 pixels on x and the ball has 12 pixels. Then we must make sure that the ball has hit the paddle, and not the empty space behind it:

if (ball_pan.pos_x < 715).

If I wouldn't use this final statement it would appear as if the computer had a thick paddle, like in the picture below:

If you combine the four statements above you will get the collision detection line used in the game.


Untitled

Power - ups

Sometimes
you need to give your player a speed boost. Maybe you want to give him more health.
What about instant ammo packs for all the weapons?

action power_health
{
show_panels(); // display the panels
my.metal = on;
my.passable = on;
while (vec_dist(my.x,
player.x) > 50)
{
my.pan += 3 * time;
wait (1);
}
my.invisible
= on;
player.light = on;
player.lightrange = 300; // the player
will glow red
player.lightred = 100;
snd_play (gotitem_snd, 70, 0);
while (my.skill1 < 500) // ~ 30 seconds
{
if (player._armor < 100)
{
player._armor
+= 0.8 * time;
}
else
{
if (player._health <
200)
{
player._health
+= 0.5 * time;
}
}
my.skill1 += 1 * time;
wait (1);
}
ent_remove (me);
player.light
= off; // clean up the mess
player.lightrange
= 0;
player.lightred
= 0;
}

The first line will display the ammo / health / armor panels that come with the templates. The power_health object will rotates until the player comes close to it. If this happens, the object will become invisible and the player will generate a red light around him. As long as skill1 < 500 (for 30 seconds or so) the armor will go up to 100 points and the health will be increased until it reaches 200 points. When the time has expires (skill1 is bigger than 500) the invisible power-up will disappear; we have kept it "alive" so far because we have used its skill1. The player stops emitting light red and the things go back to normal.

Let's take a look at action power_ammo:

action
power_ammo
{
my.metal =
on;
my.light = on;
my.lightrange = 0;
my.lightgreen
= 200;
my.transparent
= on;
my.passable
= on;
while (vec_dist(my.x, player.x) >
50)
{
my.pan += 1 * time;
my.tilt += 1 * time;
my.roll += 1 * time;
wait (1);
}
ent_remove
(me);
snd_play (gotitem_snd, 70, 0);

// 50 bullets for every weapon - use different values here
ammo1 = 50;
ammo2 = 50;
ammo3 = 50;
ammo4 = 50;
ammo5 = 50;
ammo6 = 50;
ammo7 = 50;
}

This power - up rotates on all the axis (pan, tilt, roll). If the player comes close to this item, it will disappear and ammo1...ammo7 (the vars that hold the number of bullets in the templates) will be increased. You can give a different number of bullets to every gun or you can give ammo only to certain guns.

The last power - up:

action
power_speed
{
my.metal =
on;
my.light = on;
my.lightrange = 0;
my.lightblue
= 200;
my.transparent = on;
my.passable = on;
while (vec_dist(my.x, player.x) >
50)
{
my.pan += 3 * time;
my.tilt += 2 * time;
my.roll += 1 * time;
wait (1);
}
my.invisible
= on;
player.light = on;
player.lightrange = 300;
player.lightblue = 250;
while (my.skill1 < 300) // ~ 20 seconds
{
strength.x = 10; // strength.x = 5 in templates
strength.y = 8; //
strength.y = 4 in templates
my.skill1 += 1 * time;
wait (1);
}
ent_remove
(me);
snd_play (gotitem_snd, 70, 0);

// restore
defaults
strength.x = 5;
strength.y = 4;

player.light = off;
player.lightrange = 0;
player.lightblue = 0;
}

This power - up doubles the speed. The template player moves with the speed given by "strength" in movement.wdl. When the player picks up this power - up he will create a blue light around him and he will move with a huge speed for about 20 seconds. When the time has expired, the "normal" speed is restored and the light around the player will disappear.

I wanted to give you the chance to work with all these power - ups in my included test level, so I have added a gun and our special guest star:

action thebutcher
{
my.light =
on;
my.lightred = 100;
my.lightgreen = 100;
my.lightblue = 100;
my.frame = 31;
while (player == null) {wait (1);}
while (1)
{
if (vec_dist(my.x, player.x) < 60 &&
player._health > 0)
{
if (player._armor >
0)
{
player._armor
-= 3 * time;
}
else
{
player._armor
= 0;
player._health
-= 2 * time;
}
}
wait (1);
}
}

This stiff guy will hurt the player if he (or she) comes too close. Now you can test the power_health and power_ammo (fire all the bullets and then get the power_ammo). Get the power_speed power - up and try to run as far as possible... I'm chasing you with the power_power_gun :)


Untitled

Perfect AI

This month I have created the first article in a series that will deal with advanced AI. If you think that "Perfect AI" sounds too good to be true, here's what I plan to do: an enemy that knows the position of the player all the time and knows how to get to the player using the shortest route available.

It is obvious that every game has its particular AI needs so I want to keep the discussion at a beginner - intermediate level; this way everybody should be able to modify the existing code, adding new features to it. Don't forget the "Acknex User Magazine" topic at the forum; post your ideas there and some of them will come to life. Btw, all the articles in this magazine were requested by you at the forum.

Ok, so let's see what we can learn this month. I wrote a few articles about AI in my previous magazines, mostly using the "trial and error" method. The enemy tries to get to the player by moving towards it; when it hits an obstacle, it rotates to the left or to the right, moving a certain distance in the new direction, and then it tries to get to the player again.

If you think that this is primitive AI, well.. you are right! However, if does its job pretty well for small sized obstacles and it is extremely easy to code. Advanced AI needs to use nodes or waypoints (but I'm not going to use this term because you might confuse them with the points on the paths that can be created in Wed). A node is a point of interest placed in the level; take a look at all the colored squares in the first picture: these are the nodes created by me for this maze level.

Every node should have at least a node to talk to so feel free to add as many nodes as you need in your levels; on the other hand, don't use unnecessary nodes because you will slow down the game. Here's an example for a bad node placement:

You can see that "1" is isolated; it can't "see" any other node. The same thing goes for "3" and "8": they can "see" each other but none of them can see the rest of the nodes. Now here's the corrected version:

I have added the nodes numbered "9" and "10" and now the nodes can communicate with each other. Let's imagine the following situation:

The enemy wants to find the player, so it will do the following things:
- Asks this question: "which one of you, nodes, is the closest to the player and can see the player?"
- "It is me, pick me!" says node 2.
- "Oh, I see.." replies the enemy, "but how do I get there?"
- "Well", says node 2, "I don't know how to get you here, but I tell you that I can see node 10".
- "Good enough", replies the enemy. "Hey, node 10, what do you see?"
- "I can see node 5" replies node 10.
- "And you, node 5?" asks the enemy again.
- "I can see node 4 and node 7".
- "I can see node 7 too! Now I know how to get to the player!" says the enemy rubbing its dirty hands. "All I need to do is to move to node 7, then to 5, 10, 2 and I will find (and kill!) the player.

That's how advanced path finding works. There are a number of path finding algorithms that are well known: A*, Dijkstra, Breadth - first search, etc. I think I'll use a combination of the first two algorithms.

Now that you've got some info about AI let's see what have we got in the code used for this month's AI article:
- a network of nodes;
- only the nodes that are close to the player use precious computer resources;
- the nodes that are close to the player and can see the player are detected;
- the closest node to the player that can see the player is known, as well as its distance to the player.

I would say that this is a good start, so let's see the code that has made all these things possible. I will start by presenting you the arrays that are used in the demo:

var
node_distance[50];
var see_player[50];
var nodex[50];
var nodey[50];

Node_distance[50] will hold the distance from a specific node to the player; its elements are node_distance[0]... node_distance[49] but we aren't going to use the first element (we want to keep the things as simple as possible) so this array can be used by 49 nodes. My demo level uses 30 nodes but if you need more nodes you can increase the size of the arrays above.

See_player[50] is set to one if the node can see the player and to zero if the node can't see the player. Take another look at the picture above: node 2 can see the player so see_player[2] = 1 and node 10 can't see the player so see_player[10] = 0.

Nodex[50] and nodey[50] hold the coordinates of the nodes on x and y; we need these arrays because if we want our enemy to go to node 5 (for example) we will find the coordinates for node 5 in these arrays.

Time to see a text and a panel:

text
ai_txt // displays the texts on the screen
{
font = ai_font;
pos_x = 0;
pos_y = 0;
string = "Target node:\nD.to
target:";
flags = visible;
}

panel
ai_pan // displays the numerical values on the screen
{
pos_x = 0;
pos_y = 0;
layer = 10;
digits = 240, 0, 3, ai_font,
1, target_node;
digits = 240, 22, 3, ai_font, 1, closest_distance;
flags = overlay, refresh, visible;
}

I have ai_txt to display the two rows of text on the screen and ai_pan to display the two figures. The target node is the closest node to the player that can see the player and "D. to target" is the distance from the player to the target node. Let's take a look at function main:

function
main()
{
fps_max = 60;
clip_size = 0;
level_load(level1_wmb);
wait (3);
camera.z = 2400;
camera.tilt = -90;

We limit the frame rate to 60 fps, we make sure that we show all the triangles for all the models and then we load the level. We wait for the level to be loaded and then we make sure that we get a good view of the whole level by choosing a convenient camera height and angle.

     while (1)
{
index %= 49; //
limit the "index" value
index += 1; //
from 1 to 49

The while loop will determine the closest node that can see the player. We use a var named index as a counter; its value ranges from 1 to 49 because we can use up to 49 nodes if those arrays have 50 elements.

if ((node_distance[index] < closest_distance) && (see_player[index] == 1))

If the distance from a new node to the player is smaller than the old distance to the player and we are testing a node that can see the player (see_player[index] = 1) then we have a new winner! We have set a new target node for our enemy!

{
closest_distance = node_distance[index];
target_node = index;
}

If this isn't true, we will use the previous winner; the older node is still the closest to the player.

          else // haven't
found a new winner?
{
closest_distance = node_distance[target_node];
}
wait (1);
}
}

We will use a model that can "walk" and "stand" for the player; let's see the code for it:

action
player1
{
player = me; // I'm the player
while (1)
{
my.pan += 4 * (key_cul
- key_cur) * time; // rotates with the left / right arrow keys
player_speed.x
= 10 * (key_cuu - key_cud) * time; // moves forward / backward with the up / down
arrow keys
if (key_cuu + key_cud
> 0) // if the player is walking
{
ent_cycle("walk", my.skill46); // play "walk" frames animation
my.skill46 += 7 * time; // "walk" animation speed
my.skill46 %= 100; // loop animation
}
else // the player
is standing
{
ent_cycle("idle", my.skill46); // play "stand" frames animation
my.skill46 += 2 * time; // "stand" animation speed
my.skill46 %= 100; // loop animation
}
move_mode = ignore_passable
+ glide;
ent_move(player_speed,
nullvector);
wait (1);
}
}

The player rotates when you press the left or right arrow keys and moves forward / backward when you press the up / down arrow keys. Change "4" and "10" to change the rotation and movement speed. If the player is walking it plays its "walk" animation and if it is standing it plays its "idle" ("stand") animation. The player moves and glides along the walls if it hits them.

The last part of the code is the one associated to the nodes:

action node
// using 30 nodes in this demo
{
my.skill47 = 1234;
my.passable = on;
my.skin = 1; // red
while (player == null) {wait
(1);}
node_number += 1; // get a unique number
my.skill48 = node_number; // and store it in skill48
nodex[my.skill48] = my.x; // store x and y
nodey[my.skill48] = my.y;
// for this node

We set a weird value for skill47 to give the nodes a unique id, we make the node passable because they shouldn't block the player or its enemies and then we choose the first skin for the node (red). The node model has three skins: red, green and blue. Red is the default value, green is used by the nodes that are close to the player and blue is used by the nodes that can "see" the player.

We wait until the player is created and then we assign every node a unique number, which will be stored in skill48. Maybe you are wondering how this system works, so here's a detailed explanation: all the entity placed in our level load one by one. The first node entity that appears in the level will add one to node_number and then will store the value (one) in skill48. The second node will add one to the existing value (one) and then will store the result (two) in its skill48. The process repeats until all the node entities are loaded; when this happens, every node has stored its number inside its skill48.

The last two lines of code store the position of the node (x and y) in the level.

while (1)
{
node_distance[my.skill48] = vec_dist(player.x, my.x);

I have told you that node_distance stores the distance from the node to the player, isn't it? The line of code above does that. If the player is closer than 500 quants to the node:

          if (node_distance[my.skill48]
< 500)
{
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.pos, player.pos) == 0) // if this node can "see" the player
{
my.skin = 3; // the node changes its color to blue
see_player[my.skill48] = 1; // the node can see the player
}
else
{
see_player[my.skill48] = 0; // this node can't see the player
my.skin = 2; // can't see the player? keep the green color
}
}

We "trace" from the node to the player; if the result is zero, the node can see the player so we change the skin of the node to blue and we set see_player to 1 for this node. If the node can't see the player we use the green color for the node: the player is close but can't be seen by the node.

else // far from the player? keep the red color
{
my.skin = 1;
}
sleep (0.1); // trace 10 times a second
}
}

If the player is far away from the node, we keep the (default) red color for the node. All these operations happen 10 times a second because the last instruction inside the while loop is sleep(0.1).

Let's run the project: open level1.wmp, built it and run it using ai1.wdl:

You will see the following image:

The green squares are the nodes that are closer than 500 quants to the player. You can move the player around and see how the green nodes change. Now exit the engine and run the level again using -d traceplayer, like in the picture below:

You will see the following image:

Some of the green nodes have turned to blue; these are the nodes that are closer than 500 quants to the player and can "see" the player. It wouldn't be wise to have "trace" instructions for the nodes that are far away from the player, isn't it?

I encourage you to create one or more test levels and to learn how to place your network of nodes inside it / them. Here's a bad example for a correct network of nodes:

The enemy wants to get to the player, so it must use this path: 1, 5, 3, 2, player. If I would have added the missing node, the path from the enemy to the player would have been much shorter, isn't it? Don't forget that perfect AI starts with perfect path planning.

Next time we will learn to compute the shortest path between any two nodes in the level: the starting point (the enemy) and the target point (the player).


Untitled

Perfect AI - part II

Last month I have started to write the code for Perfect AI, the one that chooses the shortest path from a starting point (the enemy) to the destination (the player). This month you will see the algorithm in action: open level2.wmp, build it and run it:

The screen displays "Touched" (the last node that was touched by the mouse), "Start" (starting node) and "Target" (destination node). Now move the mouse over node 27 and click it; you will see that the nodes that describe the shortest path from this node to the player will change their color from white to red, like in the picture below:

You can move the player anywhere; the target node will be the closest node to the player that can see the player. You can click any node in the level and you will get the shortest path to the player every time. The numbers of the nodes that describe the shortest path appear on top of the screen. It is clear that the enemy will hunt down the player like a dog even if my example level looks like a complicated maze.

I heard many stories about how hard it is to write good AI code because the speed of the game will decrease a lot, etc. I am happy to tell you that Perfect AI runs really fast; the shortest path is computed in less than a frame every time. I have used a Floyd - Dijkstra - George :) algorithm because it is simple to implement and it works extremely fast.

I advise you to read the article in Aum27; I won't explain what I did there again.

function main()
{
mouse_range = 5000;
fps_max = 60;
clip_size = 0;
level_load(level2_wmb);
wait (3);
camera.z = 2400;
camera.tilt = -90;
init_mouse();
while (distances_computed
== 0) {wait (1);}
run_algorithm();

We are using the mouse to "touch" the nodes; move the mouse over a specific node and you will see its number on the screen. The first line of code makes sure that we can touch any node even if it is placed up to 5000 quants away. We limit the frame rate to 60 fps, we show all the triangles for all the models, we load the level and then we set a convenient camera position and tilt angle. The function named init_mouse is your typical mouse function; I will list its code in a moment. I chose to compute the distance between the nodes that can see each other directly (not the shortest distance nor the actual path!) at game start so the algorithm will not run until distance_computed is set to 1 by another action.

     while (1)
{
index %= max_nodes;
if ((node_to_player[index]
< closest_distance) && (see_player[index] == 1))
{
closest_distance = node_to_player[index];
target_node = index;
}
else
{
closest_distance = node_to_player[target_node];
}
index += 1;
wait (1);
}
}

The rest of the code check which one of the nodes (0...29) is the closest node to the player that can see the player. This will be the target node, the goal for our enemy. Read the article in Aum27 for a through explanation.

action player1
{
player = me;
while (1)
{
my.pan += 4 * (key_cul
- key_cur) * time;
my.skill1 = 15
* (key_cuu - key_cud) * time;
if (key_cuu + key_cud
> 0)
{
ent_cycle("walk", my.skill46);
my.skill46 += 10 * time;
my.skill46 %= 100;
}
else
{
ent_cycle("idle", my.skill46);
my.skill46 += 2 * time;
my.skill46 %= 100;
}
move_mode = ignore_passable
+ glide;
ent_move(my.skill1,
nullvector);
wait (1);
}
}

I haven't changed the action attached to the player model; ok, I have increased its speed and I got rid of the var named player_speed (I'm using some skills now) but the rest of the code is just like the one used in Aum27.

Let me show you the arrays used for this demo:

var
node_to_player[30];
var see_player[30];
var nodex[30];
var nodey[30];
var node_to_node[900];
var
visited[900];
var path[30];

Node_to_player will store the distance from every node to the player; see_player will be set to 1 if that node can "see" the player and to 0 if the node can't see the player. Nodex and nodey store the x and y coordinates for the 30 nodes; these values will be used by the enemy to navigate from one node to another. Node_to_node stores the distance between any two nodes and acts like a matrix of 30x30 elements. Visited stores the path from a node to any other node and path stores the real path from start to target. Don't be afraid, we will use many pictures along the way.

Prepare for action!

action node
{
my.enable_scan = on;
my.enable_touch = on;
my.enable_click = on;
my.event = trace_back;
my.skill47 = 1234;
my.passable = on;

Every node is sensitive to scan, touch and click; if one of these events take place, the node will run its associated trace_back function. Skill47 was set to a weird value (for future use) and the node was made passable.

number_of_nodes += 1;
my.skill48 = number_of_nodes;
nodex[my.skill48] = my.x;
nodey[my.skill48] = my.y;
while (number_of_nodes < (max_nodes - 1)) {wait (1);}

Every node must get an ID number (0...29); the nodes appear in the level in a random order so number_of_nodes = 0 for the first node, number_of_nodes = 1 for the second node, etc because number_of_nodes = -1 at game start. Every node stores its ID number in its own skill48; the coordinates (x and y) for the node are stored in nodex and nodey. We wait until all the nodes are placed in the level.

     while (current_node <= number_of_nodes)
{
if (current_node
== my.skill48)
{
temp.x = 360;
temp.y = 30;
temp.z = 1000;
scan_entity (my.x, temp);
current_node += 1;
}
wait (1);
}
distances_computed = 1;

The first node (0) will scan for the nodes nearby; if its scan finds some nodes it will trigger their events. The scanning takes place in all the directions horizontally (360 degrees) and up to 1000 quants. We wait for a frame and then the second node (1) will do the same thing, etc. This way we don't have 30 scan_entity instructions running at the same time so the frame rate will remain big even on weak PCs. As soon as all the nodes have scanned their neighbors we set distances_computed to 1, allowing the run_algorithm function in main to... run. We'll take about that function a little later.

     while (player == null) {wait (1);}
while (1)
{
node_to_player[my.skill48]
= vec_dist(player.x, my.x);
if (node_to_player[my.skill48]
< 500)
{
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.pos, player.pos) == 0)
{
see_player[my.skill48] = 1;
}
else
{
see_player[my.skill48] = 0;
}
}
sleep (0.1);
}
}

We wait until the player is created and then we store the distance from every node to the player in node_to_player. If the player has come closer than 500 quants to the node, the node will trace the player, setting see_player = 1 if the node can see the player and see_player = 0 if the node can't see the player. The nodes trace 10 times a second; that's the only part of the Perfect AI that uses precious computer resources. Please note that only the nodes that are close to the player try to trace it so if six nodes can see the player we are using (in theory) less than a trace every frame and this is very good for us!

That's the code used for the nodes and it doesn't look too complicated. Before I move on, let me show you how these nodes work together:

Node 1 scans on a radius of 1000 quants; it detects nodes 2, 3, 4, 5, 6, 7. All these nodes will trace back to node 1; you can see that 2 and 3 are able to see 1 (the node that scanned them) but the rest of the nodes can't see it. The process repeats for all the nodes.

Let's see the event function associated to the nodes:

function
trace_back()
{
if (event_type == event_scan)
{
my.skill45 = handle(you);
trace_mode = ignore_me
+ ignore_models + ignore_passents;
if (trace (my.x,
you.x) == 0)
{
you = ptr_for_handle(my.skill45);
index = you.skill48 + my.skill48 * max_nodes;
node_to_node[index] = vec_dist(my.x, you.x);
}
}
if (event_type == event_touch)
{
touched_node =
my.skill48;
}
if (event_type == event_click)
{
start_node = my.skill48;
}
}

If the node is scanned it will store the "you" entity in skill45. Who is "you"? It's the node that has scanned this node, triggering its event (node 1 in the picture above). Any trace instruction would destroy the "you" pointer so we store it in skill45. If the node can "see" the node that scanned it, we restore the "you" pointer and we store the distance between "my" (the node that was scanned) and "you" (the node that has scanned) in node_to_node. I use "index" as an index inside the array that creates the virtual matrix.

Node_to_node has 900 elements so you should see it as a matrix that has 30 lines and 30 columns.

Let's get back to the event function; if the node is touched by the mouse, touched_node (one of the vars that are displayed on the screen) is set to the value stored in skill40 for that node, which is the number of that node. This way we can move the mouse pointer over a node and we will see its number. Finally, if the node is clicked with the left mouse button, start_node, the first point on the shortest path, is set to the number of that node.

Here's an easy function, the one that deals with the mouse movement:

function init_mouse()
{
mouse_map = pointer_pcx;
mouse_mode = 2;
while (1)
{
if (mouse_left
== 1) {display_path();}
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
wait (1);
}
}

The only thing that must be mentioned here is that the same left click runs display_path, the function that displays the shortest path between start_node and target_node.

Ok, let's stop for a moment. We have some nodes scanning each other, tracing back... it looks like we have we processed a lot of data but how does it look like?
That's an excellent question! :)

That's how node_to_node looks for the first 10 nodes; node 0 and 1 can see each other and the distance between them is 384 quants, node 2 and 4 can't see each other (the distance between them is zero), etc. Please be aware that the nodes will get different numbers when you run the demo level so take this table as a simple example.

Time to face the most horrible function you've ever seen:

function
run_algorithm()
{
i = 0;
j = 0;
while (i < max_nodes)
{
while (j < max_nodes)
{
index = j + i * max_nodes;
if (node_to_node[index] == 0)
{
node_to_node[index] = 999999;
}
j += 1;
}
i += 1;
j = 0;
}

I took a good look at the code and it doesn't look that frightening! I have used several vars with short names (i, j, k, l) to make the code more readable. The piece of code above computes the correct index for the array and replaces all the zeros with 999,999. Do you remember the previous table? If two nodes couldn't see each other the distance between them was zero. We want to compute the shortest path between any two nodes so if they can't see each other we'd better set a huge distance between them. The table should look like this now:

Ok, so if a node can see other node I can see its distance to that node in the table; if it can't see the other node the distance between them is 999,999. Time to compute the distance between any two nodes, even if they can't see each other:

     i = 0;
j = 0;
k = 0;
while (i < max_nodes)
{
while (j < max_nodes)
{
while (k < max_nodes)
{
if (node_to_node[j + i * max_nodes] + node_to_node[i + k * max_nodes]) < (node_to_node[j
+ k * max_nodes])
{
node_to_node[j + k * max_nodes] = node_to_node[j + i * max_nodes] + node_to_node[i
+ k * max_nodes];
visited[j + k * max_nodes] = i;
}

I'm going to use a numerical example to make the things easier for you. We go through all the nodes and if distance(2, 10) + distance(10, 15) < distance(2, 15) then 10 is a node on a shorter path, so we make the change: we will not use the older, longer route from now on. We also store the newly visited node (10 in my example).

Take a look at the picture above: node 2 knew that the shortest path to node 15 was through 4, 5, 6, 7, 8, 9 and 11 but now it has learned that there is a shorter path through node 10. The process repeats for all the nodes.

if (j == k)
{
node_to_node[j + k * max_nodes] = 0;
visited[j + k * max_nodes] = 0;
}
k += 1;
}
k = 0;
j += 1;
}
k = 0;
j = 0;
i += 1;
}
}

When j = k we set the distance between j and k: distance(0, 0).... distance (29, 29) to zero. A node should be zero quants away from itself, isn't it? We do the same thing with the visited node. Those vars are incremented, initialized, etc to make sure that the three neste while loops continue to run passing through all the elements of node_to_node. You are interested in seeing a new node_to_node table, arent't you?

That's a lovely table! I can see the shortest distance between any two nodes, even if they can't see each other! The distance between node 2 and node 6 is 768 quants and so on. Now we need to compute the actual path (we don't know it yet) and we are ready to go!

Do you remember the tiny function used for the mouse movement? When you click the left mouse button you run this function - function display_path:

function display_path()
{
proc_kill(4);
while (mouse_left == 1) {wait
(1);}
k = 0;
while (k < 30)
{
path[k] = 0;
k += 1;
}

Only one instance of this function should be running; we wait for the left mouse button to be released and then we reset the array that holds the actual path, named... path. We do that because a part of an older, longer path would continue to be stored inside the path array:

     k = 0;
path[k] = target_node;
find_path(start_node, target_node);
sleep (1);
i = k;
while (i >= 0)
{
temp.x = nodex[path[i]];
temp.y = nodey[path[i]];
temp.z = 50;
ent_create (marker_mdl,
temp, dynamic_light);
i -= 1;
sleep (0.5); //
every 0.5 seconds
}
}

We write the first path element (target_node) and then we run the function that finds the shortest path between start_node and target_node. That function is extremely fast so you don't need to wait for a second; wait (1) would work ok too. I chose to wait for a second because I'm going to use a few dynamic lights and I want you to see them in their full splendor. The var named k stores the number of nodes on the shortest path; we will see how it does that in a second. As long as we haven't created all the lights that describe the shortest path between start_node and target_node, we get the previously store coordinates of the proper nodes from nodex and nodey and we create invisible entities that generate red light about 50 quants above the ground level. This way we don't have to interact with the nodes but we will make them look reddish.

These red lights are created every 0.5 seconds and their associated function has its code listed below:

function
dynamic_light()
{
my.invisible = on;
my.passable = on;
my.lightrange = 100;
my.lightred = 255;
my.lightgreen = 0;
my.lightblue = 0;
sleep (3);
ent_remove(me);
}

I have used the same marker model for these red light entities but it is made invisible and passable so who cares about it? The red lights will exist for 3 seconds and then they will be removed; this way we can have up to 6-7 lights glowing on our path at the same time.

Feeling tired? Me too. There's only one function left to be discussed:

function find_path(l,
j)
{
i = 0;
while (i < max_nodes)
{
if ((i != j) &&
(visited[i + max_nodes * j] == 0))
{
if (node_to_node[l + max_nodes * j] == node_to_node[l + max_nodes * i] + node_to_node[i
+ max_nodes * j])
{
k += 1;
path[k] = i;
next_node = i;
i = max_nodes - 1;
if (path[k] == l)
{
return;
}
}
}
i += 1;
}
j = next_node;
find_path(l, j);
}

This function writes the actual path inside the "path" array. Let's see how it was called from function display_path:

find_path(start_node, target_node);

This means that if I run find_path(14, 5) the function will find nodes that create the shortest path between 14 and 5, for example: 14 2 7 19 5. Let's see how it does that.

The var named i starts from 0 and we will search from this node (0) the rest of the 29 nodes (max_nodes = 30). If the current node can "see" one of the other nodes (excepting itself, i != j) and if it is a node on the shortest path (because the shortest path includes it) then we have found a new path element; we increment k and we store the new node number inside the "path" array. We copy the new node on the shortest path in next_node because it will loose its value right away; we set i = 29 to get out of the while loop because we want to eliminate other paths that have the same length, like in the picture below:

If path[k] = l we have reached the starting point (l = start_node) so we get out of the function, otherwise we set the next node on the path as target (j = next_node) and the function calls itself again (that's how recursive functions work). If your previous function call has searched the path between nodes 2 and 10, now it will search for the path between 2 and 7, if node 7 is the next node on the shortest path. The process repeats as many times as it is needed and at the end of it the whole path is stored in the array named path.

That was all! I don't want you to feel too bad if you don't understand how this code works right away; a good AI system is pretty hard to code, after all. The last article in this series will teach you how to set up a playable level without touching a single line of code! Perfect AI was written with these goals in mind:
- Extremely friendly setup for the level designer;
- Simple usage for the programmer.

The AI engine was built so next month we will have fun with a playable level. I'll see you then!

What do you say? You have found a bug in my Perfect AI code?

I see... so you have clicked on node 1 and instead of seeing 1 - 2 - 3 - 4 - 5 on the screen you only got 1 - 3 - 4 - 5! This isn't a bug; node 1 can see node 3 directly so it doesn't need node 2 to create the shortest path to the player. If you like to have more points on the shortest path (why would you want that?) use smaller range scan_entity instructions and place more nodes in your levels.

Final advice: test your paths carefully. I had to move a few of the nodes in my demo level a little because they were seeing each other directly, like in the picture below:

These minor changes don't change the gameplay at all so don't be shy and do the same thing in your levels if you need to.


Untitled

Perfect AI - part III

This month we will witness player's panic as he (or she) is being attacked by a drone that uses the Perfect AI code we've covered so far. We will discuss many new things but don't worry, the hard part is over.

Your assignment is simple: run for your life all the time. If the drone runs into you, it kills you instantly. If the drone sees you, it starts firing rockets at you; every rocket takes 10 points from your health. The drone will chase you to your death but you've got a bigger movement speed so you should be able to stay alive for a while :)

This Perfect AI series has started two months ago; therefore, I encourage you to read the articles in Aum27 and Aum28 before moving on. This article will describe only the functions and actions that were added or were changed. The script file - ai3.wdl - is split in two parts; the new code added for this month's demo is placed below these lines in ai3.wdl:

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// new stuff for the 3rd Perfect AI episode
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

Let's examine the new code.

var
dist_to_drone[30];
var drone_start[12] = 830,
700, 100, -830, 700, 100, 830, -700, 100, -830, -700, 100;

The first array holds the distance from the drone to its surrounding nodes (or zero if the nodes can't see it). Drone_start holds the starting coordinates for the drone model.

I chose four starting positions for the drone and drone_start[12] holds their coordinates. The drone will pick a random starting point every time.

text
ai_txt // displays the texts on the screen
{
font = system_font;
pos_x = 0;
pos_y = 0;
string = "Current path:
\n\nHealth:\nTarget:\nStart:";
flags = visible;
}

panel
ai_pan // displays the numerical values on the screen
{
pos_x = 0;
pos_y = 0;
layer = 10;
digits = 80, 24, 2, system_font,
1, player.health;
digits = 80, 37, 2, system_font, 1, target_node;
digits = 80, 50, 2, system_font, 1, start_node;
digits = 120, 0, 4, system_font,
1, path[0];
digits = 150, 0, 4, system_font, 1, path[1];
digits = 180, 0, 4, system_font, 1, path[2];
digits = 210, 0, 4, system_font,
1, path[3];
digits = 240, 0, 4, system_font, 1, path[4];
digits = 270, 0, 4, system_font, 1, path[5];
digits = 300, 0, 4, system_font,
1, path[6];
digits = 330, 0, 4, system_font, 1, path[7];
digits = 360, 0, 4, system_font, 1, path[8];
digits = 390, 0, 4, system_font,
1, path[9];
digits = 420, 0, 4, system_font, 1, path[10];
digits = 450, 0, 4, system_font, 1, path[11];
digits = 480, 0, 4, system_font,
1, path[12];
digits = 510, 0, 4, system_font, 1, path[13];
digits = 540, 0, 4, system_font, 1, path[14];
flags = overlay, refresh,
visible;
}

We are using a text and a panel to display the current path from the drone to the player, start_node, target_node and player's health.

The action associated to the nodes has changed a little so we will discuss it again briefly:

action
node
{
my.invisible = on;
my.enable_scan = on;
my.event = trace_back;
my.skill47 = 1234;
my.passable = on;
number_of_nodes += 1;
my.skill48 = number_of_nodes;
nodex[my.skill48] = my.x; // store x and y
nodey[my.skill48] = my.y; // for this node
while (number_of_nodes < (max_nodes - 1)) {wait (1);}

I chose to hide the nodes in this demo but you can make them visible again by commenting the first line. The nodes are sensitive to scanning; if they are scanned, function trace_back() will run. We set a weird value for skill47 (1234) to make sure that we can detect if a certain entity is a node or not and we make the nodes passable because they shouldn't block the player or the drone. Every node gets a unique ID number (0...29) which is store in its skill48. We also store the x and y coordinates for every node in nodex and nodey. Finally, we wait for all the nodes to be placed in the level.

     while (current_node <= number_of_nodes)
{
if (current_node
== my.skill48)
{
temp.x = 360; // horizontal scanning angle
temp.y = 30; // vertical scanning angle
temp.z = 1000; // scanning range
scan_entity (my.x, temp); // scan the nodes nearby
wait (1);
current_node += 1; // move to the next node
}
wait (1);
}
distances_computed = 1;

Every node will scan the nodes that are closer to 1000 quants to it; when all the nodes have scanned their neighbors the var named distances_computed is set to 1.

     while (player == null) {wait (1);}
while (1)
{
node_to_player[my.skill48]
= vec_dist(player.x, my.x);
if (node_to_player[my.skill48]
< 500)
{
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.pos, player.pos) == 0)
{
see_player[my.skill48] = 1;
}
else
{
see_player[my.skill48] = 0;
}
}
sleep (0.1);
}
}

We wait until the player is created and then we store the distance from every node to the player inside the array named node_to_player. If the player comes closer than 500 quants to a node we trace from the node to the player; if the node can "see" the player, it sets see_player to 1, otherwise see_player will be set to 0. Every node traces 10 times a second.

Time to take a look at the event function associated to the nodes:

function trace_back()
{
if (event_type == event_scan)
{
if (you.skill47
== 1234) // scanned by a node
{
my.skill45 = handle(you);
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.x, you.x) == 0)
{
you = ptr_for_handle(my.skill45); // restore the "you" pointer
index = you.skill48 + my.skill48 * max_nodes;
node_to_node[index] = vec_dist(my.x, you.x);
}
}
else // scanned
by the drone (skill47 = 5678)
{
my.skill45 = handle(you);
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.x, you.x) == 0)
{
you = ptr_for_handle(my.skill45);
dist_to_drone[my.skill48] = vec_dist (my.x, you.x);
if ((dist_to_drone[my.skill48] < min_drone) && (dist_to_drone[my.skill48]
!= 0))
{
min_drone = dist_to_drone[my.skill48];
}
wait (2);
if (dist_to_drone[my.skill48] == min_drone) // this is the closest node to the
drone?
{
start_node = my.skill48;
}
}
else
{
dist_to_drone[my.skill48] = 0;
}
}
}
}

If the node is scanned by another entity, there are two possibilities:
1) The node was scanned by another node (skill47 = 1234). If this is true, we store the pointer to the entity that has performed the scanning (you) and then we trace from this node to "you". If the node can see the entity that has scanned it, we store the distance between the node and the entity in node_to_node;
2) The node was scanned by the drone (skill47 = 5678). We store the "you" pointer and then we trace back from the node to the drone. If this node can see the drone, we store the distance between the drone and the node in dist_to_drone. The variable named min_drone will store the distance from the closest node to the drone; start_node will be the closest node to the drone if its dist_to_drone will hold a value equal to min_drone.

You can see that the drone has scanned 5 nodes but only node 2 and node 4 can see the drone; start_mode will be set to 2 because that's the closest node to the drone that can see the drone.

The code associated to the player has changed too:

action
player1
{
player = me;
my.health = 100;
my.enable_entity = on;
my.enable_impact = on;
my.event = decrease_health;
// I loose health when I collide with entities

The player has 100 health points (health = skill40) and it is sensitive to other entities (the drone and its rockets). The event associated to player's action is decrease_health.

     while (my.health > 0)
{
my.pan += 4 * (key_cul
- key_cur) * time;
my.skill1 = 15
* (key_cuu - key_cud) * time;
if (key_cuu + key_cud
> 0)
{
ent_cycle("walk", my.skill46); // play "walk" frames animation
my.skill46 += 10 * time; // "walk" animation speed
my.skill46 %= 100; // loop animation
}
else // the player
is standing
{
ent_cycle("idle", my.skill46); // play "stand" frames animation
my.skill46 += 2 * time; // "stand" animation speed
my.skill46 %= 100; // loop animation
}
move_mode = ignore_passable
+ glide;
ent_move(my.skill1,
nullvector);
wait (1);
}

As long as the player is alive, it can move and rotate using the arrow keys. If the player is walking, it plays its "walk" animation; otherwise, it plays its "stand" animation. The player moves ignoring the passable entities (the nodes) and glides along the walls if it collides with them.

     my.skill46 = 0;
while (my.skill46 < 80)
{
ent_cycle("death",
my.skill46); // play "death" animation
my.skill46 += 2
* time;
wait (1);
}
}

When the player is dead, it plays its "death" animation. Let's take a look at the code associated to the drone now:

starter
drone
{
while (total_frames < 5) {wait (1);}
ent_create (drone_mdl, nullvector, init_drone);
}

The drone is created by a "starter" function that waits for 5 frames and then creates the drone in the level and associates it the function init_drone:

function init_drone()
{
while (player == null) {wait (1);}
var corner;
my.skill47 = 5678;
corner = sys_seconds % 4;
if (corner == 0)
{
my.x = drone_start[0];
my.y = drone_start[1];
my.z = drone_start[2];
}
if (corner == 1)
{
my.x = drone_start[3];
my.y = drone_start[4];
my.z = drone_start[5];
}
if (corner == 2)
{
my.x = drone_start[6];
my.y = drone_start[7];
my.z = drone_start[8];
}
if (corner == 3)
{
my.x = drone_start[9];
my.y = drone_start[10];
my.z = drone_start[11];
}

The function waits until the player is created and then creates a local variable named corner. We set skill47 = 5678 as a unique ID for the drone and then we generate a random number from 0 to 3 using sys_seconds. We choose one of the four starting positions for the drone depending on the random value stored in "corner".

     while (player.health > 0)
{
my.lightrange =
400; // emit light
my.lightred = 0;
my.lightgreen =
255; // green light
my.lightblue =
0;
trace_mode = ignore_me
+ ignore_models + ignore_passents;
if (trace (my.pos,
player.pos) == 0)
{
shoot_rocket();
sleep (1);
}

As long as the player is alive, the drone emits green light on a diameter of 400 quants. If the drone can see the player (trace = 0) it starts shooting rockets at it every second.

else
{
min_drone = 999999;
temp.x = 360; // horizontal scanning angle
temp.y = 360; // vertical scanning angle
temp.z = 800; // scanning range
scan_entity (my.x, temp);
sleep (1);

k = 0;
while (k < max_nodes)
{
path[k] = 0; // reset the array, get rid of the previously stored path
k += 1;
}
k = 0; // start with the first element of path[30]
path[k] = target_node; // write the first path element

If the drone can't see the player, min_drone is set to a huge distance and the drone starts scanning on a range of 800 quants in order to get the closest node to it. We reset the array named "path" (that's the array that stores the current path) and then we write the first path element = target_node.

snd_play (getpath_wav, 70, 0);
if (start_node != target_node)
{
find_path(start_node, target_node);
wait (2);
i = k;
while (i >= 0)
{
temp.x = nodex[path[i]];
temp.y = nodey[path[i]];
temp.z = my.z; // use drone's height for z
d1_marker = ent_create (marker_mdl, temp, follow_path); // create an invisible
entity that guides the drone
wait (1);
while (vec_dist (d1_marker.x, my.x) > 10)
{
my.skill1 = 10 * time;
my.skill2 = 0;
my.skill3 = 0;
move_mode = ignore_passable + glide;
ent_move(my.skill1, nullvector);
wait (1);
}
i -= 1;
wait (1);
}
}
}
}
}

We play a sound that announces
a new path search and then, if start_node and target_node are different, we run
the find_path function from Aum28; this recursive function finds the shortest
path between the two nodes passes as its parameters. The result of the function
is stored in the array named path; we've talked about it in the previous paragraph.

We wait for 2 frames and then we start moving the drone from one path point to another. We create an entity named d1_marker above every node along the shortest path and then we move the drone towards it until the distance between the drone and the invisible d1_marker entity is below 10 quants; when this happens, we create d1_marker above the next node on the shortest path and then we move the drone towards it. The process repeats until the drone has reached the last node stored inside the array named path.

The entity named d1_marker runs this simple function:

function
follow_path()
{
my.invisible = on;
my.passable = on;
vec_set (temp.x, my.x);
vec_sub (temp.x, you.x);
vec_to_angle (you.pan, temp);
while (d1_marker == me) {wait (1);}
ent_remove (me);
}

The function makes the marker invisible and passable and rotates the drone towards it. The older unused markers will be removed as new markers are being created.

Let's take a look at the functions that create the rockets:

function
shoot_rocket()
{
vec_set (temp.x, player.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp); // turn the drone towards the player
if (player.health > 0)
{
ent_create (rocket_mdl,
my.pos, move_rocket); // create a rocket
}
}

Let me remind you that the function named shoot_rocket() runs when the drone can see the player. The function turns the drone towards the player and if the player isn't dead it creates rockets and runs them using the function below:

function move_rocket()
{
my.pan = you.pan;
my.enable_entity = on;
my.enable_impact = on;
my.enable_block = on;
my.event = remove_rocket;
while (my != null)
{
my.skill1 = 15
* time;
my.skill2 = 0;
my.skill3 = 0;
move_mode = ignore_you
+ ignore_passable; // ignores the drone (its creator = you)
ent_move(my.skill1,
nullvector);
wait (1);
}
}

The rockets and their parent (the drone) have the same pan angle and they are sensitive to impact with other entities or with the level blocks. The rocket moves with the speed given by its skill1 and it ignores its creator, the drone (you) while moving. If the rocked collides with something, its remove_rocket() event will run:

function
remove_rocket()
{
snd_play (hit_wav, 100, 0);
wait (1);
ent_remove (me);
}

That's an easy function so I'll let you figure out what it does. I'm going to take care of the last function:

function
decrease_health()
{
if (you.skill47 == 5678)
// collided with the drone?
{
my.health = 0;
// instant death
}
else // hit by a rocket?
{
my.health -= 10;
// loose 10 health points
}
if (my.health <= 0)
{
my.event = null;
snd_play (death_wav,
40, 0);
}
}

This function is the event function associated to the player. If the player collides with the drone (skill47 = 5678) the player will die instantly; otherwise, the player was hit by a rocket and it will loose 10 health points. If its health decreases to 0, the player will stop responding to events and it will play its death sound.

That was the first playable demo that uses the Perfect AI core. The drone will search for the player and will find it every time; the only drawback is that the while loop keeps running until all the points on the path are visited so if the player changes its position quickly the drone won't be able to detect its new location until it reaches the last point of the previous path. I have coded the drone this way because you can see and understand how the paths are changing from one target_node to another; however, this limitation can be overcome easily by inserting a new trace in the while loop that moves the drone.

Next month we will get out of the maze level (sniff...) and we will move into a typical 3D shooter level with human enemies. I promise to give the player a gun so stay tuned!

Oh, and don't blame me if the player model penetrates the walls during its "death" animation sequence:

This problem appears because of the strange "death" animation of the model and has nothing to do with my code.


Untitled

Perfect AI - part IV

The 4th Perfect AI episode adds a few more important features to the code we've got so far. Don't forget to read the articles in Aum27...29 to see how it all started.

Open level4, build it and run it using ai4.wdl:

Take a deep breath and then start running; the four little guys will try to hunt you down. Use "W", "S", "A", "D" to move, left mouse button to shoot. Try to survive as much time as possible; take a look at the four paths displayed on the screen and see how they change depending on your position. I could have used many more enemies in this demo, but trust me, four are enough in this level. Oh, you can kill all the enemies; I managed to do it.

Some of the code from Aum29 has changed, more code was added; we will discuss all the new or modified actions and functions, placed below this header in ai4.wdl:

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// new stuff for the 4th Perfect AI episode
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

define health skill42;
define start_node skill43;
define closest_node skill44;

We are using a simple panel to display the paths for the 4 enemies, player's health and the target node (the destination for the enemies):

panel
ai_pan // displays the numerical values on the screen
{
pos_x = 0;
pos_y = 0;
layer = 10;
digits = 80, 72, 2, system_font,
1, player.health;
digits = 80, 85, 2, system_font, 1, target_node;

     digits = 120, 0, 4, system_font, 1, path[0];
// display the first 15 points on the shortest path
digits = 150, 0, 4, system_font, 1, path[1];
// should be enough for everybody
digits = 180, 0, 4, system_font, 1, path[2];
// many more digits here
............................................................

flags = overlay, refresh, visible;
}

This demo uses 30 nodes that exchange information; let's take a look at the action that drives them:

action
node // using 30 nodes in this demo
{
my.invisible = on;
my.enable_scan = on;
my.event = trace_back;
my.skill47 = 1234;
my.passable = on;
node_id += 1;
my.skill48 = node_id;
nodex[my.skill48] = my.x;
nodey[my.skill48] = my.y;
while (node_id < (max_nodes
- 1)) {wait (1);}

Every node is invisible and sensitive to scan; when a node is scanned, it runs its trace_back event. We set skill47 = 1234 for every node and then we make it passable, because the player and its enemies should be able to pass through it. Every node gets a unique id number, which is stored in its own skill48. We also store the x and y coordinates of the node in two arrays named nodex and nodey and then we wait until all the nodes are placed in the level.

     while (current_node <= node_id)
{
if (current_node
== my.skill48)
{
temp.x = 360;
temp.y = 30;
temp.z = 1000;
scan_entity (my.x, temp);
wait (1);
current_node += 1;
}
wait (1);
}
distances_computed = 1;

The nodes start scanning each order in this order: node0 scans node1... node29, then node1 scans node0... node29, etc. We are using a horizontal scanning range of 360 degrees, a vertical scanning angle of 30 degrees and a scanning range of 1000 quants. As soon as the last node (29) has scanned node0... node28, a var named distances_computed is set to 1.

     while (1)
{
node_to_player[my.skill48]
= vec_dist(player.x, my.x);
if (node_to_player[my.skill48]
< 500)
{
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.pos, player.pos) == 0)
{
see_player[my.skill48] = 1;
}
else
{
see_player[my.skill48] = 0;
}
}
sleep (0.1);
}
}

Every node stores the distance from it to the player in an array named node_to_player; if the player comes closer than 500 quants to the node, the node traces from its position to the player. If the node can "see" the player, it sets see_player to 1, otherwise see_player is set to 0. All these things happen 10 times a second.

Let's see what happens when a certain node is scanned by another node:

function
trace_back()
{
if (event_type == event_scan)
{
if (you.skill47
== 1234) // scanned by a node
{
my.skill45 = handle(you);
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.x, you.x) == 0)
{
you = ptr_for_handle(my.skill45);
index = you.skill48 + my.skill48 * max_nodes;
node_to_node[index] = vec_dist(my.x, you.x);
}
}

If one of the nodes is scanned, we check if the scanner has skill47 = 1234; if this is true, we know that the scanner was a node. We store the "you" entity (trace would destroy it) in skill45 and we trace back from the scanned node to the scanner. If the node can "see" the node that has scanned it, it restores the "you" pointer and stores the distance between the two nodes in the vector named node_to_node (a bidimensional array that was simulated using a simple array).

          else
{
my.skill45 = handle(you); // store "you" because trace will destroy it
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.x, you.x) == 0) // if this node can "see" the enemy that scanned
it
{
you = ptr_for_handle(my.skill45); // restore the "you" pointer
dist_to_enemy[my.skill48 + max_enemies * you.skill48] = vec_dist (my.x, you.x);
if ((dist_to_enemy[my.skill48 + max_enemies * you.skill48] < you.closest_node)
&& (dist_to_enemy[my.skill48 + max_enemies * you.skill48] != 0))
{
you.closest_node = dist_to_enemy[my.skill48 + max_enemies * you.skill48];
}
wait (2);
if (dist_to_enemy[my.skill48 + max_enemies * you.skill48] == you.closest_node)
{
you.start_node = my.skill48;
}
}
else
{
you = ptr_for_handle(my.skill45);
dist_to_enemy[my.skill48 + max_enemies * you.skill48] = 0;
}
}
}
}

The "else" branch runs when a certain node is scanned by one of the enemies; we store "you" and we trace from the node to the enemy that has scanned the node, storing the distance from the node to the enemy in the array named dist_to_enemy. If the distance between the node and the enemy is smaller than the older distance stored by the enemy (closest_node = skill44) and it is bigger than zero, this node will be set as the closest node to the enemy. We give the rest of the nodes the chance to change this for two frames (in case that one of them is closer to the enemy) and then we set the starting node for the enemy as the node that is the closest to the enemy. If the node can't see the enemy, its dist_to_enemy will be set to zero.

All these things happen because the enemy needs to know which node is the closest to it and sees it. The enemy will move towards the closest node and then it will run the function that generates the shortest path (more on that a little later).

Let's discuss my favorite action:

action
player1
{
player = me;
my.health = 100;
my.push = -1;
my.enable_entity = on;
my.enable_impact = on;
my.event = decrease_health;
while (my.health > 0)
{
my.pan += 8 * (key_a
- key_d) * time;
my.skill1 = 15
* (key_w - key_s) * time;

The player has 100 health points; its push value is -1, so it won't pass through walls (their push is zero). The player is sensitive to other entities and runs its decrease_health event when something hits it. As long as the player is alive, it can rotate using "A" and "D" and moves forward / backward with "W" and "S".

          if (key_w + key_s
> 0)
{
ent_cycle("walk", my.skill46);
my.skill46 += 10 * time;
my.skill46 %= 100;
}
else
{
ent_cycle("idle", my.skill46);
my.skill46 += 2 * time;
my.skill46 %= 100;
}
if (mouse_left
== 1)
{
players_bullet();
}
move_mode = ignore_passable
+ glide;
ent_move(my.skill1,
nullvector);
wait (1);
}

If the player is moving ("W" or "S" is pressed), the model plays its "walk" animation in a loop, otherwise it plays its "idle" animation in a loop. If the player presses the left mouse button, the function named players_bullet will run. The player model moves ignoring passable entities and glides along the walls.

     my.skill46 = 0;
while (my.skill46 < 80)
{
ent_cycle("death",
my.skill46);
my.skill46 += 2
* time;
wait (1);
}
}

The lines above run when the player has died: they run player's "death" animation once. Let's see the most complicated function - the one attached to the enemies:

action enemy
{
my.push = -2;
my.enable_entity = on;
my.enable_impact = on;
my.health = 100;
my.event = enemy_event;
enemy_id += 1;
my.skill48 = enemy_id;
my.skill47 = 5678;
while ((player.health >
0) && (my.health > 0))
{
trace_mode = ignore_me
+ ignore_models + ignore_passents;
if (trace (my.pos,
player.pos) == 0)
{
my.skill46 = 0;
shoot_bullet();
while (my.skill46 < 100)
{
ent_cycle("standshoot", my.skill46);
my.skill46 += 15 * time;
wait (1);
}
}

The enemy has its "push" set to -2; this way the enemies can't kill each other because their bullets have push = -1. The enemy is sensitive to other entities and to impact; its health is set to 100 and its event is enemy_event. Every enemy gets a unique id number (0...3 in this demo) which is stored in its own skill48. We set skill47 to 5678 - that's how we can identify if a certain entity is an enemy or not.

The while loop continues to run if the player and the enemy are alive. The loop traces from the enemy to the player; if the enemy can see the player, is starts shooting at it using the function named shoot_bullet, and playing its "standshoot" animation in a loop.

else
{
my.closest_node = 999999;
temp.x = 360;
temp.y = 360;
temp.z = 800;
scan_entity (my.x, temp);
if (vec_dist (my.x, player.x) > 1000)
{
my.skill46 = 0;
while (my.skill46 < 100)
{
ent_cycle("alert", my.skill46);
my.skill46 += 2 * time;
wait (1);
}
}
else
{
wait (2);
}
while (function_busy == 1) {wait (1);}
function_busy = 1;

If the enemy can't see the player, its sets its closest_node distance to a huge value and then it scans the nodes nearby because it needs to detect the closest node to it. If the player is farther than 1000 quants, the enemy plays its "alert" animation, pretending to think before choosing a new path that leads to the player. If the distance between the enemy and the player is smaller than 1000 quants, the enemy will wait for 2 frames before choosing a new path.

I have used a single function that computes the paths for all the enemies. When the function is busy, the var named function_busy will be set to 1; otherwise it will be set to 0. The enemy waits until function_busy is 0 and then it takes control over it, setting function_busy to 1.

path_index = 0;
while (path_index < max_nodes)
{
path[path_index + max_enemies * my.skill48] = 0;
path_index += 1;
}
path_index = 0;
path[path_index + max_enemies * my.skill48] = target_node;
snd_play (getpath_wav, 100, 0);
if (my.start_node != target_node)
{
node_index = find_path(my.start_node, target_node, my.skill48, path_index);
wait (2);
function_busy = 0;

At first, we reset the array named "path", because we need to get rid of the previously stored path from the enemy to the player. Target_node, the closest node to the player that can see the player, is the first path element. The engine will play a short sound, letting us know that one of the enemies needs to get the shortest path to the player. If the starting node and the target node are different, we run the function named find_path that takes four parameters and returns the result in node_index. We wait for the path to be computed and then free the function by setting function_busy to zero.

while (node_index >= 0)
{
temp.x = nodex[path[node_index + max_enemies * my.skill48]];
temp.y = nodey[path[node_index + max_enemies * my.skill48]];
temp.z = my.z;
vec_set (destination_node, temp);
vec_set (temp.x, destination_node.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp);
while ((vec_dist (destination_node.x, my.x) > 10) && (relax == 0))
{
ent_cycle("run", my.skill46);
my.skill46 += 10 * time;
my.skill46 %= 100;
my.skill1 = 10 * time;
my.skill2 = 0;
my.skill3 = 0;
move_mode = ignore_passable + glide;
ent_move(my.skill1, nullvector);
if (trace (my.pos, player.pos) == 0)
{
node_index = -1;
}
wait (1);
}
node_index -= 1;
wait (1);
}
}
}
}

The shortest path to the player was returned by function find_path, now we need to move the enemy on that path! We retrieve the x and y coordinates for the nodes on the path, using enemy's height for z, and then we rotate the enemy towards the destination node. As long as the enemy hasn't come closer than 10 quants to the player and the player isn't dead yet (relax = 0), the enemy plays its "run" animation in a while loop and moves ignoring passable entities and gliding along the walls.

If the enemy can see the player (trace returns zero), node_index is set to -1, allowing the enemy to get out of the while loop, in order to compute a new path to the player, because the shortest path to the player has changed. Finally, node_index is decreased, allowing the enemy to move towards a new node as soon as it has come closer than 10 quants to the previous node.

     // the player is dead, so switch to "stand" if
you are alive
while (my.health > 0)
{
ent_cycle("stand",
my.skill46); // play "stand" frames animation
my.skill46 += 4
* time; // "stand" animation speed
my.skill46 %= 100;
wait (1);
}
}

The lines of code above will run if the player had died; the enemy will switch to "stand" if it is alive and will play this animation in a loop. The function below computes and writes the actual path for every enemy that uses it:

function find_path(sn,
tn, id, px)
{
var p_i = 0;
while (p_i < max_nodes)
{
if ((p_i != tn)
&& (visited[p_i + max_nodes * tn] == 0))
{
if (node_to_node[sn + max_nodes * tn] == node_to_node[sn + max_nodes * p_i] +
node_to_node[p_i + max_nodes * tn])
{
px += 1;
path[px + max_enemies * id] = p_i;
next_node = p_i;
p_i = max_nodes - 1;
if (path[px + max_enemies * id] == sn)
{
return(px);
}
}
}
p_i += 1;
}
tn = next_node;
find_path(sn, tn, id, px);
}

The function takes four parameters:
- Start node (sn), the node that is the closest node to the enemy and can see the enemy;
- Target node (tn), the node that is the closest node to the player and can see the player;
- Enemy id (id), a number that tells the function which enemy needs to get the path to the player;
- Path index (px), used as an index for the shortest path to the player.

The function will search from the current node to the rest of the nodes; this demo uses 30 nodes, so every node has to check 29 more nodes. If the current node can see one or more of these nodes and if we have a node that is a part of the shortest path (the shortest path includes it), we move the the next path array element and we store the node number inside the path[] array (a bidimensional array that was created using a simple array). We store the new node on the shortest path in next_node, because it will loose its value right away, and then we stop testing other values (p_i = max_nodes - 1) because we want to eliminate other paths that might have the same length.

If we have reached the node that is the starting point for the enemy, we get out of the function, returning the value stored in px. If the function continues to run, p_i is incremented; if the current node can't see another node, tn = next_node sets the next node on the path as target and the (recursive) function is run again, searching for the shortest path but being one step closer to the final result.

The hard part is over; the rest of the functions are simple:

function
shoot_bullet()
{
vec_set (temp.x, player.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp);
if (player.health > 0)
{
ent_create (bullet_mdl,
my.pos, move_bullet);
}
}

That's the function that runs if the enemy can see the player; it rotates the enemy towards the player and if player's health is above zero, it starts creating bullets that use the function move_bullet():

function
move_bullet()
{
my.skill47 = 5;
my.pan = you.pan;
my.push = -1;
my.enable_entity = on;
my.enable_impact = on;
my.enable_block = on;
my.event = remove_bullet;
while (my != null)
{
my.skill1 = 15
* time;
my.skill2 = 0;
my.skill3 = 0;
move_mode = ignore_you
+ ignore_passable + ignore_push;
ent_move(my.skill1,
nullvector);
wait (1);
}
}

I have set skill47 to 5 for every bullet fired by the enemies; the bullet and the enemy that creates it have the same pan angle. I have set push = -1 for the bullet, because this way the bullet can pass through enemies without hurting them (their push was set to -2) but hits the player (its push was set to -1). The bullet is sensitive to other entities and to level blocks; its event function is named remove_bullet. As long as the bullet continues to exist, it moves with the speed given by skill1, ignoring its creator, passable entities and other entities that have their push value below -1.

function
remove_bullet()
{
if ((my.skill47 == 5) && (you.skill47 == 5))
{
my.passable = on;
wait (2);
ent_remove (me);
return;
}
my.event = null;
ent_playsound (my, hit_wav, 1000);
sleep (0.1);
ent_remove (me);
}

The function above runs when the bullet hits something. If two enemy bullets have collided, one of them is made passable and after two frames it is removed; if the bullet hits something else, it stops responding to other event, plays a hit_was sound and disappears after 0.1 seconds.

function
decrease_health()
{
if (you.skill47 == 5678)
{
my.health = 0;
}
else // hit by a bullet?
{
my.health -= 1;
}
if (my.health <= 0)
{
my.event = null;
snd_play (death_wav,
40, 0);
}
}

Function decrease_health is the one that runs when the player is hit by something; if the player has collided with one of the enemies (you.skill47 = 5678), the player will die instantly because the enemy will stab it (more on that a little later). If the player is hit by a bullet, it looses 1 health point every tick; the bullet will be active for 0.1 seconds so I'll let you do the math. If player's health goes below zero, the player will stop responding to events and it will play the death_wav sound. Let's see the event function associated to the enemies:

function enemy_event()
{
if (you.skill47 == 5678)
{
if ((vec_dist (my.x,
player.x)) > (vec_dist (you.x, player.x)))
{
my.passable = on;
sleep (0.5);
my.passable = off;
}
}

If the enemy has collided with another enemy, and if "you" are closer than "my" to the player, "my" will become passable, allowing "you" to pass first; this situation lasts for 0.5 seconds.

     if (you == player)
{
my.event = null;
player.health =
0;
vec_set (temp.x,
player.x);
vec_sub (temp.x,
my.x);
vec_to_angle (my.pan,
temp);
my.skill46 = 0;
while (my.skill46
< 90)
{
ent_cycle("point", my.skill46);
my.skill46 += 1 * time;
wait (1);
}
relax = 1;
}

If the enemy collides with the player, it will stop reacting to other events. The player will loose its health and the enemy will turn towards the player; the enemy will play its "point" (stab) animation once and then the var named "relax" will be set to 1, telling all the enemies to relax (to stop and switch their animations to "stand").

     if (you.skill47 == 1)
{
my.health -= 1;
if (my.health <=
0)
{
my.event = null;
my.invisible = on;
my.passable = on;
}
}
}

If the enemy is hit by player's bullet (you.skil47 = 1), it will loose 1 health point every tick. If the enemy is dead, it will stop reacting to other events, and then it will become invisible and passable.

function
players_bullet()
{
proc_kill(4);
while (mouse_left == 1) {wait
(1);}
snd_play (bullet_wav, 50,
0);
ent_create (bullet_mdl, player.pos,
move_players_bullet);
}

The function above runs when the player presses the left mouse button; proc_kill(4) will stop all the other instances of the current function. We wait until the left mouse button is released (we disable auto fire), we play a sound and then we create a bullet that is driven by function move_players_bullet():

function move_players_bullet()
{
my.skill47 = 1;
my.pan = you.pan;
my.tilt = you.tilt;
my.passable = on;
my.enable_entity = on;
my.enable_impact = on;
my.enable_block = on;
my.event = remove_bullet;
my.ambient = 100;

Every bullet fired by the player has its skill47 set to 1; the bullet and the player have the same pan and tilt angle. The bullet is passable at first; it is sensitive to other entities and to level blocks. The bullets fired by the player and by the enemies share the same event function - remove_bullet().

     while (my != null)
{
if (vec_dist (player.x,
my.x) > 30) {my.passable = off;}
my.skill1 = 50
* time;
my.skill2 = 0;
my.skill3 = 0;
move_mode = ignore_you
+ ignore_passable;
ent_move (my.skill1,
nullvector);
wait (1);
}
}

The bullet will stop from being passable when its distance to the player has become bigger than 30 quants. The bullet moves with the speed given by skill1, ignoring its creator and all the passable entities.

"Next month we will get out of the maze level (sniff...) and we will move into a typical 3D shooter level with human enemies. I promise to give the player a gun so stay tuned!" That's what I wrote at the end of the article that has appeared last month, so some of you might feel a little frustrated. The truth is that I got attached to the maze level :) and I wanted you to be able to see the enemies chasing the player all over the map.

Well, I like to keep my promises, so I have added a new, simple player action named "player2" at the end of the ai4.wdl file. Open the level and replace the action attached to the player (player1) with the new action (player2) to change the camera to a 1st person shooter experience. Use "W", "S", "A", "D" to move and the mouse to rotate and shoot.

What comes next month? New code and a playable level with a big number of nodes and enemies, monsters that wait until you get closer to them and then attack you, and more!


Untitled

"The making of" Perfect AI

This month I have decided to talk less about the code and more about how Perfect AI works; this way you should be able to edit my code easily. Let's test the demo first; open level5.wmp, build it and run it using the script named ai5:

I'd start running if I were in your place! Use "W", "S", "A", "D" and the mouse to move and fire. There are two enemies that start to chase you right away and another one (to the right) that waits until you come closer than 1000 quants to him before attacking you. If you shoot the guy, it'll forget about those 1000 quants and it will start to chase you too.

The enemies will shoot you as soon as they see you unless you ask them to be more gentle, so let's see the options right away:

We can set "Alert_dist" and we can check (or not) the "Gentle" flag. If we set Alert_dist to zero, the enemies will start to chase the player as soon as the level is loaded, otherwise they will wait until the player has come closer than the values specified for Alert_dist in quants (1000 quants in my example). Check the Gentle flag and the enemies will try to come close to the player, and then they will start to shoot at it. Uncheck the flag and they will shoot the player as soon as they are able to see it; this makes the things much more real and fun.

You can see that the level is a combination of indoor and outdoor areas, which doesn't really matter, as long as you place the nodes indoors and outdoors. I told you that perfect AI starts with perfect path planning; however, the enemies should be able to get to the player even if the nodes aren't placed perfectly in the level. The level I'm using this month is a mess - here's an example:

The enemy wants to find the player, so it should use the path highlighted by the red arrows; however, you can see that some other nodes can see each other (take a look at the green arrow) but the enemy would run into the wall if it would follow them, because the enemy is thicker than a bullet! The algorithm tries to find the shortest distance from the enemy to the player every time, so it will instruct the enemy to follow the route that includes the green arrow! The code used this month allows the enemies to glide along walls; if they get stuck, they stop and ask for a new path towards the player. Now you can finally place your nodes in the level without being extremely careful, but don't forget that every node uses additional resources.

This demo can work with up to 100 nodes and up to 40-50 enemies in a level. Before you start thinking that I am somehow related to Pinocchio, let me show you a screenshot that was taken while I was testing the code with about 40 enemies; I'm the one hiding behind those boxes but I'm pretty sure I'll be dead soon.

I have assumed that any path between those 100 nodes (in case you use them all) has a maximum number of 50 points, which I think is ok for any regular level. If you need more nodes, enemies, etc you can have them by increasing the size of the arrays. Well, I think that 100 nodes should be enough for everybody, but what do I know? I'm not everybody...

The algorithm used for Perfect AI works ok, but it can slow down the game a little bit. How little? My tests show that the find_path function that was used in my previous articles sets the limit to about 400 enemies running at the same time in the level. I can hear you laughing... 400 enemies are more than enough for a single level, but these 400 enemies would use ALL the resources, so if you would need to add more entities, code, panels, etc to your level, you would end up with maybe 50 enemies in a level, which isn't too much if you are creating a strategy game.

The solution is simple: I have decided to create a separate script file that is run only once, and only by the developer (not by your customers). This script named compute.wdl will detect and write all the paths between any two nodes in your level; the actual paths between any two nodes are stored in the file named paths.txt (should be paths1.txt for the first level, paths2.txt for the second level and so on). What do we gain from here? The enemies don't need to ask for the correct path, because it is already computed for them, and the speed increases with about 800%. I'll let you do the math...

This is a small portion of the paths.txt file:

1 999 2 1 999 3 1 999 4 1 999 5 1 999 6 3 1 999 7 8 1 999 8 1 999 9 8 1 999 10 5 1 999 11 10 5 1 999 12 13 16 1 999 13 16 1 999 14 1 999 15 16 1 999 16 1 999 17 1 999 .....

I have used "999" as a separator, in order to gain some extra speed with file_var_read, but the example above would look like this

1
2 1
3 1
4 1
5 1
6 3 1
7 8 1
8 1
9 8 1
10 5 1
11 10 5 1
12 13 16 1
13 16 1
14 1
15 16 1
16 1
17 1
...............

The path from 1 to 1 is obviously through 1; nodes 2, 3, 4 and 5 can see 1 directly. The path from node 6 to node 1 includes node 3 and so on. All the possible paths between any pair of nodes are written inside this file. Our playable script (ai5.wdl) loads all these values into a huge array named paths; the enemies will simply pick the shortest path from this array whenever they need it, so they will not need to compute the path at all. If you want to see how these paths are computed in compute.wdl, you will find a commented "wait (1);" instruction in there; remove the comment and you will see the find_path function going through every possible combination.

Ok, let's go back to "the making of". How does all this stuff work?

We place our nodes in the level and they get an unique id number automatically. I could have set skill1 to 1 for the first node, skill1 to 2 for the second node, etc but I thought that this method will help me gain more friends among the level designers out there :) These nodes start scanning each other: node 1 scans node 2, 3, 4, ... , 40 (if our level has 40 nodes), then node 2 scans node 1, 3, 4, ... , 40, node 3 scans node 1, 2, 4, ... , 40 and so on. When all the nodes have scanned each other, a variable named distances_computed is set to 1 and the path finding algorithm can run.

But why do these nodes scan each other? Take a look at the picture below.

Let's assume that node 1 scans the nodes around it (using those golden circles) which are 2, 5 and 7. You know that ent_scan goes through walls too, so we need to find a method that allows us to detect which nodes can "see" each other directly. The answer is simple: we trace back from every node that was scanned to the scanner, so node 2 traces from its position to node 1, node 5 traces to node 1 and node 7 traces to node 1. Node 2 and node 5 can see node 1, but node 7 can't see it; function trace_back will set the array node_to_node[2, 1] = 300 and node_to_node[5,1] = 340, which are the distances from the scanned nodes to the scanner. The value for node_to_node[7, 1] will remain zero, because node 7 can't see node 1.

I started to understand a few things... so when all the nodes have finished scanning each other, we have an array (named node_to_node) that know which nodes can see each other and the distance between them, right? What comes next?

Well, I have told you that distances_computed is set to 1 and the path finding algorithm can run; the algorithm replaces the zeros in node_to_node with a huge value: 999,999. Why did I do that? The algorithm wants to detect the shortest path from one node to another, so it would hapily use a distance that is equal to zero; however, we know that node_to_node[x, y] = 0 means that x and y are two nodes that can't see each other, so the distance between them can be huge!

The algorithm goes through all the nodes, finding the shortest distance between any two nodes; when it is over, node_to_node[x, y] will contain the shortest distance between any two nodes, even if those nodes can't see each other. Take another look at the picture above; node_to_node[1, 7] would be set to 500 if node_to_node[1, 2] would be 200 and node_to_node[2, 7] would be 300.

We have got the shortest distance between any two nodes on the map, be we haven't got the actual path yet - that's what function find_path does. It is a recursive function that chooses the shortest distance between any pair of nodes.

The picture above shows the shortest path from A to I: ABCI, with a cost (or a distance) of 350. It is clear that ADEC has a bigger cost than ABC, and function find_path does just that: it replaces ADEC with ABC if it needs to, because 100 + 150 < 90 + 110 + 80. Function find_path returns the actual points on the shortest path from the enemy to the player, and these values are written inside paths.txt.

There's only one thing that wasn't discussed: how do we know where are the start and target nodes; in other words, how do we detect the closest node to the player and the closest node to a certain enemy? Every node waits until the player has come closer than 500 quants to it, and then it traces from its position to the player. If node 3 can see the player, it sets see_player[3] = 1; if it can't see the player, it sets see_player[3] = 0. All the nodes that can see the player at a certain moment are compared, and the closest node to the player wins the competition: that will be the target node, the node that will be set as a destination for all the enemies. On the other hand, the enemies scan the nodes and every node traces back from its position to the enemy. The closest node to the enemy that can see the enemy will be the starting node for that enemy.

I have ent_created the player in this demo because I didn't want it to be scanned by the nodes at game start; set player_pos[3] inside ai5.wdl to the values that fit your needs.

That's how Perfect AI works! I think that you have a decent AI system in your hands now; it isn't a fully finished product yet, but the AI part is really over. You will need to add some code that makes the enemies take smoother turns, maybe some pain sounds, some death animations, etc, but that code isn't AI related. Don't worry, I don't plan to abandon Perfect AI; I've spent about a month working at it, after all. Post your suggestions at the forum and if you want me to continue to add features to Perfect AI I will do it, now or in the near future.


Untitled

Physics demo

A6 commercial and A6 pro include a good physics engine and this article will show you how easy it is to set it up.

Let's start with function main:

function
main()
{
level_load (phdemo1_wmb);
camera.x = 0;
camera.y = -600;
camera.z = 40;
camera.pan = 90;
ball_pos.x = -95; // initial
value, ranges from -95 to 105
ball_pos.y = 32;
ball_pos.z = 220;
ent_create (arrow_wmb, ball_pos, move_arrow);
}

We load the level and then we set the camera position and pan angle, to make it look at the board:

Before I forget, I have used a simple, oriented bitmap for the score.

We set ball_pos, and then we create the green arrow (arrow_wmb). The arrow moves all the time because of its associated function:

function
move_arrow()
{
my.passable = on;
while (balls > 0)
{
while (my.x <
105)
{
my.x += 5 * time;
ball_pos.x = my.x;
wait (1);
}
while (my.x >
-95)
{
my.x -= 5 * time;
ball_pos.x = my.x;
wait (1);
}
wait (1);
}
}

The arrow is passable and it will continue to move as long as we haven't used all the five balls (coming right on); the arrow will change its x coordinate from -95 to 105 using 2 simple while loops. Please note that ball_pos.x = arrow_wmb.x every frame.

Ok, so we've got a green wmb arrow that moves from left to right and back. Let's see what happens when we press the "Space" key:

entity* ball_ptr;

on_space = create_ball;

function
create_ball()
{
if ((balls < 1) || (ball_ptr != null)) {return;}
ball_ptr = ent_create(ball_mdl, ball_pos, init_sphere);
balls -= 1;
}

We have defined a pointer named ball_ptr. We are going to create 5 balls and this pointer will be associated to every ball at its creation time. If we run out of balls or if the pointer to the ball isn't null (which means that the ball is still bouncing around in our level) the function returns. If the number of ball is bigger than zero we create a ball, at the position given by ball_pos, running the function init_sphere. This function is the only part of the demo that uses physics; don't worry, I will comment it line by line:

function init_sphere()
{
phent_settype(my, ph_rigid, ph_sphere);
Enable physics for this ball; it will behave like a sphere when it comes to collisions

phent_setmass(my, 1, ph_sphere);
The ball has 1Kg (~2 lbs) and behaves like a sphere when it comes to rotations

phent_setfriction(my, 30);
Friction = 30 (could be 0...100)

phent_setelasticity(my, 30, 0);
Bounce a little (value = 30, could be 0...100)

     temp.x = 0;
temp.y = 0;
temp.z = -380;
ph_SetGravity(temp);
Set gravity for the sphere
(temp.z = -380 corresponds to the earth gravity factor, g = 9.81 m/s2)

while (my.z > -150) {wait (1);}
Wait until the ball has approached the floor level

sleep (3);
Then give it three more seconds to bounce around

phent_setelasticity(my, 0, 0);
Now stop bouncing

phent_setdamping(my, 0, 100);
Apply a big damping factor to prevent the ball from rotating too much

sleep (2);
Roll on the floor for two more seconds

phent_settype(my, 0, 0);
Now stop the ball; A6 commercial allows us to use one object (ball) at a time

ball_ptr = null;
Free the pointer because we need to assign it to a new ball
}

This is all the code that makes the ball jump and bounce and finally find its way into one of the baskets. If you don't own A6 commercial or A6 pro yet, take a look at the movie and see what you could do with only a few lines of code. It would be almost impossible to code this realistic behavior without a physics engine.

Physics movie (divx 5.01)


Untitled

Player selection

This article will help you to create a player selection menu that includes 3 different models, each one of them having 3 different skins. You choose the desired model using the left and right arrows, and then you choose the skin using the up and down arrows. Press Enter to start a level that uses the player you've selected when you are happy with your selection.

This is a standalone project, but function main is really simple:

function main()
{
on_d = null;
fps_max = 60;
level_load (dummy_wmb);
}

We load a level named dummy_wmb; this level is a simple hollowed cube with the 3 models placed inside it.

I have used a small light that was placed close to the floor, so the level looks completely dark, although the texture used for the cube isn't black.

Each model has this action attached to it:

action
rotate_model
{
my.skin = 1;
my.light = on;
my.lightred = 155;
my.lightgreen = 155;
my.lightblue = 155;
my.lightrange = 0;

Every model displays its first skin at game start; the models have their "light" flag set, so they will glow in the dark because of their lightred, lightgreen and lightblue values, even if their lightrange is set to zero.

     while (game_started == 0)
{
my.pan += 3 * time;
if (key_cuu ==
1)
{
while (key_cuu == 1) {wait (1);}
my.skin += 1;
if (my.skin == 4) {my.skin = 1;}
}
if (key_cud ==
1)
{
while (key_cud == 1) {wait (1);}
my.skin -= 1;
if (my.skin == 0) {my.skin = 3;}
}
if (key_cur ==
1)
{
while (key_cur == 1) {wait (1);}
camera.pan -= 40;
}
if (key_cul ==
1)
{
while (key_cul == 1) {wait (1);}
camera.pan += 40;
}

The while loop above will continue to run for as long as game_started remains zero. The models will rotate around their pan angle; if we press the up arrow key, we wait until the key is released and then we set the next skin for the model. The same thing happens when we press the "down" key; please note that we have to limit my.skin to 1..3 because every model has 3 skins.

If we press the right arrow key, we wait until it is released and then we subtract 40 degrees from camera.pan, in order to display the next model. Take a look at the picture below:

Our mission is simple: we have to rotate the camera with 120 degrees if we want to be able to see (and select) a new model. What is with those 40 degrees then? Well, we have 3 models, and each one of them has the action attached to it, so when we press the right arrow key, the camera is rotated with 40 + 40 + 40 = 120 degrees, get it? The same thing happens when we press the left arrow key (in a reversed direction, of course).

          camera.pan %= 360;
if (key_enter ==
1)
{
game_started = 1;
player_model = camera.pan / 120 + 1;
player_skin = my.skin;
start_game();
}
wait (1);
}
}

We need to limit camera.pan to 0...359 degrees (you'll see why in a little while) and then we check if the Enter key was pressed. If this is true, we set game_started to 1 in order to get out of this while loop and then we set player_model to 1, 2 or 3, depending on the angle of the camera (0, 120 or 240). Finally, we set a variable named player_skin to the number of the skin that was selected by the player, and then we call the function named start_game():

function start_game()
{
level_load (level_wmb);
wait (3);
if (player_model == 1)
{
player = ent_create
(cbabe_mdl, start_pos, player_moves);
}
if (player_model == 2)
{
player = ent_create
(guard_mdl, start_pos, player_moves);
}
if (player_model == 3)
{
player = ent_create
(hero_mdl, start_pos, player_moves);
}
}

This time we load the real level and then we create a player model depending on the value that was stored in player_model; the function that drives the model is named player_moves():

function
player_moves()
{
my.skin = player_skin;
while (1)
{
my.skill1 = 5 *
(key_w - key_s) * time;
my.skill2 = 0;
my.skill3 = 0;
my.pan += 3 * (key_a
- key_d) * time;
move_mode = ignore_passable
+ ignore_passents + glide;
ent_move(my.skill1,
nullvector);
if (key_w + key_s
> 0)
{
ent_cycle("walk", my.skill46);
my.skill46 += 5 * time;
my.skill46 %= 100;
}
camera.x = my.x;
camera.y = my.y;
camera.z = my.z
+ 200;
camera.tilt = -90;
wait (1);
}
}

The first line of code sets the correct player skin, depending on the value that was stored in player_skin; the player can move using the WSAD keys. If the player is walking (W or S are pressed), it plays its "walk" animation in a loop. The camera is placed 200 quants above player's origin and inherits its x and y. Take a look at the shot below; it was taken from the playable level.


Untitled

Radar

One good thing about the old raycasting engines was the you could easily have a radar (call it automap if you want to). This piece of code will allow you to see the whole map on a panel and more than that, what's moving on it.

First of all I have got these values (in Wed) for the level boundaries in the office.wmp level:

var level_minx = -1800;
var level_maxx = 1800;
var level_miny = -1500;
var level_maxy = 1500;

Feel free to choose approximate values because if you are using a small radar panel it won't matter than much if level_minx is -1800 or -1824.

The idea behind the radar code is pretty simple: we use a panel for the map (a screenshot of the top view in Wed, without unnecessary geometry on it) and several small panels with higher layers for the entities that move on the panel.

panel radar_pan
{
bmap = radar_map;
layer = 20;
pos_x =
0;
pos_y = 0;
flags = overlay, refresh,
d3d, visible;
}

panel
entity1_pan
{
bmap = pentity_map;
// the player
layer = 21;
pos_x =
0;
pos_y = 0;
flags = transparent,
overlay, refresh, d3d;
}
...............................................................................

panel entity5_pan
{
bmap = entity_map;
layer = 21;
pos_x =
0;
pos_y = 0;
flags = transparent,
overlay, refresh, d3d;
}

You can see that radar_pan (the level) has layer = 20 and the other panels (the entities) have layer = 21 so they will be displayed over radar_pan. I have only used 5 entity panels in this example, but you can add as many panels / entities as you want.

Every entity that wants to be displayed on the radar must include this line in its action / function (better make it the first line):

radarx_ptr = me; x = 1..5 in my example

The player has a radar1_ptr = me line in its player_prog action, the patrolling guard has a radar2_ptr = me line in its patrol_prog action and so on. Btw, the panel for the player (entity1_pan) uses a different bitmap (the player is blue and all the other entities are red on the radar).

We need to start the radar by running

function
init_radar()
{
while (1)
{
if (radar1_ptr != null)
{
entity1_pan.visible =
on;
entity1_pan.pos_x = scalex
* (abs(radar1_ptr.x - level_maxx));
entity1_pan.pos_y = scaley
* (abs(radar1_ptr.y - level_miny));
}
...........................................................................................................................

    if
(radar5_ptr != null)
{
entity5_pan.visible =
on;
entity5_pan.pos_x = scalex
* (abs(radar5_ptr.x - level_maxx));
entity5_pan.pos_y = scaley
* (abs(radar5_ptr.y - level_miny));
}
wait (1);
}
}

If the entity will appear on the radar (the radarx_ptr pointer to it isn't null) we make its panel visible and we calculate its position on the radar. Let's take a look at this line:

entity1_pan.pos_x = scalex * (abs(radar1_ptr.x - level_maxx));

You know that you can move a panel on the screen by changing its pos_x and pos_y parameters; this is what we are doing here. For radar1_ptr.x = -1800 and level_maxx = 1800:

scalex * (abs(radar1_ptr.x - level_maxx)) = length_of_the_radar_bitmap (128 pixels) -> scalex = 128 / 3600 = 0.0355

I could have measured the length of the bitmap (using bmap_width and bmap_height) and get rid of scalex and scaley buy these parameters allow you to do a "fine tuning" to the radar. I'm using a similar formula for the y axis so I won't explain it here.

Ok, I got your point but what if I have the same action for 5 monsters? I can't assign a single pointer to all of them!

I see your point but I have the answer. Check a different flag (set a different skill value) for every monster and set the pointers this way:

if (my.flag1 == 1) {radar3_ptr
= me;}
if (my.flag2 == 1) {radar4_ptr = me;}

This is what I did with the two elevators that appear in the office level (and on the radar, of course). I have zipped the doors.wdl and office.wdl that include all these changes, so you have a working example.


Redefining the keys

The ability to redefine the keys can be implemented even if you use A4 but it is much easier to do it with A5. I have created a standalone example that moves a wmb box in a small level; you can redefine the movement keys anytime you want to by clicking a small panel.

First of all we define some strings, vars, a panel (click on it to redefine the keys) and a text:

string
empty_str, "          ";
string up_str, "Move up";
string down_str, "Move
down";
string left_str, "Move left";
string right_str,
"Move right";
var left_key;
var right_key;
var up_key;
var down_key;
entity* player;

panel
redefine_pan
{
bmap redefine_map;
pos_x = 510;
pos_y = 10;
flags = refresh, overlay, visible;
on_click = change_keys();
}

text
redefine_txt
{
pos_x = 10;
pos_y = 10;
font = comic_font;
string = empty_str;
}

Function main is simple:

function
main()
{
fps_max
= 50;
d3d_panels = on;
load_level (<redeftst.wmb>);
wait
(2);
camera.x = 0;
camera.y = 0;
camera.z
= 490;
camera.tilt = -90;
mouse_map = pointer_map;
mouse_mode = 2;
while(1)
{
mouse_pos.x = pointer.x;
mouse_pos.y
= pointer.y;
wait(1);
}

}

We set the camera position and angle, we select the mouse pointer and we make it visible (mouse_mode = 2); function main ends with some code that moves the mouse. The action associated to the player contains a single line of code:

action set_player
{
player = me;
}

Every time we click the redefine_pan panel, change_keys is executed:

function
change_keys()
{
redefine_txt.visible = on;

redefine_txt.string = up_str;
while (key_any == 0) {wait(1);}
while (key_any == 1) {wait(1);}
up_key = key_lastpressed;
key_set(up_key, move_up);

redefine_txt.string = down_str;
while (key_any == 0) {wait(1);}
while (key_any == 1) {wait(1);}
down_key = key_lastpressed;
key_set(down_key, move_down);

redefine_txt.string = left_str;
while (key_any == 0) {wait(1);}
while (key_any == 1) {wait(1);}
left_key = key_lastpressed;
key_set(left_key, move_left);

redefine_txt.string = right_str;
while (key_any == 0) {wait(1);}
while (key_any == 1) {wait(1);}
right_key = key_lastpressed;
key_set(right_key, move_right);

redefine_txt.visible = off;
}

Let's take a good look at the code for the "Move up" key - the code for all the other keys is similar:

redefine_txt.visible = on;
Makes the text visible

redefine_txt.string = up_str;
Displays the "Move up" string

while (key_any == 0) {wait(1);}
Waits until we press a key

while (key_any == 1) {wait(1);}
Waits for release

up_key = key_lastpressed;
Stores the last pressed key in the up_key variable

key_set(up_key, move_up);
Assigns function move_up to the key

I have used a simple move_up function, without collision detection. You don't want to use a function like that in your game.

function
move_up()
{
while (key_pressed(up_key) == 1)
{
player.x += 10
* time;
wait (1);
}
}

As long as the key stored in up_key is pressed, the player (the box) is moved to the right. The functions that are used to move the box down, left and right are similar. Make sure that the functions associated to the player are using a single key (you need an action that moves the player to the right, another one that moves the player to the left and so on).


Untitled

Robby the robot

Who is Robby? Well, it is your everyday robot. It's an npc but it can add atmosphere to your game. What does it do? Not much, but it does it well. It waits for a few seconds then it walks for a few seconds... Have I mentioned that it can detect and avoid any obstacle and - even more - that it can't get stuck?

Please load and run the demo level right now. I would say that the code is pretty impressive considering the fact that it uses only one action and two tiny functions, but who would believe me? Let's take a look at the action attached to Robby:

action
robby_the_robot
{
my.enable_entity = on;
my.enable_impact = on;
my.enable_block = on;
my.event = robby_event;
ent_create(antenna_pcx, nullvector, check_front);

The robot is sensitive to other entities, to impact and to level blocks; if one of these events happen, robby_event will run. We create a virtual "antenna" - I have used two antennas similar to this for the first AI demo in Aum4.

     while(1)
{
standing_time =
3 + random(7);
while (standing_time
> 0)
{
standing_time -= time / 16;
ent_cycle("stand", my.skill20);
my.skill20 += 2 * time;
my.skill20 %= 100; // loop
wait (1);
}

Robby uses a while (1) loop so it will repeat whatever
he's doing all day long. We generate standing_time = 3...10 seconds - it is the
time spent by Robby while playing its "stand" animation in a while loop. Have
you noticed that we're subtracting time / 16 from standing time every frame? This
way we can subtract a second from standing_time every... second. This is pretty
cool because we can work with "real" values, we don't have to guess how many ticks
would that be and so on.

walking_time = 10 + random(10);
walk_speed.x = 1;
walk_speed.y = 0;
walk_speed.z = 0;
walk_speed *= time;
while (walking_time > 0)
{
if (content(front_coords) == content_solid)
{
my.skill40 = my.pan;
my.skill41 = 30 + random(90);
while (my.pan < my.skill40 + my.skill41) // rotate 30..120 degrees
{
my.pan += 3 * time;
wait (1);
}
}

The robot got out of the first while loop that was used for "stand"; now it is the time to walk for 10...20 seconds - that's the value that will be set for walking_time and the speed will be given by walk_speed. We're setting only the coordinate on x so the robot will move in the direction given by its current pan angle. Do you remember the antenna? It is the red sprite in the demo but you can remove the comment in the wdl file and it will disappear. This antenna is placed 30 quants in front of the robot (trust me on that, we'll take a look at its function right away). If the antenna detects content_solid (walls, wmb entities, etc) the robot stores its current pan angle in skill40 and changes its pan value by adding a random value (30...120) to it.

else
{
walking_time -= time / 16;
ent_cycle("walk", my.skill20);
my.skill20 += 4 * time;
my.skill20 %= 100; // loop
move_mode = ignore_passable;
result = ent_move (walk_speed, nullvector);
if (result == 0) // got stuck!
{
walk_speed.x *= -1; // reversed movement
my.skill40 = my.pan;
my.skill41 = 30 + random(90);
while (my.pan < my.skill40 + my.skill41) // rotate 30..120 degrees
{
my.pan += 3 * time;
ent_cycle("walk", my.skill20); // play reversed "walk" animation
my.skill20 -= 4 * time;
my.skill20 %= 100; // loop
move_mode = ignore_passable; // ignore passable entities
ent_move (walk_speed, nullvector);
wait (1);
}
walk_speed.x *= -1; // restore walk_speed
}
}
wait (1);
}
wait (1);
}
}

If the antenna hasn't found
content_solid, the robot should be able to move forward so it plays its "walk"
animation in a loop and moves. Take a close look at this line:

result = ent_move (walk_speed, nullvector);

It looks "normal", isn't it? The good news is that result will hold the distance covered by Robby so if result is zero, it means that Robby got stuck and we should do something about it. So what do we do if Robby gets stuck? We reverse walk_speed by multiplying it with -1, we change its pan angle by adding a random value (30...120 degrees) to it, we play a reversed "walk" animation (notice the line with -= 4 * time) and we move the robot backwards. If result isn't zero (Robby isn't stuck anymore) we multiply walk_speed with -1 again in order to restore the original walk_speed value.

Time
to take a look at those two tiny functions I've told you about:

function check_front()
{
my.passable = on;
my.transparent = on;
while (1)
{
my.x = you.x +
30 * cos(you.pan); // the antenna is placed
my.y = you.y +
30 * sin(you.pan); // 30 quants in front of the player
my.z = you.z;
vec_set (front_coords,
my.pos);
wait (1);
}
}

Function check_front is the function associated to the "antenna"; it makes the red sprite, passable, transparent and places it 30 quants in front of the robot, all the time. But this function does one more important thing: sets the var named front_coords to the coords of the red sprite. If you were here when we've talked about the action associated to the robot, you know that we check the content of front_coords to see if the way is clear or not.

Maybe you are wondering: if we have that mighty antenna, why bother with result = 0 and so on? Well... life isn't always fair. Robby could get into a situation like the one shown in the picture below:

You can see that the antenna thinks that everything is ok but Robby got stuck near the corner of the box.

function
robby_event()
{
if (event_type == event_entity)
{
snd_play (sorry_wav,
50, 0);
}
}

The last function tells Robby if it has collided with an entity or not. I have thought that it would be nice if Robby would say "Sorry!" when it runs into the player. This simple event plays a sound; the code that makes Robby avoid other entities (including the player) is based on result = 0 and we've already discussed it.

I hope that you will have fun with this project - I did.


Untitled

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.


Untitled

Rpg movement

There comes a time in a man's life when he needs to prove that he is a real man. And then he grabs his trusty mouse and starts killing hordes after hordes of monsters, eager to prove that he isn't just a dreamer, but a poet, a guitar player and a... oh, yeah, and a mighty warrior.

I wanted to create a standalone project; this way you will be able to add more stuff to it easily. What will you get? A player that moves using gravity, plays its "walk" and "stand" animation sequences and moves to the target indicated by clicking on the map with the left mouse button. You can choose any angle and position for the camera because it won't matter; in fact, I have decided to use a freely movable camera to prove that the target is set correctly no matter what.

function main()
{
level_load (rpg1_wmb);
mouse_map
= pointer_pcx;
mouse_mode = 2;
camera.x = player.x;
camera.y = player.y;
camera.z = 200;
camera.pan = 90; // initial
pan angle
camera.tilt = -60;
while (1)
{
mouse_pos.x = pointer.x;
mouse_pos.y
= pointer.y;
if ((mouse_pos.x < 2) &&
(camera.x > -2500)) {camera.x -= 10 * time;}
if
((mouse_pos.x > screen_size.x - 2) && (camera.x < 2500)) {camera.x
+= 10 * time;}
if ((mouse_pos.y > screen_size.y
- 2) && (camera.y < 2500)) {camera.y -= 10 * time;}
if ((mouse_pos.y < 2) && (camera.y > -2500)) {camera.y += 10 * time;}
wait (1);
}
}

Function main loads the level, sets pointer_pcx as the mouse pointer, shows the pointer and then it sets the initial x, y, z, pan and tilt for the camera. You can play with these initial values until your game looks great (tip: choose a huge camera.z value to mask those horrible looking models :)

The code inside the loop updates the position of the mouse every frame and creates a freely movable camera. This camera will move from -2500 to 2500 quants on x and y. I've got these values in Wed as level boundaries but you can use any other values here, depending on the size of your level.

Let's discuss the first picture: if we move the mouse at the left side of the screen (mouse_pos.x < 2) and if camera.x > -2500 (left boundary on x), we subtract 10 * time from camera.x, moving it to the left. The things work the same way when we move the mouse pointer towards the right side of the screen, upwards or downwards.

Time to take a look at the action that controls our player:

entity* destination;

action warlock
{
player = me;
while (1)
{
my.skill1 = 5 * time;
my.skill2 = 0;
vec_set (temp, my.x);
temp.z -= 3000;
trace_mode =
ignore_you + ignore_passable + use_box;
my.skill3
= -trace (my.x, temp);

Please note that I have defined a pointer named destination; every time the player clicks a point on the map, we will assign "destination" to the target entity (the red potion that appears close to the mouse pointer). The player moves using its skills1 (which gives the speed) and skill3 (used for gravity).

      if (destination != null)
{
vec_set (temp.x, destination.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp);
my.tilt = 0; // don't bow :)
if (vec_dist (my.x, destination.x)
> 40)
{
move_mode = ignore_passable;
ent_move (my.skill1, nullvector); // moves using skill1..3
ent_cycle("walk", my.skill46);
my.skill46 += 10 * time; // "walk" animation speed
my.skill46 %= 100; // loop animation
}
else
{
destination.invisible = on;
ent_cycle("idle", my.skill46); // play "stand" frames animation
my.skill46 += 2 * time;
// "stand" animation speed
my.skill46 %= 100; // loop animation
}
}
wait
(1);
}
}

If the pointer named "destination" isn't null the player rotates towards "destination" (the red potion); this rotation would change its tilt angle too, so the next line of code resets the tilt value to zero. If the distance between the player and the red potion is below 40 quants, the player will move ignoring all the passable objects and it will play its "walk" animation in a loop. If the distance between the player and "destination" is smaller than or equal to 40 quants ("else") the red potion becomes invisible; the player model will play its "stand" animation in a loop until the player presses the left mouse button again.

on_mouse_left = set_target;

Every time we press the left mouse button, function set_target will run. Let's pretend for a moment that somebody could take a picture of you while you are playing with this demo from within the level.

I didn't know that you are so young!

The thick blue line is the very surface of your monitor. You have clicked the left mouse button somewhere on the screen and now you expect the warlock to move to that position in the 3D world! How does this stuff work? Let's get back to function set_target.

function
set_target()
{
var pos1;
var pos2;
pos1.x = mouse_pos.x;
pos1.y = mouse_pos.y;
pos1.z = 0;
vec_for_screen (pos1, camera);
pos2.x = mouse_pos.x;
pos2.y = mouse_pos.y;
pos2.z = 20000;
// use a big value here
vec_for_screen (pos2, camera);
trace
(pos1, pos2); // now "target" holds the coordinates of the hit point
destination = ent_create (destination_mdl, target, show_target);
}

I have defined two local variables that will be set this way:

- pos1 will store the position of the mouse as a 3D world coordinate that is placed close to the surface of your monitor;

- pos2 will store the position of the mouse as a 3D world coordinate that is placed 20,000 quants in front of player's eyes.

That's what vec_for_screen does! You give it the x, y position in the 2D world and the depth and it returns a position in the 3D world. Our function traces between pos1 and pos2 in order to get the "target" coordinates. Target is a predefined vector that is set automatically to the 3D world coordinates where trace (pos1, pos2) hits the ground, as indicated by the green arrow in the picture above.

The last line in the function creates the "destination" entity at the position given by "target" and attaches it the show_target() function:

function
show_target()
{
my.passable = on;
while (mouse_left
== 1) {wait (1);}
while (mouse_left == 0) {wait (1);}
ent_remove (me);
}

This small function makes the target entity (the red potion) passable and then it waits until the player releases the left mouse button. As soon as the player presses the left mouse button again (a new target is set) the old target entity is removed.

You can (and you should) combine the code in this article with Aum12's sword combat code to get a good starting point for your game.


Stratego: RTS and TBS template

First of all, RTS stands for Real Time Strategy and TBS means Turn Based Strategy, but I guess you knew that already. I have created a game demo that will get you started if you plan to create a strategy game. The demo allows you to:
- move the camera;
- create two types of buildings; one produces soldiers, the other one produces nothing (can be used for technology upgrades?)
- move the units using a primitive path finding system.

I have set the screen resolution to 800x600 pixels and I have set mouse_range to 3000 just to make sure that I can touch and click anything on screen (default = 1000). I'm also using

var walk_speed = 2; // soldier speed
var animation_speed = 6; // soldier's animation speed.

My main function is simple (as always):

function
main()
{
D3D_PANELS = ON;
LOAD_LEVEL (<strat1.wmb>);
move_camera(); // new stuff here
}

Let's take a look at move_camera:

function move_camera()
{
level_marginx = 200; // scroll the camera from -200 to 200
level_marginy = 200; // on X and Y
CAMERA.ARC = 70;
CAMERA.X = 0; // build your levels with the starting point in the origin
CAMERA.Y = 0; // because at game start the camera will point to the origin
CAMERA.Z = camera_height; // change camera_height to get closer or farther to the ground
CAMERA.PAN = 90; // set the camera to look down.
CAMERA.TILT = -90;
CAMERA.ROLL = 0;

MOUSE_MAP = cursor_map; // choose the cursor and make it appear in the center of the screen
MOUSE_POS.X = SCREEN_SIZE.X / 2;
MOUSE_POS.Y = SCREEN_SIZE.Y / 2;
MOUSE_MODE = 2;

WHILE (1)
{
MOUSE_POS.X = POINTER.X; // mouse movement;
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);
}
}

The camera movement
code works this way:

IF (MOUSE_POS.X < 1 && CAMERA.X > level_marginx * (-1))
{
CAMERA.X -= 10 * TIME;
}

If the mouse moves to the left and reaches the left edge (MOUSE_POS.X < 1) and we can move the camera (CAMERA.X > -200) then we move the camera to the left (scroll left). The other 3 IF statements look and work in the same way; we're using SCREEN_SIZE.X and SCREEN_SIZE.Y to get the size of the screen in pixels for the right and bottom edge. We could have used numbers because we already know that we're running in 800x600 pixels, but this way our game is prepared for serious strategy games; the code can read the resolutions and adapt the camera movement automatically. Don't forget that the Y axis is reverted, so if we want to scroll upwards we have to reduce CAMERA.Y to make it work.

We are adding a panel that creates buildings if it is clicked and plays a nice sound when we're over the buttons using the mouse_over function:

PANEL
main800_pan
{
BMAP main800_map;
LAYER 20;
POS_X 0;
POS_Y 0;
BUTTON 4, 4,
building1_map, building1_map, building11_map, create_building1, NULL, mouse_over;
BUTTON 4, 65, building2_map, building2_map,
building22_map, create_building2, NULL, mouse_over;
FLAGS OVERLAY, REFRESH, TRANSPARENT,
VISIBLE;
}

If we click the building1 button, it will trigger the create_building1 function:

function create_building1()
{
WHILE (MOUSE_LEFT ==
1 || MOUSE_RIGHT == 1) {WAIT (1);} // wait for mb release
WHILE (1)
{
IF
(MOUSE_LEFT == 1)
{
build1_pos.X = MOUSE_POS.X;
build1_pos.Y = MOUSE_POS.Y;
build1_pos.Z = camera_height; // place buildings at ground position
vec_for_screen (build1_pos,CAMERA);
CREATE (<build1.wmb>, build1_pos, build1_action);
END;
}
IF
(MOUSE_RIGHT == 1) {END;} // abort if right mouse button is pressed
WAIT
(1);
}
}

If you click the building1 button, you could trigger several events, so this line

WHILE (MOUSE_LEFT == 1 || MOUSE_RIGHT == 1) {WAIT (1);}

waits for the left mouse button release, then all the stuff goes on. If the lmb is clicked again, we're getting the mouse pointer coordinates in build_pos, converting them to world coordinates with vec_for_screen. We create the building1 (build1.wmb) at pointer position and make it run the build1_action:

function build1_action()
{
MY.PUSH = 20;
// units (soldiers) have PUSH = 10, building have PUSH = 20
WHILE (MOUSE_LEFT
== 1) {WAIT (2);} // don't create units by mistake
MY.ENABLE_SCAN
= ON; // SCAN - sensitive
MY.ENABLE_CLICK = ON;
MY.EVENT = build_stuff1;
// create units when clicked
scan_area.PAN = 360;
scan_area.TILT
= 60;
scan_area.Z =
900;
vec_set (MY_POS,
build1_pos);
SCAN MY_POS, MY_ANGLE, scan_area;
IF (RESULT >
0 && RESULT < 200) // buildings must be separated by 200 quants or
more
{
PLAY_SOUND cantplacehere_snd, 100;
REMOVE ME;
}
ELSE
{
PLAY_SOUND buildingplaced_snd, 100;
}
}

Building1 is sensitive to SCAN and CLICK. When it is placed on the ground, it will start scanning around it. If another building was found nearby (RESULT > 0 means that SCAN has found another building) and it is closer than 200 quants, the newly created building is instantly removed. If there isn't any buiding in the area, the building succedes and a buildingplaced_snd sound is played.

What happens if any existing building is clicked? It's build_stuff1 event is triggered:

function build_stuff1()
{
IF (EVENT_TYPE == EVENT_CLICK)
// make sure it's a click
{
IF (MY.SKILL10
> 2)
{
END; // 8 positions filled, so stop creating units
}
IF (MY.SKILL9 ==
1 && MY.SKILL10 == 1) // exclude center of the building
{
MY.SKILL9 += 1;
}
temp.X = unitstartxpositions[MY.SKILL9]
+ MY.X;
temp.Y = unitstartxpositions[MY.SKILL10]
+ MY.Y;
temp.Z = MY.Z;
CREATE (<guard.mdl>,
temp, unit1_action);
PLAY_SOUND unitcreated_snd,
50;
MY.SKILL9 += 1;
IF (MY.SKILL9 >
2)
{
MY.SKILL9 = 0; // next row
MY.SKILL10 += 1;
}
}
}

Every building can create up to 8 units (soldiers). We have a few arrays to help us do that:

var unitstartxpositions[3] = -65, 0, 65; // stores unit offsets related to the wmb center
var unitstartypositions[3] = -65, 0, 65; // we aren't using 0,0 (building center)

The idea is to place the building1 wmb on the ground; these arrays will help us place the units - look at this picture

We can place 8 units around the building (3 rows and 3 columns, but we're excluding the center). The figures in the picture are the offsets of the units related to the building. I'm using SKILL9 and 10 for these offsets (the first 8 skills can be accessed in Wed and I thought I should keep them free for your use). When the building is clicked for the first time, SKILL9 and SKILL10 are 0, so the first soldier will have an offset given by unitstartxpositions[0] = -65 and unitstartypositions[0] = -65. The building has the SKILL9 increased by 1 at the second click (second soldier), so the unit will have the offset (0, -65). These things go on and on (excluding (0,0)) until all the units (8 soldiers) have been created. Finally, the most complex function - move_unit - is going to be exposed:

function move_unit ()
{
MY.PUSH = 10; // lower push for units
IF (EVENT_TYPE == EVENT_ENTITY) // collision with buildings or other units
{
// caveman's path finding code
EXCLUSIVE_ENTITY;
MY.PAN = RANDOM (360);
WAITT (10); // wait before setting the target again
temp.X = MY.SKILL12; // stored mouse pointer coordinates
temp.Y = MY.SKILL13;
temp.Z = 0;
vec_sub (temp, MY.X);
vec_to_angle (MY.PAN, temp); // rotate unit towards the target again
// remove the lines above if you want to add your path finding code
}
IF (EVENT_TYPE == EVENT_CLICK)
{
MY.SKILL11 = 0;
WHILE (MOUSE_LEFT == 1) {WAIT (2);} // wait for lmb release
WHILE (MY.SKILL11 == 0) // wait for the target to be set (lmb click)
{
IF (MOUSE_LEFT == 1)
{
EXCLUSIVE_ENTITY; // stop parallel running actions for this unit
MY.SKILL11 = 1; // target has been set
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.SKILL12 = temp.X; // store pointer's coords before they get lost
MY.SKILL13 = temp.Y;
vec_sub (temp, MY.X);
vec_to_angle (MY.PAN, temp); // rotate unit towards mouse
MY.SKILL15 = 500; // maximum path length = fuel :)
WHILE (abs(MY.X - MY.SKILL12) + abs(MY.Y - MY.SKILL13) > 3 && MY.SKILL15 > 0) // stops near the mouse pointer
{
move_mode = ignore_you + ignore_passable;
ent_move (walk_speed, nullvector);
ent_cycle("walk", MY.SKILL14);
MY.SKILL14 += animation_speed * TIME;
IF (MY.SKILL14 > 100) {MY.SKILL14 = 0;}
MY.SKILL15 -= TIME; // burn fuel
WAIT (1);
}
ent_cycle("stand", 0); // set the idle frame
}
WAIT (1);
}
}
}

We'll ignore IF (EVENT_TYPE == EVENT_ENTITY) for now; this happens only if the soldier collides with one of the buildings or other soldiers. To move the soldiers, we click on them (producing an EVENT_CLICK), then we want to click somewhere on the map and we expect to unit to move there. I'm using soldier's SKILL11...SKILL15 in this function; I could have used SKILL9 and 10 because they aren't connected with SKILL9 and 10 used in the building functions, but I wanted to make the things as clear as possible.

When we click the unit, its SKILL11 is set to zero and it will stay this way until we click on the map to set the target. If we have set the target, temp is used to get the mouse pointer coordinates; we're converting them to world positions with vec_for_screen and we're safely storing them in SKILL12 and 13 because temp can suddenly change its value; we already know unit's Z coordinate, so we don't need to store it.

The unit must rotate towards the mouse then it will start moving, trying to reach its target. My path finding code is really simple, so the unit can "think" too much before finding the right path; this is why I have added "fuel". If a unit can't find its correct path, it will move towards the target until it burns all its fuel

WHILE (abs(MY.X - MY.SKILL12) + abs(MY.Y - MY.SKILL13) > 3 && MY.SKILL15 > 0)

will make the unit stop when it reaches the target or when SKILL15 = 0 (it ran out of fuel). I'm using a simple method to check if the unit has reached the target; please remember that SKILL12 and SKILL13 are holding target's coordinates.

The unit moves towards the target and the "blue" differences become smaller and smaller. When their sum is smaller then 3, the unit stops.
I have used the new guard.mdl file; it walks with walk_speed set at the beginning and is animated using these lines:

ent_cycle("walk", MY.SKILL14);
set the animation frames to the first walk frame (MY.SKILL14 = 0 for now, so the guard is set at the first walking frame)

MY.SKILL14 += animation_speed * TIME;
go through all the walk animation frames; SKILL14 contains the current walk frame

IF (MY.SKILL14 > 100) {MY.SKILL14 = 0;}
loop the animation

If the unit has stopped (reached the target or ran out of fuel), it switches to stand mode

ent_cycle("stand", 0); // set the idle frame = first stand frame

Did I say this was the latest function? We've got the path finding code to discuss, but it is really simple:

IF (EVENT_TYPE == EVENT_ENTITY) // collision with buildings or other units
{
// caveman's path finding code
EXCLUSIVE_ENTITY;
MY.PAN = RANDOM (360);
WAITT (10); // wait before setting the target again
temp.X = MY.SKILL12; // stored mouse pointer coordinates
temp.Y = MY.SKILL13;
temp.Z = 0;
vec_sub (temp, MY.X);
vec_to_angle (MY.PAN, temp); // rotate unit towards the target again
// remove the lines above if you want to add your path finding code
}
.................................................................................

If the unit collides with something, it picks a random direction and moves for a little more than 1/2 second, then it "reads" the right direction from SKILL12 and 13 and tries to reach the target again.

There are many things that you should add to this code to make it perfect ;)
- Better path finding code;
- The buildings can't be placed one over the other, but you can place buidings on top of isolated units and that's wrong. I wouldn't use SCAN for every unit because it would slow down the game in theory (I did that with the buildings, but there aren't too many of them in a level). I would store all the unit positions in an array and I would compare building's coordinates with all the unit coordinates stored in the array. The array could be used to improve the path finding algorithm, too.
- Etc.

Please note that I have used a regular model with 644 polygons for the soldier. I wouldn't do that if I were you - with 100 models visible on screen at the same time, you'll slow down the engine and you don't want that.


SCRAP - SCReen cAPture code

If you are like me, you have looked at the METAL movie in "Hot features" and you're wondering: how did he do that movie? He must have used two PCs and a great capture card... This is a good method and I have successfully used it for my first Takeover movies, but then I thought that I should use a single PC and I came up with this simple scrap code.

First of all, I'd like to say that I heard somebody at the forum speaking about this and it inspired me (I can't find the post anymore, maybe it got deleted by mistake). The idea was to use SCREENSHOT every time the player was moving and save the shots on the hard drive, then assemble a movie using the screenshots. Scrap works in a different way, but uses the same idea.

You'll have to INCLUDE scrap.wdl in your game file; don't forget to insert scrap_movie(); in main to call the scrap function,

var
Count = 11000; // just a number bigger than 10000
var
Count1 = 0;
var Count2 = 0;
var Count3 = 0;
var
Count4 = 0;
var Count5 = 0;
var Count6 = 0;
var
Count7 = 0;
var Count8 = 0;
var Count9 = 0;

function
scrap_movie ()
{
WHILE (1)
{
IF (Count
== 11000 && KEY_M == 1) {WAIT (2); Count = 0;}
WHILE (KEY_M
== 1) {WAIT (1);}
WHILE (count
< 10000)
{
Count1 = Count - 1000;
Count2 = Count - 2000;
Count3 = Count - 3000;
Count4 = Count - 4000;
Count5 = Count - 5000;
Count6 = Count - 6000;
Count7 = Count - 7000;
Count8 = Count - 8000;
Count9 = Count - 9000;

IF (Count < 10) {screenshot "Mv000",Count;}
IF (Count >= 10 && Count < 100) {screenshot "Mv00",Count;}
IF (Count >= 100 && Count < 1000) {screenshot "Mv0", Count;}
IF (Count >= 1000 && Count < 1010) {screenshot "Mv100", Count1;}
IF (Count >= 1010 && Count < 1100) {screenshot "Mv10", Count1;}
IF (Count >= 1100 && Count < 2000) {screenshot "Mv1", Count1;}

IF (Count >= 2000 && Count < 2010) {screenshot "Mv200", Count2;}
IF (Count >= 2010 && Count < 2100) {screenshot "Mv20", Count2;}
IF (Count >= 2100 && Count < 3000) {screenshot "Mv2", Count2;}

..........................................................................
// repeats for Count = 3000...8000

IF (Count >= 9000 && Count < 9010) {screenshot "Mv900", Count9;}
IF (Count >= 9010 && Count < 9100) {screenshot "Mv90", Count9;}
IF (Count >= 9100 && Count < 10000) {screenshot "Mv9", Count9;}

Count += 1;
IF (KEY_M == 1) {WAIT (2); Count = 11000;}
WHILE (KEY_M == 1) {WAIT (1);}
WAIT (1);
}
WAIT (1);
}
}

We're using Count to start or stop the movie when the "M" key is pressed; if Count is smaller than 10000, the shots are saved on your hard drive; to stop this process, we're setting Count to 11000 (any number greater than 10000 is a valid Count number).

SCREENSHOT can save up to 999 shots so if your movie is going to have 25 fps, you're limited to 999 / 25 = 40 seconds. I have added a few variables (Count1...Count9) that are used to expand the screenshots to 9999 shots = 7 minutes with 25 fps.

When you press M, you'll notice a slowdown - that's normal because the shots are saved on your hard drive. When you want to finish the capture, press M again. Exit the game and move to the shots folder - you can create one like this:

SAVEDIR "C:\\SHOTS";

If you don't use a line like this in your main file, the shots will be saved in your working folder, mixing with the rest of your files.

Please take a look at the shots; their names start with mv0000.pcx and can continue to mv3201.pcx for example. Use any program that can convert pcx files to movies (Animation Shop that comes with Paint Shop Pro is a good example), then save the movie as avi, mpg, etc.

Use small resolutions for your movies; they'll look great anyway because they're ran in small windows, not in full screen. I use 320x200 all the time and this is the way I've created the METAL movie, too. Just change the screen resolution to 320x200 in your game, then press M to start the capture. If you've seen some game preview movies, you've noticed they all come in small sized windows.

I ran a test on two of my computers: Athlon 900 MHz, 60 GB Quantum Fireball Plus AS 7200 rpm and Athlon 750 MHz, 13 GB Quantum Fireball 4400 rpm. Scrap saves around 25 fps on the system with the fast hard drive and 15 fps for the second PC. If you get more than 8 fps, you can create movies - the movies on my site have 15 fps and a decent size.

If you have a good processor with a fast hard drive, you can create 400x300 pixels movies, but their size is too big unless you make a 2 second movie. Intel PIII 1000 MHz with 60GB QFB Plus AS 7200 rpm will deliver 15 fps in 400x300.


Untitled

Scrolling panel

Sometimes you want to be able to scroll some text printed on a panel but you can't simply change its pos_x and pos_y parameters because you don't want a huge panel to move up and down across the screen. You need something smaller, something like a window in the panel. Ok, so let's use one:

First of all we define a panel:

panel
spanel_pan
{
bmap = spanel_pcx;
pos_x = 0;
pos_y = 0;
layer = 30;
button 217, 25, arrow1_pcx, arrow1_pcx, arrow1_pcx, scroll_up, null, null;
button 217, 45, arrow2_pcx, arrow2_pcx,
arrow2_pcx, scroll_down, null, null;
window = 10,
20, 200, 50, text_pcx, text_pos.x, text_pos.y;
flags = overlay, d3d, refresh;
}

This is a common panel definition; we have two arrow - shaped buttons that will be used to scroll the text up and down and a window definition. Let's take a closer look at it:

window = 10, 20, 200, 50, text_pcx, text_pos.x, text_pos.y;

This instruction creates a window (a hole) in the panel. The window will be placed at (10, 20) and will have 200 pixels on x and 50 pixels on y. The text that will appear in this window is printed on text_pcx; its coordinates are given by text_pos.x and text_pos.y

function
scrolling_text()
{
mouse_toggle(); // show the cursor
text_pos.x = 10;
text_pos.y = 20;
while (1)
{
if (key_h == 1) //
press H to show the panel
{
while (key_h == 1) {wait
(1);}
spanel_pan.visible
= (spanel_pan.visible == off);
}
wait (1);
}
}

Function scrolling_text is called in main; it shows the cursor using a template function (mouse_toggle) and then sets the starting position for the text that will appear in the window. The while loop shows the panel or hides it when we press the "H" key, using a single line of code. Try to understand how this line works:

spanel_pan.visible = (spanel_pan.visible == off);

and you'll be a step closer to the yellow belt :)

The last functions are:

function
scroll_up()
{
while (mouse_left
== 1)
{
if (text_pos.y <
350) // text height - window height
{
text_pos.y += 3 * time;
}
wait (1);
}
}

function
scroll_down()
{
while (mouse_left
== 1)
{
if (text_pos.y >
0)
{
text_pos.y
-= 3 * time;
}
wait (1);
}
}

These functions run when we press one of the arrows that make the text scroll. If we press one of the arrows, the text scrolls in the corresponding directions as long as the left mouse button is pressed. If the text has reached its upper / lower limit, it can't be scrolled anymore. Have fun but don't forget that any panel uses video memory; try to use a decent width / height for every panel.


Untitled

Security cameras

This tiny article will show you how easy it is to implement a security camera. We will use a camera that scans for the player but before I proceed let's see a picture of scan_entity in action. This precious instruction will scan all the entities within a given sector. We can set the scanning angle horizontally and vertically; we can set the scanning distance as well:

I have created a security camera using a microscope model; don't mess with a programmer when he is looking for models :)
The action attached to the camera is security_camera:

action security_camera
{
my.skill1 = my.pan;
init_alarm();
while (1)
{
while (my.pan <
my.skill1 + 60)
{
my.pan += 1 * time;
if (random(1) > 0.9)
{
start_scanning();
}
wait (1);
}
while (my.pan >
my.skill1 - 60)
{
my.pan -= 1 * time;
if (random(1) > 0.9)
{
start_scanning();
}
wait (1);
}
wait (1);
}
}

We store the initial pan angle for the camera in skill1, we call the function init_alarm (more on that a little later) and then we rotate the camera 60 degrees around its starting pan angle, to the left and to the right - that's a 120 degrees scanning angle. Scan_entity is an instruction that eats resources so we shouldn't call it every frame. One of the solutions would be to replace wait (1) with waitt (3) or something like that but the movement would become choppy. There are several methods to fix this problem; the easiest method is to generate a random number and when it is bigger than a certain value (0.9 in my example) we call the function that does the scanning. You can adjust the rotation speed by changing 1 * time.

Ok, now we've got a random number every frame; when its value is over 0.9 (btw, this happens a few times a second) function start_scanning will run:

function
start_scanning()
{
temp.pan = 45;
temp.tilt = 90;
temp.z = 700;
scan_entity(my.x, temp);
if ((result > 0) &&
(result < 500))
{
my.light = on;
my.lightred = 250;
my.lightgreen =
0;
my.lightblue =
0;
my.lightrange =
0;
wait (2);
my.light = off;
alarm = 1;
}
}

We set the horizontal scanning angle to 45 degrees, the vertical scanning angle to 90 and the scanning distance to 700 quants. Scan_entity will set result to the distance between the camera and the closest entity, if the entity is closer than 500 quants. If the camera has detected the player, it will change its color to red for 2 frames and - more important - the var named alarm will be set to 1.

I have promised to talk about function init_alarm a little later so here it is:

function init_alarm()
{
while (alarm == 0) {wait (1);}
snd_loop(alarm_wav, 100, 0);
}

This function was called at the beginning of the game and it has waited for alarm to become 1. As soon as this happens, an alarm sound is played in a loop. This is the place where you should alert the enemies, close some doors, shut down the engine... whatever you want to do.


Untitled

Shooting switches / statues

This piece of code will show you how you can move to the next level only if you have destroyed a certain number of idols / statues / switches / whatever you want.

action statue
{
my.enable_shoot
= on;
my.event = destroy_me;
statues_left += 1;
while (1)
{
if (vec_dist(my.x, player.x)
< 50 || my.skill10 == 1)
{
play_sound
(destroyed_snd, 70);
statues_left
-= 1;
_gib
(10);
ent_remove
(me);
return;
}
wait (1);
}
}

Every statue is sensitive at shooting; their number is counted at startup. If the player comes close to a statue (or shoots it) a sound is played, the number of statues is decreased, the statue breaks in pieces and disappears.

What about shooting? Here's the event that is triggered when the statue is shot:

function destroy_me()
{
if (event_type == event_shoot)
{
my.skill10 = 1;
}
else
{
my.skill10 = 0;
}
}

It doesn't look like having something to do with the statues, isn't it? If you look at action statue above you'll see that the statue is destroyed if the player comes close to it OR if skill10 = 1 - that's what the event does.

We must have another entity that takes us to the next level on impact:

action next_level
{
my.enable_impact
= on;
my.event = change_level;
}

When we impact with this entity, its change_level event will be triggered:

function change_level()
{
if (statues_left
!= 0) {return;}
me = null;
beep; beep;
// load_level <next.wmb>;
wait (2);
}

You can see that we can't move to the next level if we haven't destroyed all the statues. You will have to remove the comment for the load_level line and change the name to your_next_level.wmb


Untitled

Skeletons Inc.

I'm not sure about you, but I feel pity for the old skeleton trying to find its way out... What, you haven't played Sk. Inc. yet?

The goal of this game is to get enough points in order to move to the next level (pretty original, huh?). The number of lives and points and their corresponding texts are displayed with the code below:

panel
skinc_pan // displays the numbers
{
pos_x = 0;
pos_y = 0;
digits = 120, 575, 4, skinc_font,
1, lives;
digits = 650, 575, 4, skinc_font, 1, points;
flags = refresh, visible;
}

text
skinc_txt // displays "Lives:" and "Points:"
{
pos_x = 0;
pos_y = 575;
font = skinc_font;
string = skinc_str;
flags = visible;
}

Function main is as simple as possible:

function
main()
{
level_load (skinc_wmb);
wait (2); // wait for the level to be loaded
clip_size = 0; // show all the triangles for
all the models
fps_max = 30; // lock the
frame rate to 30 fps
}

The camera is attached to an arrow.mdl placed in the level, using the code below:

action init_camera
{
my.invisible = on;
while (player == null) {wait (1);}
vec_set (camera.pos, my.pos);
while (1)
{
camera.arc = max
(10, (camera.arc - 2 * time * key_equals)); // press "+" and "-"
camera.arc = min
(70, (camera.arc + 2 * time * key_minusc)); // to zoom in  / out
vec_set (temp.x,
player.x);
vec_sub (temp.x,
my.x);
vec_to_angle (camera.pan,
temp); // rotate towards the player
wait (1);
}
}

The camera rotates towards the player all the time; the apparent distance between the camera and the player can be changed by pressing "+" and "-". Why apparent distance? Because I'm not changing the distance, but camera.arc which gives a similar effect. Let's look at the action associated to the player:

action skeleton
// the player
{
while (key_any == 1) {wait
(1);}
my.z = 30;
player = me;
falling = 0;

The first line waits until all the keys are released; this is useful when we restart the game. The skeleton is placed with its feet on the floor; play with 30 if you use another model and / or level. The variable named falling will be set to 1 if the player is falling and to 0 if it is sitting or walking on solid ground.

     while (lives > 0)
{
if (key_cuu ==
1)
{
my.pan = 0;
my.skill10 = my.x;
while ((my.x < my.skill10 + step_width) && (falling == 0))
{
ent_cycle("walk", my.skill20);
my.skill20 += animation_speed * time;
my.skill20 %= 100;
my.x += speed * time;
check_content();
wait (1);
}
}

As long as the player is alive, if we press the "up" arrow, the player will rotate in that direction (pan = 0) and then it will store its current x position in its skill10. As long as the player hasn't walked a distance equal or greater than step_width and if it sits on solid ground, everything inside the while loop will run. This includes the animation and the movement, which is done by simply changing player's x coordinate, because we don't need to check for collisions with other entities. The function check_content will detect if the player is standing on solid ground or not.

The code for the rest of the movement keys (down, left, right) is similar so I will let you look at it; we will meet again close to the end of this function.

          if (key_cud ==
1)
{
my.pan = 180;
my.skill10 = my.x;
while ((my.x > my.skill10 - step_width) && (falling == 0))
{
ent_cycle("walk", my.skill20); // play walk frames animation
my.skill20 -= animation_speed * time; // reversed "walk" animation
my.skill20 %= 100; // loop the animation
my.x -= speed * time;
check_content();
wait (1);
}
}
if (key_cul ==
1)
{
my.pan = 90;
my.skill10 = my.y;
while ((my.y < my.skill10 + step_width) && (falling == 0))
{
ent_cycle("walk", my.skill20); // play walk frames animation
my.skill20 += animation_speed * time; // "walk" animation speed
my.skill20 %= 100; // loop the animation
my.y += speed * time;
check_content();
wait (1);
}
}
if (key_cur ==
1)
{
my.pan = 270;
my.skill10 = my.y;
while ((my.y > my.skill10 - step_width) && (falling == 0))
{
ent_cycle("walk", my.skill20); // play walk frames animation
my.skill20 += animation_speed * time; // "walk" animation speed
my.skill20 %= 100; // loop the animation
my.y -= speed * time;
check_content();
wait (1);
}
}
wait (1);
}

See? I told you that the rest of the code is similar. Let's take a look at the lines below; if this code runs the player has lost all the lives:

my.skill20 = 0;
while (my.skill20 < 100) // the player is dead
{
ent_cycle("death", my.skill20); // play death frames animation
my.skill23 += 5 * time; // "death" animation speed
wait (1);
}
}

We reset skill20, because it was used for the "walk" animation and "death" should start from its first frame.

Function
check_content() tests the floor and sets falling to 0 if the player walks or stand
on solid ground, otherwise falling = 1.

function
check_content()
{
vec_set (temp, my.pos);
temp.z -= 64;
if (content (temp) != content_solid)
// if it is a hole
{
exclusive_global;
lives -= 1;
falling = 1;
snd_play (falling_sound,
70, 0);
while (player.z
> -1000)
{
player.z -= 35 * time;
wait (1);
}
sleep (2);
if (lives >
0)
{
points = 0;
main();
}
}
else
{
falling = 0; //
walking on solid ground
}
}

The first line stores player's coordinates in temp, then we substract 64 quants from temp.z; this point should be placed below player's feet (and inside the floor block, you got that right). If content (temp) sees a hole, the player will fall, so we stop all the other instances of the action, we decrease the number of lives, we set falling to 1 (it is needed in action skeleton), we play a falling sound, we move him downwards until its height is about -1000 quants, we wait for 2 seconds, we reset the score, we restart the level, we... oh, I should stop now.

If content(temp) is solid, falling is set to 0 so the player will be able to move ok.

Every sphere that adds points to the score has this action attached to it:

action bonus1
{
my.passable = on;
my.transparent = on;
my.light = on;
my.bright = on;
my.lightgreen = 250;
my.lightrange = 0;
while (vec_dist (player.x,
my.x) > 30)
{
my.pan += 3 * time;
// rotate if the player is far
wait (1);
}
snd_play (bonus_sound, 30, 0);
while (my.scale_x < 3)
{
my.scale_x += 0.1
* time;
my.scale_y = my.scale_x;
my.scale_z = my.scale_x;
my.z += 0.5 * time;
my.alpha -= 2 *
time;
wait (1);
}
points += 10;
ent_remove (me);
}

The sphere is passable, transparent and is illuminated by a (green) light. If the player is farther than 30 quants, the sphere will rotate. When the player has come close, we play a sound and then we increase the scale of the sphere until it reaches 3, we move it upwards, we decrease its alpha. We add 10 points to the score and then we remove the sphere.

The game would be boring if we wouldn't use a few tricks; the first one is:

action block_breakable
{
while (vec_dist (player.x, my.x) > 70) {wait (1);}
sleep (1.5); // wait 1.5 seconds
ent_morph (my, "block2.wmb");
// morph the initial block into this one
snd_play (break_sound, 50, 0);
sleep (0.1);
ent_morph (my, "block3.wmb");
// then this block
sleep (0.1);
ent_morph (my, "block4.wmb");
// and finally this block
sleep (0.1);
while (my.z > -500) //
now move the final block downwards
{
my.z -= 15 * time;
wait (1);
}
ent_remove (me); // and then remove it
}

This block will break in small pieces if you walk over it, so you will have a limited set of options to complete the level. It sits quietly in the dark until the player has come closer than 70 quants, then waits for 1.5 seconds, then morphs into another block, plays a sound, waits 0.1 seconds, morphs into another block and so on. The last block will be moved downwards until its z is about 500 quants and then it will be removed. You can be sure that the evil check_content() function discussed above will set falling to 1 if you spend too much time sitting on one of these blocks.

The second (and sadly, the last) block is:

action
block_end
{
my.invisible = on;
my.passable = on;
while (points < 100) {wait (1);} // will be invisible until the player has
100 points
my.passable = off;
my.invisible = off;
snd_play (finished_sound,
30, 0);
sleep (0.5);
my.invisible = on;
snd_play (finished_sound,
30, 0);
sleep (0.5);
my.invisible = off;
snd_play (finished_sound,
30, 0);
}

This block is like a bridge that gets lowered when you have managed to gather 100 points, but wait! It isn't a bridge and it isn't lowered... Anyway, it is the red block that allows you to get to the end of the level. The block will be invisible and passable until the player has 100 points; as soon as this happens, the block will flash, a sound will be played several times and the player will be able to move towards the flag:

action
flag
{
my.passable = on;
while (player == null) {wait (1);}
while (vec_dist (my.x, player.x) > 50) {wait (1);}
player.light = on;
player.lightred = 250;
player.lightrange = 100;
sleep (3); // wait for 3
seconds
exit; // shut down the engine
}

If the player has come close enough to the flag, it will glow red light (it has to celebrate it somehow, right?), and after 3 seconds the engine will shut down. I know that this is a sad idea for "Next Level" but if some of your customers complain about it you can tell them that this game was only a demo and you can give them their money back. Not all of them will complain so you will get something anyway...


Untitled

Smooth model shading

You know the problem: when your models moves from light to darkness, its ambient changes suddenly. The same thing happens when the model moves from dark places to brighter parts of the level. Wouldn't it be nice to have models that smoothly change their ambient light depending on the amount of light in the level? The idea behind this snippet is simple: we set unlit = on for our model in order to make it insensitive to any light source, we get tex_light on the floor below its feet and then we set the ambient of the model depending on tex_light's value.

I wanted to show you the difference between normal shading and smooth shading, so I have placed 2 guards in a test level. One of the guards uses the standard patrol_path action and the other guard uses the new patrol_path_ok action, a slightly modified version of patrol_path:

action
patrol_path_ok
{
actor_init();
smooth_shadow(); // the only
change

// attach next path
temp.pan = 360;
temp.tilt = 180;
temp.z = 1000;
result = scan_path(my.x,temp);
if (result == 0) { my._MOVEMODE = 0; } // no path found

// find first waypoint
ent_waypoint(my._TARGET_X,1);

     while (my._MOVEMODE > 0)
{
// find direction
temp.x = MY._TARGET_X
- MY.X;
temp.y = MY._TARGET_Y
- MY.Y;
temp.z = 0;
result = vec_to_angle(my_angle,temp);

force = MY._FORCE;

// near target? Find next waypoint
// compare radius must exceed the turning cycle!
if (result < 25) { ent_nextpoint(my._TARGET_X); }

// turn and walk towards target
actor_turnto(my_angle.PAN);
actor_move();

          // Wait one tick,
then repeat
wait(1);
}
}

You can see that the extra line calls our small function named smooth_shadow. Let's take a look at it:

function
smooth_shadow()
{
my.unlit = on;
while (1)
{
vec_set(temp, my.x);
temp.z -= 1000;
trace_mode = ignore_me
+ ignore_passable + ignore_models + ignore_sprites + scan_texture;
trace (my.x, temp);
// get tex_light
my.ambient = tex_light
/ 2.5;
waitt(4); // trace
4 times a second
}
}

This function traces 1000 quants below actor's origin in order to get the value of tex_light - the ambient value of the shadow map brightness. Tex_light can range from 0 to 255, so we divide its value by 2.5 in order to set the ambient for the model in the 0..100 range. If you think that your model is too dark you can use any value below 2.5

To see my test level, copy the \smooth folder in your Gamestudio folder, open smooth.wmp, build it and run. You will see 2 guards walking on the same path - I'll let you guess who's the guy that uses smooth shading.


Untitled

Spreadsheet viewer

This article offers you a chance to impress your boss, amazes your relatives, shows everybody that you are a serious dude after all, makes the world a better place and so on.

The code reads the data inside a text file and displays it as 3d graphs. Before I begin, let's see what's inside the data.txt file that came with this article:

data1, 100
data2, 50
data3, 35
data4, 75
data5, 58
end

Let's start with the function that reads the data:

function
read_data()
{
data_handle = file_open_read("data.txt"); // open the file
while (str_cmp (temp_string,
"end") == 0) // read until you have reached "end"

We open for reading the file named data.txt and we continue to read its content until we reach the end of the data ("end").

{
file_str_read(data_handle, temp_string);
str_cpy(data_name.string[i], temp_string);
if (str_cmp (temp_string, "end") == 0) // don't try to read inexistent data numbers after "end" -> this can create trouble
{
data_value[i] = file_var_read(data_handle);
if ((data_value[i] < 0) || (data_value[i] > 100)) // data_value must be in the 0..100 interval
{
error_text.visible = on; // make the error text visible
sleep (5); // for 5 seconds
exit; // and then shut down the engine
}
}
else
{
break; // get out of the loop -> don't increase "i" because it would be wrong and we need its correct value in main
}
i += 1;
}
ready = 1; // all the data was read
}

You can see that we have two columns inside data.txt: one for strings and the other one for their associated values. The strings are read in the temporary string temp_string and then their content is copied to data_name, an array of strings. The values are stored directly in the array named data_value. If the values read from data.txt are below zero or over 100 we show an error for 5 seconds and then we shut down the engine. The var named i (from index) will be 1 for the first record (data + value), 2 for the second record and so on. When we get out of the while loop, i = 5 if data.txt has 5 records, i = 2 if data.txt has 2 records and so on.

As soon as we get to the end of the file ("end") we use break to get out of the while loop and then we set ready to 1; this is a var that will be used in main. At this point we have all the strings stored in data_name and all the values stored in data_value.

function
main()
{
camera.arc = 75; // set the field of view
read_data(); // read the data stored in data.txt
level_load (dummy_wmb);
while (ready == 0) {wait
(1);} // wait until all the data is read
temp.x = 270;
temp.y = 100 - (5 - i) *
25;

We set the field of view to 75 degrees, we call the function that reads data.txt, we load a dummy level and then we wait until all the data is read (until ready = 1).

Let me tell you a few things about the dummy level - it is a special level. If you open it in Wed you will see that it contains a single block used as the origin for the 3d bars, but it hasn't got a hollowed box to surround it. If you "forget" to put your level inside a hollowed block or a sphere, the engine will render infinite distances (in fact, the limit is clip_range but let's not argue about that, ok?). Why would I want to do that? If something appears in the level for a few frames, it will remain there forever, even if you remove that entity, panel, text, etc. I have applied this method because I wanted to use a single panel for all the columns. If my data.txt file would have contained 50 records I would have needed to define 50 panels, right? With my trick I use a single panel to display all the values that appear over the 3d bars. I get the first value and I display it over the first bar for a few frames, then I read the next value and I move the panel above the 2nd bar for a few frames and so on.

The only drawback is that if I move the camera everything will be messed up, but why would I want to do that? Let's get back to main.

The 3d bars are in fact models; their scale_z is changed depending on their associated values, making the bars look shorter or taller. The models will be placed 270 quants in front of the player; their position on y will change depending on the number of records (data + value) like in the picture below:

You can see that no matter what number of records I use (from 1 to 5) in data.txt, the bars are aligned properly. The line that takes care of that is:

temp.y = 100 - (5 - i) * 25;

Do you remember i from function read_data? If we use 5 records in data.txt, i = 5 so temp.y = 100; for i = 4, temp.y = 75 and so on. The 3d bars will be created at the position given by temp; we've managed to align them on y but how can we align them on z? Our bar model has the origin in its center so if we would scale several bars without taking extra precautions we would get something like this:

Well... it is clear enough that if we scale an object its origin will remain at the same height; we must move the bars up or down depending on the value read from data.txt.

     j = 0;
while (j < i)
{
temp.z = -100 +
data_value[j];

We will use another counter named j because i holds the number of data records and we don't want to loose that value. The bar model has a height of 2 quants. If the value associated to a certain bar is 100, that bar will fill all the space on z (200 quants); for smaller values, the bars will be moved downwards with a quantity equal with data_value; this way all the bars will be aligned to the "floor".

          ent_create (bar_mdl,
temp, size_bar);
temp.y -= 50;
j += 1;
wait (3);
}
}

All the bars will be created at the position given by temp; the 2nd bar will have its y decreased by 50 quants and so on. We have to wait at least 3 frames because we want to display 5 different values in 5 different places using a single panel with a digit. If you have an older 3d card that uses double buffering you could wait (2) but it is safer to assume that the card uses triple buffering so wait (3) is the way to go.

Every 3d bar that is created runs this function:

function
size_bar()
{
my.albedo = 100;
my.light = on;
my.lightrange = 0;
my.lightred = bar_color[k+0];
my.lightgreen = bar_color[k+1];
my.lightblue = bar_color[k+2];
k += 3;
my.scale_z = data_value[j];
vec_set(temp_value, my.pos);
vec_to_screen(temp_value,
camera);
value_pan.pos_x = temp_value.x;
value_pan.pos_y = 80 + (100
- data_value[j]) * 4;
temp_value = data_value[j];
value_pan.visible = on;
}

I have set albedo to 100 because the bars must be sensitive to light; their colors are taken from an array - its definition is given below:

var bar_color[15] = 250, 0, 0, 0, 250, 0, 0, 0, 250, 250, 250, 0, 250, 250, 250; // red, green, blue, yellow, white

The first 3 values will give the color of the first bar, the next 3 colors will be used by the second bar and so on. I have decided to use red, green, blue, yellow and white; all the colors are applied to the white bar model and make it shine because I have set my.light = on.

The bars are scaled on z depending on the value stored in data_value; we want to display the figures on top of the bars so we use vec_to_screen to get the screen coordinates for every bar, then we place value_pan at its correct position, depending on data_value. Let's see how this works:

value_pan.pos_y = 80 + (100 - data_value[j]) * 4;

If the bar has a value of 100, value_pan.pos_y is set to 80; if the value is below 100, value_pan.pos_y will be bigger because the y coordinate is zero at the top of the screen. For every point below 100 we have to add 4 to value_pan.pos_y because the bar has a height of 2 quants and because every bar below 100 is moved downwards, in order to keep the alignment at the "floor" level.

Temp_value is used to display the value over the bar, using a single panel with a digit for all the values:

panel
value_pan // displays the values over the bars
{
pos_x = 0;
pos_y = 0;
layer = 10;
digits = 0, 0, 3, standard_font,
1, temp_value;
flags = d3d;
}

You might have noticed that I have used an array of strings - this is something that you can't read about in the manual because it isn't implemented at this point. However, we can use a text with multiple strings to create an array of strings:

text data_name
// create an array of strings using a text
{
font = standard_font;
pos_x = 0;
pos_y = 0;
strings = 6;
string = "
"; // 20 characters each
string = "
";
string = "
";
string = "
";
string = "
";
string = "
";
}

This text definition allows us to create an array of strings with 6 elements but we can use any value. This project can work with 1...5 data records so an array with 6 elements was more that what I needed.

I'm a lazy programmer so I try to work as little as possible; therefore, I have created a small c++ program to help myself during the development. All you have to do is to run filegen.exe with a few parameters and it will create the data.txt file for you. Here are some examples:

filegen 56 23

will create the following data.txt file:

data1, 56
data2, 23
end

filegen 99 88 77 66 55

will create the following data.txt file:

data1, 99
data2, 88
data2, 77
data2, 66
data2, 55
end

This way you don't have to edit data.txt to see how your graphs look with different record values. I have included the source code.

The project can be easily modified to accept csv (comma separated values, imported by many spreadsheet programs, including Excel). You can display the data names under (or over) the bars using a single text; if you want to display many more bars at the same time, change their scale_y depending on the number of records or make them thinner from the beginning.


Untitled

Stationary weapons

This article will show you how to create stationary weapons: cannons, big machine guns, etc.

The most complicated piece of code is listed below:

action
sweapon
{
while (player == null) {wait (1);}
player._health = 100;
my.passable = on;
while (player._health > 0)
{
if (player_immobile
== 0)
{
while (vec_dist (player.x, my.x) > 100) {wait (1);}
vec_set (players_speeds, strength);
vec_set (players_angles, astrength);
vec_set (strength, nullvector);
vec_set (astrength, nullvector);
player_immobile = 1; // can't move
while (key_any == 1) {wait (1);}
player.pan = 0;
player.tilt = 0;
}

We wait until the player is created and then we set its health to 100 (in case you forgot to do that in the action associated to the player). The stationary weapon (a cannon, in my example) will be passable.

Action sweapon will continue to run if player's health is greater than zero; I have defined a variable named "player_immobile" that is set to 0 when the player can move and is set to 1 when the player is immobile. If the player can move, we wait until it has come closer than 100 quants to the cannon and then we copy its speed on x, y, z and its angular speed in players_speed and players_angles. The next lines of code set strength and astrength to zero, so the player will not be able to move or rotate from now on. Player_immobile is set to 1, we wait until all the keys are released and then we reset player's pan and tilt angles.

          else // player_immobile
== 1
{
player.x = -1250;
player.y = -728;
if ((key_cuu == 1) && (player.tilt < 30))
{
player.tilt += 2 * time;
}
if ((key_cud == 1) && (player.tilt > -10))
{
player.tilt -= 2 * time;
}
if ((key_cul == 1) && (player.pan < 30))
{
player.pan += 2 * time;
}
if ((key_cur == 1) && (player.pan > -30))
{
player.pan -= 2 * time;
}
my.pan = player.pan;
my.tilt = player.tilt;

If the player can't move, we set its x and y coordinates where we need them; I have got these values at runtime, using the debug panel. If we press the "up" arrow key and player's tilt angle is below 30 degrees, we increase the tilt angle. If we press the "down" arrow key and player's tilt is above -10 degrees, we decrease the tilt angle. The same thing happens with the pan angle; it changes its values from -30 to 30 degrees using the "left" and "right" arrow keys.

The last two lines of code are copying player's pan and tilt angle to the cannon.

if (key_ctrl == 1)
{
while (key_ctrl == 1) {wait (1);}
vec_for_vertex (rocket_coords, my, 190);
snd_play (fire_wav, 100, 0);
ent_create (rocket_mdl, rocket_coords, move_rocket);
}
if (key_space == 1)
{
vec_set (strength, players_speeds);
vec_set (astrength, players_angles);
player.pan = 0;
player.tilt = 0; // restore pan and tilt
player_immobile = 0; // can move
while (vec_dist (player.x, my.x) < 200) {wait (1);}
}
}
wait (1);
}
}

If we have pressed the "Ctrl" key on the keyboard, we wait until it is released (we disable autofire), we get the vertex coordinates for the rocket (vertex #190 for the cannon model), we play a sound and then we create a rocket that moves using the function named move_rocket.

If we press "space", we restore the initial strength and astrength values, allowing the player to move again. We set pan = 0, tilt = 0 and player_immobile = 0, telling the rest of the action that the player can move from now on. We allow the player to get away from the cannon if he doens't need the cannon anymore.

Let's get back to the function that moves the rocket:

function
move_rocket()
{
my.enable_entity = on;
my.enable_block = on;
my.event = remove_rocket;
my.pan = camera.pan;
my.tilt = camera.tilt;
rocket_speed.y = 0;
rocket_speed.z = 0;
while (my != null)
{
rocket_speed.x
= 100;
rocket_speed *=
time;
move_mode = ignore_you
+ ignore_passable;
ent_move (rocket_speed,
nullvector);
wait (1);
}
}

The rocket is sensitive to entities and level blocks; when it collides with one of the entities or with a level block, its event function named remove_rocket will run. The rocket has the same pan and tilt angles with the camera and its speed is given by rocket_speed.x. The rocket moves in a while loop, ignoring its creator (the cannon) and all the passable entities.

Curious to see the event function for the rocket? Ok, there you go:

function remove_rocket()
{
wait (1);
if (you != null)
{
you._health -=
150;
}
ent_create(explosion_pcx, my.pos, sprite_explosion);
my.event = null;
my.invisible = on;
wait (2);
ent_remove (me);
}

If the rocket hits an entity (you != null) it takes 150 health points from that entity, creates an explosion sprite, sets its event to null (so it won't react to anything else from now on), becomes invisible and it is removed after two frames. The function that drives the explosion sprite is listed below:

function
sprite_explosion()
{
my.scale_x = 5;
my.scale_y = my.scale_x;
my.scale_z = my.scale_x;
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);
}

The scale of the sprite is increased five times on x, y and z; this might come in handy if you want to replace the sprite with a model. The explosion is passable and has its "flare" and "bright" flags set; the ambient of the sprite is set to 100. We will hear a sound at the impact point and the explosion frames will be played in a loop; as soon as the last explosion frame was displayed, the sprite will be removed.


Untitled

Steroidz

Mike Bravo 23 calling base! There's a giant asteroid coming towards us! No, there are hundreds of them and they're all coming towards our ship!

If this isn't scary stuff then in must be Steroidz! Let's take a look at function main:

function main()
{
fps_max = 40;
level_load (steroidz_wmb); // dummy level
wait (2);
score = 0; // reset score
asteroid_generator();
display_lives();
}

We lock the frame rate to 40 and then we load the level (a huge mdl sphere and a tiny cube, otherwise it won't build!). When the level is loaded we reset the score and start generating asteroids and displaying the number of lives. We'll talk about these two functions a little later; let's take a look at the action that moves player's ship:

action
players_ship
{
my.dead = 0;
my.metal
= on;
my.skill22 = 0; // reset the number of hits
my.enable_impact = on;
my.event = player_dies;
player = me;
while (camera == null) {wait (1);}
while (my.dead == 0) // as long as the player isn't dead
{
player.tilt += 0.5 * (key_cuu
- key_cud) * time;
player.pan +=
2 * (key_cul - key_cur) * time;
ship_speed.x = 0.6 * key_space * time + max ((1 - time * 0.05), 0) * ship_speed.x;
ship_speed.y = 0;
ship_speed.z =
0;
ent_move(ship_speed, nullvector);

vec_set (temp, nullvector);
vec_sub
(temp, camera_distance);
vec_to_angle
(camera_angle, temp);
camera_angle.roll
= 0;

vec_set (camera.x, camera_distance);
vec_rotate (camera.x, my.pan);
vec_add
(camera.x, my.x);

vec_set (camera.pan,
my.pan);
ang_rotate (camera.pan,
camera_angle);

vec_for_vertex(particles_left,
my, 89);
vec_for_vertex(particles_right,
my, 83);

if (key_space == 1)
{
effect (ship_particles, 2, particles_left, normal);
effect (ship_particles, 2, particles_right, normal);
if (shipsound_handle == 0)
{
snd_loop (ship_snd, 50, 0);
shipsound_handle = result;
}
}
else
{
snd_stop (shipsound_handle);
shipsound_handle = 0;
}

if (key_ctrl == 1) // press ctrl to fire
{
if (my.skill35
== 0)
{
my.skill35 = 1; // disable autofire
ent_create (rocket_mdl, player.pos, ship_rocket);
snd_play (firerocket_snd, 80, 0);
}
}
else
{
my.skill35 = 0; // enable fire again
}

wait (1);
}
}

I have defined dead as skill20 so my.dead = 0 means my.skill20
= 0; skill20 will be set to 1 when the player is dead. The player is sensitive
to impact and it will die after 5 hits; the number of hits is stored in skill22.
I'm using the arrow keys (cursor up and down to tilt the ship, cursor left and
right to pan it), space for thrust (includes inertia) and ctrl to fire.

We are rotating the camera towards the ship, adding the proper distance (specified in var camera_distance) and then we are rotating the camera using ang_rotate, capable of rotating an object (the camera in Steroidz) depending on the ship direction / angles.

If we press space, the engines generate particles and plays a sound in a loop. The proper engine vertices are those with numbers 89 and 83 (get these numbers in Med); the two vec_for_vertex lines set the two vars (particle_left and particle_right) to these vertex coordinates.

If we press Ctrl, we create a rocket at player's position; the function that runs the rocket is:

function
ship_rocket()
{
wait (1);
my.ambient
= 100;
my.skill30 = 0;
my.enable_entity
= on;
my.enable_block = on;
my.event
= remove_me;
my.passable = on;
my.pan
= player.pan;
my.tilt = player.tilt;

my.skill12 = 100 * time;
my.skill13 = 0;
my.skill14 = 0;

while (my != null) // as long as the
rocket hasn't exploded
{
my.skill12 = 100 * time;
my.skill30
+= 0.1 * time;
if (vec_dist (my.x,
player.x) < 100) {my.passable = on;}
// don't collide with the player
else {my.passable = off;}
if (my.skill30
< 100)
{
ent_move (my.skill12, nullvector);
}
else
{
ent_remove
(me);
}
wait (1);
}
}

The code is similar to the one
used in the bow & arrow code. The rocket will move until it hits something
(an asteroid, of course) but if it hasn't hit anything it will be removed after
a certain period of time.

I have used a simple trick to create an infinite "playground" for our ship: I have placed a huge sphere in the level and I'm using a simple action to keep the player in the centre of the sphere (so when the player moves, the sphere moves with him in the same direction):

action
sky_follows_player
{
my.passable = on;
while (player == null) {wait (1);}
while (1)
{
vec_set (my.pos, player.pos);
wait (1);
}
}

The sky must be passable because the asteroids have to pass
through it and move towards the player. Let's get back to function asteroid_generator:

function asteroid_generator()
{
while (1)
{
if (random(1) > 0.97)
{
asteroid_angle = player.pan + 30 - random(60); // -30...+30 degrees
asteroid_pos.x = player.x + (5000 + random(5000)) * cos (asteroid_angle);
asteroid_pos.y = player.y + (5000 + random(5000)) * sin (asteroid_angle);
asteroid_pos.z = player.z + 300 - random(600);
ent_create (asteroid_mdl, asteroid_pos, move_asteroid);
}
waitt (3);
}
}

function move_asteroid()
{
my.ambient =
-50;
my.metal = on;
my.enable_entity
= on;
my.event = destroy_asteroid;
vec_set (temp.x, player.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp); // turn towards the player
my.pan += 3 - random(6); // miss the player from time to time
my.tilt += 1 - random(2);
while (my != null)
{
my.roll += 5 * time;
my.skill3 = 20 * time;
my.skill4
= 0;
my.skill5 = 0;
ent_move(my.skill3, nullvector);
wait (1);
}
}

The asteroids are generated from time to time, when random(1) > 0.97, on a torus (a ring) that surrounds the player, like in the picture below.

It wouldn't be fair to hit the player from behind, so the asteroids are generated only in front of the player (player.pan + 30 - random(60) degrees). Function move_asteroid will rotate the asteroid towards the player, but we change the direction a little by adding small random values to the pan and tilt angles.

function
destroy_asteroid()
{
my.ambient = 100;
my.passable = on;
waitt (2);
ent_remove
(me);
snd_play (explode_snd, 70, 0);
}

If the asteroid is hit by a rocket or the player, its ambient increases for 0.125 seconds and then the asteroid is removed. If the player is hit by an asteroid, it loses a life; the function that displays the number of lives sets the visible flag to on (or off) depending on skill22 (number of hits).

function
display_lives()
{
while (1)
{
if (player.skill22 == 0)
{
first_pan.visible = on;
second_pan.visible = on;
third_pan.visible = on;
fourth_pan.visible = on;
fifth_pan.visible = on;
}
if (player.skill22 == 1)
{
first_pan.visible = on;
second_pan.visible = on;
third_pan.visible = on;
fourth_pan.visible = on;
fifth_pan.visible = off;
}
if (player.skill22 == 2)
{
first_pan.visible = on;
second_pan.visible = on;
third_pan.visible = on;
fourth_pan.visible = off;
fifth_pan.visible = off;
}
if (player.skill22 == 3)
{
first_pan.visible = on;
second_pan.visible = on;
third_pan.visible = off;
fourth_pan.visible = off;
fifth_pan.visible = off;
}
if (player.skill22 == 4)
{
first_pan.visible = on;
second_pan.visible = off;
third_pan.visible = off;
fourth_pan.visible = off;
fifth_pan.visible = off;
}
if (player.skill22 == 5)
{
first_pan.visible = off;
second_pan.visible = off;
third_pan.visible = off;
fourth_pan.visible = off;
fifth_pan.visible = off;
}
wait (1);
}
}

The functions that are being used to display the particles for the engines are:

function ship_particles()
{
temp.x = random(2) - 1;
temp.y = random(10) + 2;
temp.z = random(2) - 1;
vec_add (my.vel_x, temp);
my.alpha = 30 + random(50);
my.bmap = particle_map;
my.size = 7 + random(4);
my.flare = on;
my.bright = on;
my.lifespan = 20;
my.function = fade_particle;
}

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

There isn't anything special with these functions; temp.y sets the correct trail direction and my.alpha -= 20 * time sets its length.

After 5 hits, the player loses its lives and has to restart the game by pressing R, which simply calls function main again.


Untitled

Sword combat

This time I have created a small level where the player and a skeleton fight using swords. Although the code can (and should) be improved it offers precious information on how to create close range combat weapons (sword, knife, fist, etc). You know that I don't like to use complicate main functions:

function
main()
{
level_load (sword_combat_wmb);
wait (2);
clip_size = 0;
on_d = null;
fps_max = 40;
}

We're going to move the player using WSAD so I had to disable the debug panel that appears when you press the "D" key. The action attached to the player is:

action player_fight
{
player = me;
player.healthpoints = 100;
while (player.healthpoints
> 0)
{
camera.x = player.x
- 200 * cos(player.pan);
camera.y = player.y
- 200 * sin(player.pan);
camera.z = player.z
+ 200;
camera.pan = player.pan;
camera.tilt = -30;

These lines say: I'm the player and I have 100 health points (healthpoints is just another name for skill18 in my example). As long as my healthpoints are bigger than 0, the while loop will continue to run. The following lines are placing the camera 200 quants behind the player all the time.

my.pan += 4 * (key_a - key_d) * time - 20 * mouse_force.x * time;
player_distance.x = 10 * (key_w - key_s) * time;
player_distance.y = 0;
player_distance.z = 0;

The player can rotate by pressing A and D or by moving the mouse. You can move forward / backward using the keys W and S. Player_distance is a var that will be used in the ent_move instruction that moves the player.

          if ((key_w == 1) || (key_s
== 1))
{
ent_cycle("walk", my.skill20);
my.skill20 += 4 * time;
if (my.skill20 > 100) {my.skill20 = 0;}
}
else
{
ent_cycle("stand", my.skill21);
my.skill21 += 2 * time;
if (my.skill21 > 100) {my.skill21 = 0;}
}
ent_move(player_distance,
nullvector);

If the player is walking (W or S are pressed) we play its "walk" animation frames. If W or S aren't pressed the player is standing so we play its "stand" animation frames. Finally, player_distance is used to move the player.

if (mouse_left == 1)
{
while (my.skill22 < 100)
{
ent_vertex(my.sword_tip, 315); // get the value in Med
ent_vertex(my.sword_base, 293); // get the value in Med
trace_mode = ignore_me + ignore_passable;
trace (my.sword_base, my.sword_tip);
if (result != 0)
{
effect (particle_sparks, 10, target, normal);
if (you != null) {you.healthpoints -= 6 * time;}
ent_playsound (my, sword_snd, 50);
}
ent_cycle("attack", my.skill22);
my.skill22 += 8 * time;
wait (1);
}
while (mouse_left == 1) {wait (1);}
}
wait (1);
}

If we press the left mouse button, we enter the attack loop. This loop will run until the player has reached his last "attack" animation frame, stored in skill22. We store the sword base and sword tip (just another names for skill12 and skill15) and we trace between these two positions. If result != 0 (the player has hit something) we generate some particles at hit point using the predefined "target" vector. If the player has hit an entity (not a wall, etc) health is decreased and a sword sound is played. The player will run through his "attack" animation frames and after we release the left mouse button everything goes back to normal.

     while (my.skill23
< 90)
{
ent_cycle("death",
my.skill23);
my.skill23 += 3
* time;
wait (1);
}
my.passable = on;
}

At this point the player is dead, so he plays the "death" animation frames and then he becomes passable, therefore he can't be hit by the enemy sword from now on because trace is set to ignore passable entities.

The action attached to the enemy is similar; I have used a pointer named "enemy" but if you plan to use several enemy units replace "enemy" with "my":

action enemy_fight
{
enemy = me;
enemy.healthpoints = 100;
while (my.healthpoints >
0)
{
if (vec_dist (my.x,
player.x) < 200 && player.healthpoints > 0)
{
vec_set(temp, player.x);
vec_sub(temp, my.x);
vec_to_angle(my.pan, temp);
my.tilt = 0;
enemy_distance.x = 5 * time;
enemy_distance.y = 0;
enemy_distance.z = 0;
ent_move(enemy_distance, nullvector);
ent_cycle("walk", my.skill19);
my.skill19 += 5 * time;
if (my.skill19 > 100) {my.skill19 = 0;}

The enemy has 100 healthpoints too; if the player is alive and comes closer than 200 quants, the enemy will rotate towards him and will start chasing him, playing its "walk" animation frame in a loop.

               if
(vec_dist (my.x, player.x) < 50)
{
while (my.skill20 < 100)
{
ent_vertex(my.sword_tip, 291); // get the value in Med
ent_vertex(my.sword_base, 306); // get the value in Med
trace_mode = ignore_me + ignore_passable;
trace (my.sword_base, my.sword_tip);
if (result != 0)
{
effect (particle_blood, 2, target, normal);
if (you != null) {you.healthpoints -= 4 * time;}
ent_playsound (my, sword_snd, 50);
}
ent_cycle("attack", my.skill20);
my.skill20 += 5 * time;
wait (1);
}
waitt (6); // slows down the enemy and reduces the number of traces per second
}
}

If the player is closer than 50 quants, the enemy will attack using the similar code and a different particle function. The enemy will cause less damage - the player will have a hard time anyway :).

          else // the player
is farther than 200 quants away
{
ent_cycle("stand", my.skill21); // play stand frames animation
my.skill21 += 2 * time; // "stand" animation speed
if (my.skill21 > 100) {my.skill21 = 0;} // loop animation
}
wait (1);
}
while (my.skill22 < 80) // the enemy is dead
{
ent_cycle("death",
my.skill22); // play death frames animation
my.skill22 += 1
* time; // "death" animation speed
wait (1);
}
my.passable = on; // the corpse can't be hit
by the sword from now on
}

If the player is farther than 200 quants, the enemy will go back to its "stand" animation frames. The enemy dies sharing the same code with the player.

The functions that generate particles (player's blood and skeleton's blood) are similar:

function particle_sparks()
{
temp.x = random(2) - 1;
temp.y = random(2) - 1;
temp.z = random(1) - 1.5;
vec_add (my.vel_x, temp);
my.alpha = 30 + random(50);
my.bmap = spark_map;
my.size = 10;
my.flare = on;
my.bright = on;
my.move = on;
my.lifespan = 20;
my.function = fade_particle;
}

function
particle_blood()
{
temp.x = random(2) - 1;
temp.y = random(2) - 1;
temp.z = random(1) - 1.5;
vec_add (my.vel_x, temp);
my.alpha = 70 + random(30);
my.bmap = blood_map;
my.size = 6;
my.flare = on;
my.bright = on;
my.move = on;
my.lifespan = 20;
my.function = fade_particle;
}

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

The particles are generated in an area located near the impact point and they're falling down (temp.z < 0). Let's take a look at the panel and text:

string health_str = "Player Health: Enemy Health:";

panel health_panel
{
pos_x = 0;
pos_y = 0;
digits = 120, 575, 4, swc_font,
1, player.healthpoints;
digits = 550, 575, 4, swc_font, 1, enemy.healthpoints;
flags = refresh, visible;
}

text
health_text // displays the text
{
pos_x = 0;
pos_y = 550;
font = swc_font;
string = health_str;
flags = visible;
}

You can see that I'm using a single panel with 2 digits to display the values and a single text with a longer string to display player's and enemy's strings. You're right, that's why I have created a pointer for the enemy: I wanted to display its health too.

So what happens if the skeleton hits player's sword? The sword and the player are created as a single model in my example so player's health will be decreased anyway. You can avoid that by having a player with a separate sword model attached to him or by detecting the impact point and comparing its coordinates with sword's coordinates read by ent_vertex.


Untitled

Take-off

Sometimes we need to interrupt the player from whatever he is doing because we need to show a nice cut scene effect. If you have decided to write the code for your game from scratch, you shouldn't have too many problems but what about the rest of the users, the ones that are faithful to the template code?

The code used inside this article creates a chopper that sits on the ground, waiting for the player to come close to it; when this happens, the chopper takes off by itself.

I had to get rid of the default camera without modifying the templates so I have defined a new view:

view
cutscene_view
{
layer = 15;
pos_x = 0;
pos_y = 0;
}

The action attached to the chopper has its code listed below:

action chopper
{
my.passable = on;
while (vec_dist (my.x, player.x) > 150) // wait until the player has come closer
than 150 quants
{
ent_cycle("fly",
my.skill20); // play "fly" animation
my.skill20 += animation_speed
* time;
my.skill20 %= 100;
// loop the animation
if (!snd_playing(chopper_handle))
{
chopper_handle = ent_playsound (my, chopper_wav, 200);
}
wait (1);
}

The chopper is passable; if the player is farther than 150 quants, it plays its "fly" animation in a loop and plays the chopper_wav sound in a loop.

     cutscene_view.size_x = screen_size.x; // has
the same width
cutscene_view.size_y = screen_size.y;
// and height with "camera"
cutscene_view.x = -1685;
cutscene_view.y = -1240;
cutscene_view.z = 210;
cutscene_view.pan = 20;
cutscene_view.tilt = -15;
cutscene_view.roll = 0;
player.invisible = on; // hide the player
player.shadow = off; // and its shadow (if any)
camera.visible = off; // hide "camera"
cutscene_view.visible = on;
// show the new view

If the player has come closer than 150 quants to the chopper, we use the same screen size width and height for cutscene_view, we set a convenient position and angles for the new camera, we hide the player and its shadow, we hide the old view (camera) and then we show the new view (cutscene_view). I chose those values for cutscene_view by flying inside the level; press zero twice to be able to fly, press "D" or "F11" to see the debugging panel and look at its xyz and ang columns to get the x, y, z, pan, tilt, roll values needed for your game.

     while (my.z < 1000) // wait until the chopper
has reached a height of 1,000 quants
{
if (animation_speed
< 10)
{
animation_speed += 0.05 * time;
}
else // start flying
when animation_speed is bigger than 10
{
my.z += 10 * time;
my.x += 30 * time;
}
ent_cycle("fly",
my.skill20); // play "fly" animation
my.skill20 += animation_speed
* time;
my.skill20 %= 100;
// loop the animation
if (!snd_playing(chopper_handle))
{
chopper_handle = ent_playsound (my, chopper_wav, 500); // the sound is louder
this time
snd_tune (chopper_handle, 0, 25 * animation_speed, 0);
}

As long as the chopper hasn't reached the height of 1,000 quants, its animation speed grows from 4 to 10; when animation_speed is bigger than 10, the chopper starts to take off (increases its z) and moves forward (increases its x). We play the same "fly" animation at a bigger speed and chopper_wav is tuned accordingly.

          vec_set(temp, my.x);
vec_sub(temp, cutscene_view.x);
vec_to_angle (cutscene_view.pan,temp);
// rotate cutscene_view towards the chopper

These three lines of code will rotate cutscene_view towards the player all the time.

wait (1);
}
exit; // shut down the engine
}

The chopper has reached a height of over 1,000 quants so the engine will shut down. You could load a new level here, maybe trigger some explosions in the same level, etc. The only thing that matters is that the player is in good hands now (as long as I'm not flying the chopper).


Untitled

Tanks

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 :)


Untitled

Teleport

Beaming the player from one place to the other is really easy - all you need to do is change its coordinates and sometimes its angle. The snippet below allows you to set the destination points and angle in Wed so you don't need to change these coordinates in the C-script file every time you set a new destination.

The action attached to the teleport entity is:

action
teleport
{
while (player == null) {wait(1);}

vec_set (my.skill15, strength);
vec_set (my.skill18, astrength);

person_3rd = 0.5;
camera_dist.x = 200;
camera_dist.z = -300;
head_angle.tilt = -45;

We wait until the player is created and then we store its strength and astrength template values (player's speed on x y z and player's angular speeds). I'm using skills15..17 to store strength and skills18..20 for astrength. If you're an older reader of this magazine you already know that we can store any var in 3 consecutive skills.

We force the 3rd person camera mode and we set a decent camera distance on x and z and a good camera angle by changing the values in the templates.

     while (1)
{
while (vec_dist
(my.x, player.x) > 40) {wait (1);}
vec_set (strength,
nullvector); // the player can't move
vec_set (astrength,
nullvector); // or rotate from now on
temp.z = my.z;
my.skill10 = particle_emmiters;
my.skill11 = min_radius;
while (my.skill10
> 0)
{
temp.x = player.x + min_radius * cos(my.skill12);
temp.y = player.y + min_radius * sin(my.skill12);
temp.z += up_step;
my.skill12 += angle_step;
min_radius += radius_step;
effect (create_fx, 1, temp.x, normal);
my.skill10 -= 1;
}

When the player comes closer than 40 quants to the origin of the teleport entity, we reset its strength and astrength so it won't be able to move or rotate from now on. We create particles on a circle around the player - the radius and the height of this circle keeps growing and growing, like in the picture below:

player.shadow = off;
player.transparent = on;
player.alpha = 100;
while (player.alpha > 0)
{
player.alpha -= 7 * time;
wait (1);
}
player.invisible = on;
waitt (16);
min_radius = my.skill11;
player.x = my.skill1; // set skill1..4 in Wed
player.y = my.skill2;
player.z = my.skill3;
player.pan = my.skill4;
player.transparent = off;
player.invisible = off;
vec_set (strength, my.skill15); // restore player's speed on x, y, z
vec_set (astrength, my.skill18); // and player's angle speeds

wait (1);
}
}

If the player has its shadow flag set, its shadow will disappear; the player will fade away by changing its alpha value. After a second, the player will be moved to its new position set with skills1..4 for x, y, z and pan because sometimes we want the player to face a certain direction after being beamed.

Remember skill15..17 and skill18..20? We have stored the initial strength and astrength in them - now it is the time to get the values back because we want to be able to move the player.

The only think left to be discussed is the particle effect:

function
create_fx()
{
temp.x = random(2) - 1;
temp.y = random(2) - 1;
temp.z = random(2) + 1;
vec_add (my.vel_x, temp);
my.flare = on;
my.bright = on;
my.alpha = 50;
my.bmap = effect_pcx;
my.size = 30;
my.move = on;
my.lifespan = 100;
my.function = fade_fx;
}

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

You can see that the particles have random speeds on x, y and z but the value on z is always greater than zero so the particles will move upwards, decreasing their alpha value as they go up.


Untitled

Terrain deformation

Vec_for_mesh and vec_to_mesh were some of Aum3's hot features. Take a look at this code that generates falling bombs (ok, I've used a rocket model) that create craters when they impact with a terrain mesh.

I have created another standalone project; the example runs at 800x600x16bit, Vertex_dist[1090] is an array that will hold the distance between every vertex and hit_coords
var video_mode = 7; // 800x600
var video_depth = 16; // 16 bit mode
var vertex_dist[1090];
var hit_coords;

Function main loads our test level and sets a weird camera position (if you don't like this position, press zero and move the camera where you want it)

function
main()
{
level_load
(terrain1_wmb);
wait
(2);
camera.x = 345;
camera.y = -345;
camera.z = 300;
camera.pan = 135;
camera.tilt = -40;
}

I have added a small model in Wed; its associated action generates random falling bombs:

action bomb_launcher
{
my.passable = on;
while (1)
{
temp.x = my.x + 200 - random(400);
temp.y = my.y + 200 - random(400);
temp.z = my.z;
bomb = ent_create (bomb_mdl, temp, move_bomb);
waitt (40); // create a new bomb every 2.5 seconds
}
}

The first line in this action prevents the bomb from getting stuck inside its creator. A new bomb is generated every 2.5 seconds, in a 400x400 quants area. Every bomb moves according to its move_bomb function:

function
move_bomb()
{
my.enable_entity
= on;
my.enable_block = on;
my.event = explode_me;
bomb_speed.x = 0;
bomb_speed.y = 0;
bomb_speed.z
= -5;
bomb_speed *= time;
while (my != null)
{
ent_move (nullvector, bomb_speed);
wait (1);
}
}

You can see that the bomb is sensitive to entities and level blocks, has a negative z speed and moves until its explode_me event is triggered:

function
explode_me()
{
waitt
(2);
play_entsound (me, bumm_wav, 200);
ent_remove (me);
}

We need to wait a little to be able to store the impact coords in hit_coords. We play an explosion sound and we remove the bomb.

The terrian does all the job when its create_crate event is triggered:

action
terrain
{
terrain1
= me;
my.enable_impact = on;
my.event = create_crate;
}

function create_crate()
{
vec_set (hit_coords, bomb.pos);

First of all, we store the impact coords before the bomb disappears

wait (1);
my.skill1 = 1;
my.skill2 = 1500;

We set the vertex_dist index to 1 and skill2 to a huge distance; this distance will be much smaller for the vertices that are closer and closer to the impact point.

while (my.skill1 < 1090) // vertices 1...1089 for a 33x33 grid
{
vec_for_mesh (temp, terrain1, my.skill1);
vertex_dist [my.skill1] = vec_dist (hit_coords.x, temp.x);
if (vertex_dist[my.skill1] < my.skill2) // this vertex is closer than the others?
{
my.skill2 = vertex_dist[my.skill1]; // then it might be the one!
}
my.skill1 += 1; // next vertex
}

My terrain has a 33x33 grid with 1089 vertices; my.skill1 will be set to 1, 2, 3, ... 1089 in the while loop above. Vec_for_mesh sets temp to vertex 1, 2, 3, ... and we store the distance between vertex1 and hit_coords in vertex_dist[1], the distance between vertex2 and hit_coords in vertex_dist[2] and so on, like in the picture below:

I couldn't draw blue arrows for all the vertices, but it looks like vertex18 is the winner in my picture, so the distance between vertex18 and hit_coords will be stored in skill2.

Now we need to find out the index for the winner, because this index will be passed to function deform(vertex_number).

    my.skill1 = 1;
while
(my.skill1 < 1090)
{
if (my.skill2 == vertex_dist[my.skill1])
{
my.skill3 = my.skill1;
}
my.skill1 += 1;
}
deform (my.skill3);
}

This time we are searching for the smallest vec_dist stored in vertex_dist and when we find it we store the index in skill3 - this is the vertex that will be moved inwards. It is clear that these while loops can be optimized but they run so fast (without wait) and it isn't necessary to do it.

The final function is:

function
deform(vertex_number)
{
vec_for_mesh(temp,
terrain1, vertex_number);
vec_scale
(temp, 0.7);
vec_to_mesh (temp, terrain1, vertex_number);
}

We set vertex_number as an argument for vec_for_mesh, so temp will be set to vertex_number's coords. Temp will be scaled to 70% of its value and then vec_to_mesh will deform the vertex that was closest to hit_coords.


Untitled

Editable text

Ok, I know that the title doesn't say too many things, but I'm pretty excited when I think about this article. Wouldn't you like to be able to place a text inside your 3D world, maybe to display some of those banners for your sport game, or the name of the winner in a racing game, or an access code that changes from time to time? We know how to use regular texts that appear on the screen, but wouldn't it be nice if we could display them as objects inside our 3D worlds? Well, that's exactly what I'm going to show you today. There is some bad news: this code can't run with A5.

The idea behind this code is pretty simple: we define a font bitmap as a regular bitmap (not as a font!) and we read all its pixels. Whenever we encounter a pixel that hasn't got its RGB = 000 we draw a particle in the level. Let's start examining the code, shall we?

bmap system_font = <system.pcx>;
string temp_str; // just a temporary string that holds your text

You can see that system_font was defined using a regular bmap definition; temp_str is the string that will store our text ("Acknex rules!" in my demo).

function
create_text(my_string)
{
var number_of_chars;
var format;
var char_minx;
var char_miny;
var char_maxx;
var char_maxy;
var pixel_value;
var char_width;
var char_height;
var particle_width = 1.3;
var particle_height = 1.5;
var particle_space = 12;

Let's discuss each and every var definition:

- number_of_chars stores the number of characters in our string (13 in my demo);

- format is returned by a bmap_lock instruction and gives the type of the texture;

- char_minx is the minimum x (in pixels) for the current character on the font bitmap;

- char_miny is the minimum y (in pixels) for the current character on the font bitmap;

- char_maxx is the maximum x (in pixels) for the current character on the font bitmap;

- char_maxy is the maximum y (in pixels) for the current character on the font bitmap;

- pixel_value stores the RGB value of the current pixel that is read from the bitmap;

- char_width and char_height give the width and height of any character in our font bitmap;

- particle_width sets the width of the letters (the distance between 2 particles on the x axis);

- particle_height sets the height of the letters (the distance between 2 particles on the y axis);

- particle_space sets the space between the letters.

Play with the last three variables and you will obtain thinner or thicker texts using the same bitmap for the font.

  char_width = bmap_width(system_font) / 32;
char_height
= bmap_height(system_font) / 4;
number_of_chars = str_len(my_string);
format = bmap_lock(system_font, 0);

The lines above compute char_width (each system_font row has 32 chars) and char_height (system_font has 4 rows of chars). We get the number of characters stored in our string (Acknex rules!) and then we lock the font bitmap because we will need to work it quite a bit :)

    while (number_of_chars > 0) // create the
particles char by char
{
temp = str_to_asc(my_string);
str_clip (my_string, 1);
char_minx = (temp % 32) * char_width;
char_miny = int(temp / 32) * char_height;
char_maxx = char_minx + char_width;
char_maxy = char_miny + char_height;

Our particle-based characters are created one by one, so we use a loop that does just that. We convert the first character from our text into its ASCII equivalent, and then we remove the first character from the string because next time we will want to read the 2nd char, then the 3rd, and so on. We get char_minx and char_miny, the minimum x and y of the current char on the font bitmap, and then we compute char_maxx and char_maxy.

The picture above shows these values for the "C" character from our font bitmap as an example.

         while(char_minx <
char_maxx)
{
char_miny = char_maxy - char_height;
while(char_miny < char_maxy)
{
pixel_to_vec(pixel_value, temp, format, (pixel_for_bmap(system_font, char_minx,
char_miny)));
if (pixel_value > 0)
{
vec_set (temp, vector (100, 80, -5));
temp.y += (char_maxx - char_minx) * particle_width + number_of_chars * particle_space;
temp.z += (char_maxy - char_miny) * particle_height;
effect(particle_text, 1, temp, nullvector);
}
char_miny
+= 1;
}
char_minx += 1;
}
number_of_chars -= 1;
}
bmap_unlock(system_font);
}

We are using 2 while loops to draw the particle "pixels" for our character; the inner loop draws the columns and the other loop increases the x coordinates. We use pixel_to_vec to read the RGB value of the pixel (pixel_value) and if it isn't completely black (RGB > 0) we create a particle. We set the approximate position of our text in the 3D world using a simple vec_set instruction; please be aware that the size of the text depends on particle_width, particle_height and particle_space, so you'll have to play with (x = 100, y = 80, z = -5) from my demo until they match your level. If you want to change the orientation of the text along the x axis, simply replace "temp.y = ...;" with "temp.x = ...;". The "effect" line creates the particles; you can ent_create small sprites here if you hate particles, but you'll lose the ability to change their color, or fade them, or whatever you can do with particles. Number_of_chars is decreased outside the two loops, in order to move on to the next character from our text. Finally, we unlock the bitmap because it must be tired by now...

That was the hard part. Let's examine the functions drive the particles:

function
particle_text()
{
my.size = 1;
my.red =
255;
my.green = 100;
my.blue = 100;
my.function = keep_particles;
}

function
keep_particles()
{
my.lifespan = 100;
}

If you want to create a big text, change particle_width, particle_height and particle_space, but don't forget to choose a convenient size for the particles (1 in my demo). The following lines set the RGB combination of colors for the particles, and the last line of code inside function particle_text sets function keep_particles() as a particle nanny. All this function does is to set a lifespan of 100 for each particle, which means that the particles will live forever. You should decrease the lifespan if you plan to use different texts in the same spot.

Let's see the function that tests our beautiful script:

function
test_particles()
{
str_cpy (temp_str, "Acknex
rules!");
create_text(temp_str);
}

on_t = test_particles;

Ok, so we have to press "T" to see our text. The function associated to the "T" key copies our desired text to temp_str and then calls the ugly function we've talked about, using our string as a parameter.


Untitled

The rope and the ladder code - pay one and get the other one for free

By the time I'm writing this article, I haven't tested if the rope code works fine with ladders too, so let me create a small test level and I'll get back with you asap... I have created office.wmp - it has a rope and a ladder in it.

I have told you that the code is really simple. Let's take a look at it:

action rope
{
gravity_temp
= gravity;
my.enable_impact
= on;
my.event = rope_action;
}

If you take a look at movement.wdl in template, you'll see that the gravity is controlled by 'gravity' - a variable that changes its value if the player is under the water, etc. That's what I have used here: if the player is close enough to the rope / ladder, gravity is set to 0.01 (close to 0) and the player is able to move up and down without falling. Please note that we store the initial 'gravity' in our 'gravity_temp' variable; you can use another entity skill for that if you want to save some memory. Let's see the rest of the code:

function rope_action()
{
if (event_type
== event_impact && you == player)
{
while (my.skill10 <
my.skill1)
{
my.skill10 = abs(my.x - player.x) + abs(my.y - player.y);
gravity = 0.01;
if (key_home == 1 && my.z + (my.max_z - my.min_z) / 2 > player.z -
(player.max_z - player.min_z) / 2)
{
move (player, rope_speed_up, nullvector);
}
if (key_end == 1) // go down
{
move (player, rope_speed_down, nullvector);
}
wait (1);
}
gravity = gravity_temp;
}
my.skill10 = 0;
}

If the player collides with the rope, we check if he continues to be close to the rope in the while loop. At game start, skill10 = 0 so the condition in while is true. We compute the distance between the player and the rope on x and y and if it is small enough (smaller than skill1, which is set in wed), we're still inside the while loop, so we can move the player up and down using the keys home (up) and end (down). You'll have to choose the best value for skill1 when you're testing your ropes and ladders - this value depends on the player width, rope / ladder size, etc. If the player gets away from the rope, gravity = gravity_temp restores the original gravity and the player can walk, run or swim as before.

Don't let this line:

if (key_home == 1 && my.z + (my.max_z - my.min_z) / 2 > player.z - (player.max_z - player.min_z) / 2)

stand in your way; the explanation is quite simple. The distance between the rope / ladder and the player is computed only on x and y (I'll let you guess why), so the player could climb way above the end of the rope / ladder without loosing its x and y coordinates. This scary line makes sure that player's feet stop climbing at the end of the rope / ladder. You know that my refers to the rope / ladder. If you create an entity with its center in the origin (and I would always do that if I were you), its x, y and z are located in the center of the entity.

We can't use something like that:

if (key_home == 1 && my.z > player.z) because this won't allow the player to climb to the end of the rope / ladder

If you take a look at the first picture, you see that we must find out the 'height' (length) of the rope; fortunately we can use max_z - min_z to find it out (2nd picture). Our goal is to allow the player to climb on top of the rope / ladder (3rd picture). I hope that you know that the rope is red and the player is blue; their origins are the yellow dots.

If you look at the 1st picture again, you see that if we add half of the rope to its center (which is my.z) we get the upper z rope coordinate; that's :

my.z + (my.max_z - my.min_z) / 2

If the player has its origin in the center too (we're assuming that this is true), its lower z coordinate (player's feet) will be given by:

player.z - (player.max_z - player.min_z) / 2

We are substracting half of its body from its center, so the player can climb as long as his feet aren't above the top of the rope. Btw, the player climbs using a simple move instruction and two vectors: one of them has a positive speed on z, the other one has a negative speed on z. Change these values to get different climbing speeds.

This rope / ladder code is way too simple to be perfect but it is something that will get you started.


Untitled

Thin Space 3D

Have you ever played Demon Star? Thin Space 3D is nowhere near DS' graphics but you want the code, not the gfx, right? By the end of this article you will have a playable level with cannons, enemies... ok, you read that on the main page.

This time I'm using the function main itself to restart the game

on_r main();

function
main()
{
while (key_r == 1)
{wait (1);}
waitt (8); // wait 0.5 seconds before restarting
load_level <space01.wmb>;
score = 0; // reset the score at game start
}

We don't want to trigger multiple restarts if we keep R (restart) pressed for too long so I had to put a while loop inside main.

All the entities are triggering the explode_me() function when they have to be killed (player, enemies, rockets, etc)

function
explode_me()
{
exclusive_global;
play_sound explo_snd, 100;
if (you.skill2 == 1) {score +=
10 + random(10);}
remove me;
}

If this function was triggered by a bad guy (skill2 = 1) player's score increases. The player controls the ship using this action:

action
ship
{
player
= me;
my.enable_entity = on;
my.enable_push = on;
my.enable_impact = on;
my.event = explode_me;
camera.diameter = 0;
camera.tilt = -90;
camera.pan
= 90;
camera.z = my.z + 700;
while (1)
{
if (my.y > 2975) {return;}

       ship_speed.x = 0;
ship_speed.y = 10 * time;
ship_speed.z
= 0;
if (key_cuu == 1)
{
ship_speed.y
+= 20 * time;
}
if (key_cud == 1)
{
ship_speed.y
-= 5 * time;
}
if (key_cur == 1 && my.x
< 450)
{
ship_speed.x
= 30 * time;
}
if (key_cul == 1 && my.x
> -450)
{
ship_speed.x
= -30 * time;
}
ent_move (nullvector, ship_speed);
camera.x = my.x;
camera.y = my.y
+ 200;

if (key_space == 1)
{
fire_rocket();
}
if (key_ctrl == 1)
{
fire_bomb();
}
wait (1);
}
}

The camera can pass through walls, is tilted down and rotated by 90 degrees so that it points in the right direction, 700 quants above the player. I have measured the y coordinate = 2975 (where the level ends) in Wed. You should put an invisible, passable (or not) entity in Wed and compare its y with player's y. The ship (controlled by the player) moves upwards all the time because its ship_speed.y is positive all the time. If we press the arrows keys player's speed is doubled (key up) or halved (key down). The ship can move to the left and right as long as its x is bigger than -450 and smaller than 450.

If you press space the ship will fire rockets; press ctrl to fire bombs:

function fire_rocket()
{
exclusive_global;
while (key_space == 1) {wait (1);}
if (player == null) {return;}
vec_set (temp, player.pos);
temp.y += 40;
create (<rocket.mdl>, temp,
rocket_moves);
}

We don't allow auto fire (it wouldn't be fair) and of course that we can't fire rockets if the player is dead (player = null). The rockets are created 40 quants above the player (we store player's position in temp, we add 40 quants to player's y and then we create the rocket there). Let's take a look at the function that moves player's rockets:

function
rocket_moves()
{
my.enable_entity
= on;
my.enable_push = on;
my.enable_impact = on;
my.event = explode_me;
my.skill1 = 0;
rocket_speed.x = 0;
rocket_speed.y
= 80 * time;
rocket_speed.z
= 0;
while (my.skill1 < 10)
{
my.skill1 += time;
ent_move
(nullvector, rocket_speed);
wait (1);
}
remove
me;
}

The rocket moves in the direction of the player, much faster than the player (it would collide with the player if its speed would be smaller than player's speed). A good thing to do is to destroy the rocket after a certain distance. We don't want to fire our guns at the beginning of the level and have all the enemies dead in the entire level because our rockets made it through the level to its end - and we don't want to loose performance by letting our rockets to fly on their own in the level. This is why we set skill1 to 0 for every rocket and then we increase it depending on the frame rate. When skill1 > 10, the rocket will disappear.

function fire_bomb()
{
exclusive_global;
while (key_ctrl ==
1) {wait (1);}
if (player == null) {return;}
create (<bomb.mdl>, my.pos, bomb_moves);
}

Function fire_bomb is simpler than fire_rocket so I won't discuss it here. Do you remember that I have created the rocket 40 quants above the player? I could have created the bomb 100 quants below the player but I wanted to show you a different technique that prevents the bomb from colliding with its creator at the moment of creation. Let's take a look at the function that moves player's bomb:

function bomb_moves()
{
my.enable_entity
= on;
my.enable_push = on;
my.enable_impact = on;
my.event = explode_me;
my.skill1 = 0;
bomb_speed.x = 0;
bomb_speed.y
= ship_speed.y;
bomb_speed.z
= -10;
while (my.skill1 < 100)
{
if (my.skill1 < 5)
{
my.passable
= on;
}
else
{
my.passable = off;
}
my.skill1 += time;
ent_move
(nullvector, bomb_speed);
wait (1);
}
remove
me;
}

The bomb is destroyed when its skill1 is bigger than 100. You see that bomb_speed.z = -10; sounds fair because the bomb moves downwards but what's with that bomb_speed.y = ship_speed.y? If the player moves with the lightning speed (up is pressed) the bombs inside the ship are moving with the same lightning speed so their inertia should move them in front of their launching position. If the player presses down, the bomb should touch the ground close to its launching position. Of course that there isn't any bomb inside the ship but only you and I know this - right? This tiny line of code takes care of that.

if
(my.skill1 < 5)
{
my.passable
= on;
}
else
{
my.passable = off;
}

The bombs are created at player's position so they would collide with their creator (the player). This time I'm making the bombs passable for a short period so they can get out of the player and then I set their passable flag to off.

Let's take a look at the bad guys:

action
enemy1
{
my.z = player.z;
// make sure that it collides with the player and its rockets
my.skill2 = 1;
my.enable_entity = on;
my.enable_push = on;
my.enable_impact = on;
my.event = explode_me;

We want to make sure that enemy1 (and most of all, its rockets) can collide with the player so we set its z position at player's z position. This means that we can place enemy1 ship at any height in Wed - they'all be aligned at player's height. Enemy1 is a bad guy so we're setting its skill2 to 1 (we check this in function explode_me(), remember?)

    while (player != null)
{
if (abs(vec_dist (my.x, player.x))
< 500)
{
vec_set (my.skill10, player.x);
vec_sub (my.skill10, my.x);
vec_to_angle (my.pan, my.skill10);

my.pan += 90;
if (my.y > player.y)
{
create (<bullet1.mdl>, my.pos, bullet1_moves);
}
}
waitt (16);
}
}

The enemy1 will move only if the player is still alive (player != null). When the player comes closer than 500 quants to enemy1, the enemy will rotate (change its pan angle) and fire one bullet per socond - waitt (16). We are adding 90 degrees to the ship to make sure that we fire the bullets in the right direction - we are using a level and a camera that are rotated by 90 degrees. Enemy1 will fire only if the player is below it. The function that moves enemy1's bullet is:

function bullet1_moves()
{
my.pan
= your.pan;
my.enable_entity = on;
my.enable_push = on;
my.enable_impact = on;
my.event = explode_me;
my.skill1 = 0;
e1bullet_speed.x = 0;
e1bullet_speed.y
= -30 * time;
e1bullet_speed.z
= 0;
while (my.skill1 < 20)
{
if (my.skill1 < 2)
{
my.passable
= on;
}
else
{
my.passable
= off;
}
my.skill1 += time;
ent_move
(e1bullet_speed, nullvector);
wait (1);
}
remove
me;
}

The first line sets bullet's pan angle to enemy1's (creator's) pan angle. The bullet moves downwards and it will remove itself after a certain period of time. We are making the bullet passable at first to prevent it from colliding with its creator. I have used this trick about an year ago to get rid of an annoying bug in the templates; if you were creating an enemy that was shooting rockets, it used to shot himself dead because the rockets were colliding with its body. The bullet moves in the direction set by its own pan angle; we aren't using ent_move (nullvector, e1bullet_speed) but ent_move (e1bullet_speed, nullvector).

Finally, enemy2 aka the cannon:

action enemy2
{
my.z = -270;
my.skill2 = 1;
my.enable_entity = on;
my.enable_push = on;
my.enable_impact = on;
my.event = explode_me;
while (player != null)
{
if (abs(my.x - player.x)+ abs(my.y - player.y) < 400) // player comes close to enemy1
{
create (<bullet2.mdl>, my.pos, bullet2_moves);
}
waitt (16);
}
}

I'm using a simple trick to align all the cannons to the floor level (I have got z = -270 in Wed for this level) and I'm setting skill2 to 1 (that's a bad entity, too). Enemy2 starts shooting when the player is near; I couldn't use vec_dist here because the distance between enemy2 and the player is pretty big - the cannons are placed on the ground and the player is placed up in the sky. The cannons will fire every second, too.

Here's the last function - it moves enemy2's bullet but there's nothing special with it:

function bullet2_moves()
{
my.enable_entity
= on;
my.enable_push = on;
my.enable_impact = on;
my.event = explode_me;
my.skill1 = 0;
e2bullet_speed.x = 0;
e2bullet_speed.y
= 0;
e2bullet_speed.z = 40 * time;
while (my.skill1 < 100)
{
if (my.skill1 < 5)
{
my.passable
= on;
}
else
{
my.passable
= off;
}
my.skill1 += time;
ent_move (nullvector,
e2bullet_speed);
wait (1);
}
remove
me;
}

This game template can (and should) be improved: you can make the bombs explode when they hit the ground, compute the "real" angles between the player and the enemy bullets to make sure that every shot can be deadly :) and so on.


Untitled

Torch

This month we will learn to create a torch that can be picked up and carried around, lighting up the surrounding areas.

action
torch
{
my.passable = on;
while (player ==
null) {wait (1);}
while(1)
{
effect(flame_init, 5, my.x, nullvector);
my.lightred
= 180 + random(30);
my.lightgreen = 150 + random(20);
my.lightblue = 60;
my.lightrange = 200 + random(5);
my.ambient
= 50;
if (vec_dist (player.x, my.x) < 70)
// the player has come really close to the torch?
{
my.x = player.x + 50 *
cos(player.pan) + 20 * sin(player.pan); // 50 quants in front of the player
my.y = player.y + 50 * sin(player.pan)
- 20 * cos(player.pan); // and 20 quants sideways (to the right)
my.z = player.z + 25; // the height of the torch is set 25 quants above player's
origin
my.pan = player.pan;
my.tilt = player.tilt;
my.roll = player.roll;
}
wait (1);
}
}

The torch model is passable and waits until the player model is created; as soon as this happens, the torch starts to create particles at its origin, using the "effect" instruction. Each and every particle will behave according to what the "flame_init" function instructs it to do. The torch emits a yellowish light whose range varies from 200 to 205 quants, giving a slight flickering effect. If the player has come really close to the torch (closer than 70 quants) the torch is picked up, being placed 50 quants in front of the player and 20 quants sideways (to the right side of the screen). The torch model is placed 25 quants above player's origin on the z axis and will inherit player's pan, tilt and roll angles.

function
flame_init()
{
temp.x = (random(2) - 1) / 2;
temp.y = (random(2) - 1) / 2;
temp.z = random(1) + 1;
vec_set(my.vel_x, temp);
my.move = on;
my.bmap = flame_tga;
my.flare = on;
my.bright
= on;
my.alpha = 70;
my.lifespan = 100;
my.size = 17;
my.function = fade_flames;
}

function
fade_flames()
{
my.alpha -= 25 * time; // play with 25
if(my.alpha < 0) {my.lifespan = 0;}
}

The functions above take care of the fire particles; we set a random speed on the x and y axis (ranging from -0.5 to 0.5), as well as on the z axis (ranging from 1 to 2). The fire particles start with an initial transparency factor of 70; play with this value until you are happy with the result. The function that controls the particles decreases their alpha quickly; we can't afford to use smaller values because the torch will move together with the player, and it would create a huge, unrealistic trail behind the player. The last line of code removes the particles that can't be seen anymore. You can use another "effect" line of code inside action "torch" and slightly modified particle functions if you want to add smoke as well.

Here's a trick: I have used a torch model with a weird origin; this way I didn't have to use complicated math (but simply my.x) in order to make sure that the flames start inside the torch model. Well, the needed math is here anyway (look at the formulas that bind the player and the torch together) but I thought that this idea might be helpful in one of your future projects.


Untitled

Train tracks

The purpose of this article is simple: to help us create code for an entity that "knows" how to follow the road. You can use the code for NPCs, pedestrians walking on the street, trains, light signs, and more. Let's see a brief explanation of the method and then we'll look at the code that makes it happen.

We've got a blue vehicle and we compute the coordinates for two points in front of it: front_left and front_right. We trace below these points every frame in order to get the name of the texture. If the name we've got is the name of the texture used for the track, we don't do anything; otherwise, we rotate the vehicle until trace returns the correct texture name, so we keep the vehicle on the track without using a predefined path. If you wanted to have pedestrians that use the sidewalk correctly and you didn't want to program complex AI in order to make it happen, now you've got a simple piece of code that does what you need.

var danger_zone = 0;

define
none 0;
define left 1;
define right 2;

function
main()
{
fps_max = 60;
level_load(level_wmb);
wait (3);
camera.x = -3500; // choose a convenient camera
position
camera.y = -2100;
camera.z = 1000;
camera.pan = 31; // and angles
camera.tilt = -20;
}

I have defined "none", "left" and "right" as 0, 1 and 2. Function main loads the level and sets a convenient camera position and angles.

action
train
{
var train_speed;
var front_left;
var front_right;
while (1)
{
train_speed.x = 20 * time;
train_speed.y = 0;
train_speed.z = 0;
ent_move (train_speed, nullvector);
front_right.x = my.x + 400 * cos(my.pan) + 300 * sin(my.pan);
front_right.y = my.y + 400 * sin(my.pan) - 300 * cos(my.pan);
front_right.z = my.z;
vec_set (temp.x, front_right.x);
temp.z -= 1000; // trace 1000 quants below
trace_mode = ignore_me + ignore_passable + ignore_models
+ ignore_sprites + scan_texture;
trace (my.x,
temp);
if ((str_cmpi ("metalplain4", tex_name)
== 0) && (danger_zone == none)) // if the texture below the train isn't
named "metalplain4"
{
danger_zone = right;
my.pan
+= 2 * time; // rotate the train toward left, play with this value
}
else
{
danger_zone = none;
}

The train moves with 20 * time quants / frame. Front_right is placed 400 quants in front of the train and 300 quants sideways (in relation to its origin). We trace 1000 quants below front_right, checking if the name of the texture returned by scan_texture is correct (metalplain4, used for the track in my demo) and if danger_zone is zero or not. Danger_zone is a simple variable that is controlled by the first "antenna" that gets outside the track. If danger_zone was zero (none) we can control the train by changing its pan angle and we set danger_zone to right; play with 2 * time if you want to adjust the rotation speed. As soon as the train (in fact, front_right!) is back on the track, danger_zone is set to none, allowing either front_left or front_right to take control when needed.

     front_left.x = my.x + 400 * cos(my.pan) - 300 * sin(my.pan);
front_left.y = my.y + 400 * sin(my.pan) +
300 * cos(my.pan);
front_left.z = my.z;
vec_set (temp.x, front_left.x);
temp.z -= 1000; // trace 1000 quants below
trace_mode = ignore_me + ignore_passable + ignore_models + ignore_sprites + scan_texture;
trace (my.x, temp);
if ((str_cmpi ("metalplain4", tex_name) == 0) && (danger_zone == none))
// if the texture below the train isn't named "metalplain4"
{
danger_zone = right;
my.pan -= 2 * time; // rotate the train
toward right, play with this value
}
else
{
danger_zone = none;
}
wait (1);
}
}

The things repeat for front_left; the only thing that might bother you is this: why do we need that danger_zone variable? The answer is simple: we don't want front_left and front_right to take control at the same time, and this variable makes sure that the train is rotated until it is back on the track, and only after that it allows other rotations.

I have used big values (400 and 300 quants) for front_left and front_right; this makes the train follow the track in a more random way. If you want to see a tight contour tracing path you can reduce these values (especially 300).


Untitled

Typing tutor

It is no secret that any Acknex user can develop all kinds of useful applications using the engine; this month I want to show you how to create a typing tutor "game". The idea behind this typing tutor is simple: you see a letter falling from the sky (ok, from the ceiling) and then you try to press the corresponding letter as fast as you can. If you aren't quick, the letter falls on the ground and you loose a point. There are several attack "waves", with letters that fall with bigger and bigger speeds.

When we run the level we see this image on the screen:

There are quite a few elements displayed on the screen so let's discuss them right away:

text start_txt
{
font = comic_font;
pos_x = 0;
pos_y = 200;
string = "   Get
ready, captain!\n Press any key to begin!";
flags = d3d, visible;
}

The first text displays the text in the center of the screen. Please note that I'm using newline (\n) to make the text appear on two rows. I will use this text to display other messages during the game.

text
score_txt
{
font = comic_font;
pos_x = 10;
pos_y = 10;
string = "Total:\nCorrect:\nWave:";
flags = d3d, visible;
}

Score_txt displays the three lines of text in the upper left corner.

panel score_pan
{
pos_x = 0;
pos_y = 0;
digits = 250, 10, 3, comic_font,
1, total_chars;
digits = 250, 50, 3, comic_font, 1, correct_chars;
digits = 250, 90, 3, comic_font, 1, wave;
flags = refresh, d3d, visible;
}

This panel displays the three numerical values that appear in the upper left corner (0, 0, 1 in the picture).

I will start with the function that creates the letters:

function
create_letters(number, interval)
{
while (number > 0)
{
letter = 1 + int(random(26));
temp.x = 0;
temp.y = 350;
temp.z = 450;
str_for_asc(temp_str,
letter + 64);
str_cat(temp_str,
".wmb");
str_cpy(letter_str,
temp_str);
str_trunc(letter_str,
(str_len(letter_str) - 1));
ent_create(temp_str,
temp, falling_letters);
letter_falls =
1;
while (letter_falls)
{wait (1);}
str_cpy(temp_str,
"");
number -= 1;
sleep(interval);
}
}

You can see that this function takes two parameters: the number of letters that will be created and the interval (in seconds) between two consecutive letters. If I run the function like this:

create_letters(4, 2);

it will create four letters that will appear every two seconds. Let's see the function one more time:

function create_letters(number, interval)
{
while (number > 0)
As long as the number of letters that need to be created is bigger than zero
{
letter = 1 + int(random(26));
generate an integer from 1 to 26
temp.x = 0;
temp.y = 350;
temp.z = 450;
set the coordinates for the letter in front and above player's head
str_for_asc(temp_str, letter + 64);
convert the number to the ascii char associated to the number (65..90)
str_cat(temp_str, ".wmb");
add ".wmb" to create something like this: p.wmb
str_cpy(letter_str, temp_str);
copy the letter.wmb string in letter_str
str_trunc(letter_str, (str_len(letter_str) - 1));
now keep only the first character of the string = the letter
ent_create(temp_str, temp, falling_letters);
now create the letter
letter_falls = 1;
set this var to 1 -> it will be used in another function
while (letter_falls) {wait (1);}
wait until the other function sets letter_falls to zero
str_cpy(temp_str, "");
reset temp_str
number -= 1;
decrease the number of letters
sleep(interval);
wait the number of seconds given by interval and repeat the steps above.
}
}

When we create a letter, it will run its associated function - falling_letters():

function falling_letters()
{
total_chars += 1;
my.enable_block = on;
my.event = destroy_letter;
falling_speed.x = 0;
falling_speed.y = 0;
falling_speed.z = -7;
falling_speed.z *= wave;
falling_speed.z *= time;

We increase total_chars, the figure associated to "Total:" in the game. The letter will be sensitive to level blocks, which will trigger its destroy_letter event. Falling_speed is negative on z so the letter will fall down. I have told you that the program uses "waves"; in fact, we use a variable named wave that is set to 1 for the first level, 2 for the second level and so on. Finally, falling_speed is multiplied by time to make sure that we have the same frame rate on any pc.

     while ((my != null) && (my.event != null))
{
ent_move (falling_speed,
nullvector);
str_for_key(temp_str,
key_lastpressed);
if (str_cmpi(letter_str,
temp_str) && key_any)
{
correct_chars += 1;
snd_play(key_wav, 30, 0);
letter_fades();
str_cpy(letter_str, "");
while (key_any) {wait (1);}
}
else
{
if (key_any)
{
destroy_letter();
}
}
wait (1);
}
}

As long as the letter exists and its event isn't set to null, we move the letter downwards, we convert the scan code to its associated key and then we compare the falling letter with the key that was pressed by the player. If these two strings match, we increase correct_chars, "Correct:" on our display and then we play a small sound. We run a small function that makes the letter fade away and then we reset the string named letter_str, otherwise we could increase correct_chars several times for a single letter, which wouldn't be correct, isn't it? We wait until the key that was pressed (any key) is released.

If the match isn't true, we have typed the wrong letter so if our finger is pressing the wrong key we call the function named destroy_letter:

function destroy_letter()
{
my.event = null;
ent_remove (me);
letter_falls = 0; // the
letter has stopped
snd_play(error_wav, 50, 0);
}

This function sets event to null, removes the letter, tells the function create_letters that the current letter has disappeared so it can create a new letter and then plays an error sound. The function that fades the letters is simple:

function letter_fades()
{
my.alpha = 100;
my.transparent = on;
while (my.alpha > 5)
{
my.alpha -= 10
* time;
wait (1);
}
ent_remove (me);
letter_falls = 0; // the letter has disappeared,
create a new one
}

The letter is made transparent but if its alpha is set to 100 it looks opaque; its alpha will be decreased until it is smaller than 5. When this happens, the letter is removed and letter_falls is reset so a new letter will be created.

The last function is main(); it looks quite complicated this time but all the code inside it is simple:

function
main()
{
on_d = null;
randomize();
fps_max = 30;
level_load (tutor_wmb);
sleep (1);
camera.x = 0;
camera.y = -350;
camera.z = 150;
camera.pan = 90;
camera.ambient = 100;

We disable the debug key because we wouldn't want to show the debug panel when letter "D" is falling. We make sure that we generate random numbers (random letters) every time we run the engine, we limit the frame rate to 30 fps and then we load the level. We wait for a second and then we set the camera position and pan angle. I thought that the textures I've used are a bit too dark so I have set camera.ambient to 100 in order to make the level look brighter.

     while (key_any == 0) {wait (1);}
start_txt.string = " First
wave - prepare!";
sleep (2);
start_txt.visible = off;
create_letters(10, 2);
while(total_chars < 10)
{wait (1);}

Do you remember the text definitions above? These texts are visible at game start; check their flags if you don't believe me. This means that the text "Get ready, captain!..." is visible on the screen at game start. We wait until the player presses any key and then we change the string used by the text so now we will see "First wave - prepare" on the screen. We wait two more seconds, we hide the text and then we create 10 letters, every 2 seconds, using our beloved create_letters function. We wait until all the letters are created (total_chars = 10).

The things are similar from now on but don't worry, I will comment the code anyway:

     sleep (2); // wait for 2 more seconds
start_txt.string = " Second
wave - prepare!";
start_txt.visible = on;
sleep (3); // for 3 seconds
start_txt.visible = off;
wave = 2;
create_letters (10, 1);
while(total_chars < 20)
{wait (1);}

We wait for two seconds, we change the string for the text and then we make the text visible. We wait for three seconds and then we hide the text, we set wave to 2; this will increase the falling speed. We create 19 letters that fall every second and then we wait until the second wave is finished (total_chars = 20).

     sleep (2);
start_txt.string = " Third wave - prepare!";
start_txt.visible = on;
sleep (3);
start_txt.visible = off;
wave = 3;
create_letters (10, 0.5);
while(total_chars < 30)
{wait (1);}
sleep (5); // wait for 5
more seconds
start_txt.string = "
Impressive result!";
start_txt.visible = on;
sleep (5);
exit;
}

We wait for two more seconds, we change the string, we make the text visible for three seconds, we set wave to 3 and we create 10 more letters; a letter could fall every 0.5 seconds if you have a big typing speed. We wait until the third wave is finished (total_chars = 30) and then we display "Impressive result!" which isn't quite true because the text will appear even if you miss all the letters. This text will appear for five seconds and then the engine will shut down. And this article will do the same thing.


Untitled

Virtua Cup

If you have ever played Virtua Cop you know that we are talking about a fast, furious action game. If you haven't played it, here's how it works: you are a cop and you try to shoot as many criminals as possible. You stand still and the bad guys appear from time to time at random positions. You shoot at them, they shoot at you... pretty simple, huh? Virtua Cup is a Virtua Cop clone.

This game is another
standalone example so it has its own main function:

function main()
{
level_load (virtuacup_wmb);
wait (2);
clip_size = 0;
fps_max = 50;
generate_enemies();
}

After we load the level, we lock the frame rate at 50 and we start generating the enemies:

function generate_enemies()
{
while (1)
{
waitt (48 + random
(48));
index = 3 * int(random(10));
temp.x = enemy_pos[index + 0];
temp.y = enemy_pos[index
+ 1];
temp.z = enemy_pos[index + 2];
ent_create(enemy_mdl,
temp, enemy_shoots);
if (index > 9) {index = 0;}
}
}

The bad guys appear every 3...6 seconds at random positions. I have defined an array that holds the xyz values for every position:

var enemy_pos[30] = -450,315,0, 60,315,0, 575,315,0, -325,540,255, -65,540,255, 195,540,255, 450,540,255, -190,850,515, 65,850,515, 320,850,515;

Nobody wanted to write those values for me so I had to get them in Wed. The first 3 numbers are the xyz coords for the 1st position, the following numbers are the xyz coords for the 2nd position and so on. My array has 30 elements so it stores 10 different positions. Let's get back to function generate_enemies:

index = 3 * int(random(10));

will generate a random number between 0 and 27 step 3 (0, 3, 6, 9, 12, ..., 27). We are copying the random coordinates to temp and then we create an enemy that will use its enemy_shoots function:

function
enemy_shoots()
{
wait (1);
my.pan += 270;
my.enable_impact = on;
my.event =
kill_enemy;
ent_create(enemygun_mdl, nullvector,
attach_enemygun);
my.skill10
= 0;
while (my != null && my.skill10
< 100)
{
ent_cycle("attack", my.skill10); // play
"attack" frames animation
my.skill10 += 3 * time;
wait (1);
}
snd_play (enemyshot_snd,
70, 0);
vec_set (temp.x,
player.x); // turn towards the player
vec_sub (temp.x, enemy_gun.x);
vec_to_angle
(my.pan, temp);
ent_create
(bullet_mdl, enemy_gun, move_bullet);
waitt (32);
ent_remove (me);
}

This enemy will be rotated to face the player. If the enemy is hit by a bullet, its kill_enemy action will be executed - we'll talk about it a little later. The line below and the function that follows it:

ent_create(enemygun_mdl, nullvector, attach_enemygun);

function attach_enemygun()
{
proc_late();
my.passable = on;
my.metal =
on;
while(you != null)
{
vec_set(my.x,you.x);
vec_set(my.pan,you.pan);
my.frame = you.frame;
my.next_frame = you.next_frame;
vec_for_vertex(enemy_gun,
my, 148);
wait(1);
}
ent_remove(my);
}

will give the enemy a gun. The enemy bullet will be created at the position given by vertex 148 on the gun model.

The enemy will play its attack animation frames if it hasn't been killed by the player:

while (my != null && my.skill10 < 100)
{
ent_cycle("attack",
my.skill10);
my.skill10 += 3 * time;
wait (1);
}

As soon as the enemy ends its "attack" animation frames, it plays a sound, rotates towards the player and shoots a bullet. The enemy will remain visible for two more seconds, giving the player the chance to shoot it.

snd_play (enemyshot_snd, 70, 0);
vec_set (temp.x,
player.x);
vec_sub (temp.x,
enemy_gun.x);
vec_to_angle
(my.pan, temp);
ent_create
(bullet_mdl, enemy_gun, move_bullet);
waitt (32);
ent_remove (me);

Here's the function that moves the bullet (same function for the player and for the enemy):

function move_bullet()
{
wait (1);
if (my == null) {return;}
my.enable_entity = on;
my.enable_block = on;
my.event = remove_bullets;
my.passable = on;
my.pan = you.pan;
my.tilt = you.tilt;

my.skill1 = 0;
bullet_speed.x = 500;
bullet_speed.y = 0;
bullet_speed.z = 0;
bullet_speed *= time;
while (my.skill1 < 50)
{
if (my == null) {return;}
if (my.skill1 <
0.1)  // don't collide with the creator
{
my.passable = on;
}
else
{
my.passable = off;
}
my.skill1 += 0.1 *
time;
ent_move (bullet_speed,
nullvector);
wait (1);
}
remove me;
}

function
remove_bullets()
{
wait (1);
ent_remove (me);
}

The bullet is sensitive to impact with entities or level blocks; if it hits one of these it will be removed using the simple function remove_bullets. The bullet will get its pan and tilt from its creators (the enemies or the player) so if these guys are pointing in the right direction, so is the bullet - got it? You have seen slightly modified versions of function move_bullet in other Aum editions.

Time to take a look at function kill_enemy:

function
kill_enemy()
{
wait (1);
exclusive_entity; // stop all the other
functions for this enemy
my.enable_impact
= off; // can't be shot from now on
my.event = null;
dead_enemies
+= 1;
my.skill10 = 0;
snd_play (enemydead_snd, 70, 0);
while (my.skill10 < 90)
{
ent_cycle("die", my.skill10); // play "die"
frames animation
my.skill10 += 5 * time;
wait (1);
}
waitt (16);
ent_remove (me);
}

If the enemy is hit by a player bullet, we stop all the other functions associated to this enemy that might run at the same time. The enemy will be insensitive to other bullets from now on and its event is set to null. We are adding one more body to the dead_enemies var, we play a death sound and the "die" animation frames. The corpse will remove visible for one more second and then it will disappear.

This code associated to the enemies has been discussed - let's move on to the code that is associated to the player:

action
virtua_player // attached to the player
{
player = me;
player.invisible = on;
player.skill1
= 100;
my.enable_impact
= on;
my.event =
decrease_players_health;
camera.x =
player.x;
camera.y = player.y;
camera.z = player.z;
camera.pan = 90;
while (player.skill1 > 0) // as long
as the player is alive
{
camera.tilt += 10 *
mouse_force.y * time;
if (camera.tilt < -25) {camera.tilt
+= 1;}
if (camera.tilt >
25) {camera.tilt -= 1;}
camera.pan -= 10 * mouse_force.x * time;
if (camera.pan <
60) {camera.pan += 1;} // 90 - 30
if (camera.pan > 120) {camera.pan -=
1;} // 90 + 30
if (mouse_left == 1
&& bullets > 0)
{
player_shoots();
}
player.pan = camera.pan; // rotate the
player too
player.tilt = camera.tilt;
// not just the camera
if (mouse_right == 1 && bullets
== 0) // right click to reload the gun
{
bullets = 10;
while
(mouse_right == 1) {wait (1);}
}
wait (1);
}
}

The player is invisible because we don't want it to stand in our way but don't worry - the enemies know its position very well! The player has 100 health points (stored in skill1). If the player is hit by the enemy bullets, his event (decrease_players_health) will run.

The camera is placed in the origin of the player so make sure that you choose a good player position. As long as skill1 > 0 (the player is alive) we can move the camera around and shoot bullets. I have limited the camera movement to 60...120 degrees for pan and -25...25 degrees for tilt. Why would I need to look behind my shoulder when all the enemies are in front? If we run out of bullets we have to press the right mouse button to reload the weapon. If we press the left mouse button (LMB) and bullets > 0 we run the function player_shoots:

function player_shoots()
{
exclusive_global;
while (mouse_left
== 1) {wait (1);}
snd_play (playershot_snd,
70, 0);
ent_create
(bullet_mdl, player.pos, move_bullet);
bullets -=
1; // you have used a bullet
}

When you press the LMB function player_shoots would run several times if it wouldn't be stopped by exclusive_global. We wait until the player releases the LMB, we play a shot sound and then we create a bullet ans we move it using the same move_bullet function that was discussed above. Finally, we decrease the number of bullets.

The last function in this game runs when the player is hit by an enemy bullet:

function decrease_players_health()
{
wait (1);
if (player.skill1 > 0)
{
player.skill1 -= 10;
// decrease player's health with 10 points every shot
}
else
{
player.skill1 = 0;
}
}

If the player is alive, we decrease 10 point from his health; if the player is already dead we set his health to 0 (it isn't professional to display health values that go below zero).


Untitled

Access code

Imagine yourself in a situation like this: you have killed dozens of enemies and now you're getting really close to the end of the level. You take a turn and suddenly you find yourself in front of the door that leads to the toxic waste area exit! You try to open the door, but it won't work. But wait! There's a dial pad on the wall nearby! You try the master password (1234) but it isn't working...

string password_str = "6239";

I took a good look at the line of code above so now I'm pretty sure that the access code is 6239. Let me click the right mouse button (to show the mouse cursor) and then I will click the dial pad.

The pass was good because the door has opened! Press the asterisk ( * ) key to hide the dial pad and let's take a look at the code.

I have used a small sprite for the dial pad that appears on the wall; the action attached to the sprite is really simple:

action
dial_small
{
my.oriented = on;
my.passable = on;
my.enable_click = on;
my.event = show_dialpad;
}

The sprite is oriented, passable and sensitive to mouse clicks; as soon as we click it, its event function runs:

function
show_dialpad()
{
number_of_digits = 0;
str_cpy (guess_str, ""); // reset the string
dialpad_pan.visible = on;
}

The function above sets number_of_digits to zero, resets guess_str (the string that will hold the pass input by the player) and displays the dialpad_pan panel:

panel dialpad_pan
{
bmap = dialpad_pcx;
pos_x = 0;
pos_y = 0;
layer = 10;

     button = 143, 8, one_pcx, one_pcx, one_pcx, check_code,
null, null;
button = 270, 8, two_pcx, two_pcx, two_pcx, check_code, null, null;
button = 398, 8, three_pcx,
three_pcx, three_pcx, check_code, null, null;
button = 143, 128, four_pcx, four_pcx, four_pcx,
check_code, null, null;
button = 272, 128, five_pcx, five_pcx, five_pcx, check_code, null, null;
button = 397, 129, six_pcx,
six_pcx, six_pcx, check_code, null, null;
button = 144, 248, seven_pcx, seven_pcx, seven_pcx, check_code, null, null;
button = 273, 249, eight_pcx,
eight_pcx, eight_pcx, check_code, null, null;
button = 397, 250, nine_pcx,
nine_pcx, nine_pcx, check_code, null, null;
button = 146, 368, zero_pcx, zero_pcx, zero_pcx,
check_code, null, null;
button = 272, 370, asterisk_pcx, asterisk_pcx, asterisk_pcx, check_code, null,
null;

flags = overlay, refresh;
}

The dial pad consists of a simple panel with 11 buttons. Every time we click a button, function check_code runs and gets the number of the button that was clicked:

function check_code(button)
{
number_of_digits += 1;
snd_play (dial_wav, 30, 0);
if (button == 1)
{
str_cat (guess_str,
"1");
}

..............................................

     if (button == 9)
{
str_cat (guess_str,
"9");
}
if (button == 10)
{
str_cat (guess_str,
"0");
}
if ((button == 11) || (number_of_digits
> 4))
{
while (mouse_left
== 1) {wait (1);}
sleep (0.5);
snd_play (error_wav,
70, 0);
dialpad_pan.visible
= off;
}
}

Every time we press one of the buttons on dialpad_pan, we increment number_of_digits (the number of characters in guess_str). A sound is played and a character ("1", "2", ... , "0") is added to guess_str. If we press the 11th button ( * ) or if the number of digits has exceeded the length of our password (6239 has 4 digits) we wait until the left mouse button is released and then we play an error_wav sound and the hide dialpad_pan.

entity* locked_door;

action new_door
{
locked_door = me;
}

The door has a simple action attached to it; we set its name to "locked_door" using a previoulsy defined entity pointer. The last function compares password_str and guess_str in a while loop:

starter compare_strings()
{
while (1)
{
if (str_cmp (password_str,
guess_str) == 1) // pass ok?
{
snd_play (success_wav, 20, 0);
dialpad_pan.visible = off;
while (locked_door.pan < 180) // unlock the door!
{
locked_door.pan += 2 * time;
wait (1);
}
locked_door.pan = 180;
break; // get out of the while loop
}
wait (1);
}
}

It's a starter function, so it will run by itself at game start. If password_str (set to "6239") and guess_str match, we play success_wav, we hide the dial pad panel and then we use a while loop to change the locked_door pan from 90 to 180 degrees. You might need to change 180 to another angle that works ok for your door, depending on its orientation. We set the final pan angle for the door to 180 degrees (could have been set to 180.2 degrees, etc) and then we get out of the while loop using "break". This way we make sure that the door will open only once.

That was all! You can use the loop that rotates the door for a sliding door; change the x and / or y of your sliding door inside the while loop and you're ready to go!


Untitled

Adventure cameras

Many of you have used my "Resident Evil" - type cameras from Aum4. Please allow me to remind you how those cameras work: you move the player in the level and the closest camera to the player takes control. I believe that this system works ok for most games, but some of you have had problems with multiple story buildings, while others would have liked to be able to specify individual sensitivity areas for each camera. The code from this article fixes all these problems.

view adv_camera{ }

starter
init_cameras()
{
sleep (0.5);
camera.visible
= off;
adv_camera.pos_x = 0;
adv_camera.pos_y
= 0;
adv_camera.size_x = screen_size.x;
adv_camera.size_y
= screen_size.y;
adv_camera.visible = on;
adv_camera.x
= 820;
adv_camera.y = -1150;
adv_camera.z =
620;
adv_camera.pan = 120;
adv_camera.tilt =
-25;
adv_camera.roll = 0;
}

I have defined a new view named adv_camera. The starter function waits until the level is loaded, disables the default "camera" view, sets the position and size for adv_camera and then makes it visible. The last lines of code set the initial position and angles for adv_camera; this is what the player will see (the starting point for your game) when you run the level.

//
uses cam_number, cam_range
action detect_player
{
my.invisible
= on; // hide the model
my.passable = on;
while
(player == null) {wait (1);} // wait until the player is loaded
if (my.cam_number == 0) {beep; beep; beep; exit;}
if (my.cam_range
== 0) {beep; beep; beep; exit;}
while (1)
{
temp.x = 360; // horizontal scanning angle
temp.y = 180; // vertical scanning angle
temp.z = my.cam_range; // scanning range
scan_entity (my.x, temp);
if (result > 0) // got something?
{
set_camera(my.cam_number);
// then set the proper camera position and angles
}
wait (1);
}
}

Place several models in the level, attach them the "detect_player" action and they will turn into invisible player detecting devices. You can set their scanning ranges (cam_range) and their number (cam_number) in Wed: the first device will have its cam_number set to 1, the second device will have cam_number = 2 and so on. Oh, and if you forget to set cam_number and / or cam_range for one of the devices the engine will shut down. How do these things work? They simply scan around them; when the player comes close to the 3rd device (just an example) we call function set_camera(3) which will set the position and angles for the 3rd camera.

function
set_camera(number)
{
if (number == 1)
{
adv_camera.x = -40;
adv_camera.y = 340;
adv_camera.z = 1260;
adv_camera.pan = 270;
adv_camera.tilt = -72;
adv_camera.roll = 0;
}
........................
if (number
== 5)
{
adv_camera.x = 1900;
adv_camera.y = 1460;
adv_camera.z = 800;
adv_camera.pan = 220;
adv_camera.tilt = -35;
adv_camera.roll = 0;
}
}

The function above sets the proper adv_camera position and angles depending on the number that was passed as a parameter to it. If the player is close to the model that has its "cam_number" set to 1 in Wed, the camera will be set to x = -40, y = 340, z = 1260, pan = 270, tilt = -72 and roll = 0, get it? I'm sure that some of you would like to know how did I come up with the values used in function set_camera, so here's the answer: I have created the level, I have run it as a wmb file (without using any script) and I have moved the camera to a convenient position for the first adv_camera set of values. F11 has revealed the current x, y, z, pan, tilt, roll angles, so I wrote them down, and then I moved to the second position, and so on.


Untitled

Alarm clock

The clock code below allows you to set the alarm time using hours and minutes; the alarm will yell its annoying sound for a minute.

The main panel displays the clock picture and the four buttons (arrows) used to set the alarm time: + - hours and + - minutes.

panel
main_pan // main panel
{
bmap = main_pcx;
pos_x = 0;
pos_y = 0;
layer = 10;

     button = 260, 260, arrowup_pcx, arrowup_pcx,
arrowup_pcx, null, hours_up, null;
button = 280, 260, arrowdown_pcx, arrowdown_pcx, arrowdown_pcx, null, hours_down,
null;
button = 420, 260, arrowup_pcx, arrowup_pcx, arrowup_pcx, null, minutes_up, null;
button = 440, 260, arrowdown_pcx,
arrowdown_pcx, arrowdown_pcx, null, minutes_down, null;

flags = overlay, refresh, d3d, visible;
}

Clock_text shows the current time like this: hh:mm ss and alarm_text show the alarm time like this hh:mm. You know that h comes from hours, m = minutes, s = seconds, right?

text clock_text
{
layer = 20;
pos_x = 270;
pos_y = 200;
font = lcd_font;
string = clock_string;
flags = visible;
}

text
alarm_text
{
layer = 20;
pos_x = 300;
pos_y = 260;
font = lcd_font;
string = alarm_string;
flags = visible;
}

The most important part of the code is included in main:

function
main()
{
screen_color.red = 0;
screen_color.green = 0;
screen_color.blue = 0;
mouse_map = mouse_pcx;
mouse_mode = 2;
while (1)
{
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;

This main function isn't loading any level; we set the background color to rgb = 000, we use mouse_pcx for the mouse pointer and we show the cursor. The first 2 lines inside the while loop make sure that we can move the mouse on the panel.

str_for_num(clock_string, sys_hours);
str_cat(clock_string, ":");
if (sys_minutes < 10)
{
str_cat(clock_string, "0");
}
str_for_num(temp_string, sys_minutes); // read sys_minutes and convert it to string
str_cat(clock_string, temp_string); // add minutes to clock_string
str_cat(clock_string, " "); // leave a blank space before adding seconds
if (sys_seconds < 10) // add a "0" if the seconds are displayed with a digit
{
str_cat(clock_string, "0");
}
str_for_num(temp_string, sys_seconds); // read sys_seconds and convert it to string
str_cat(clock_string, temp_string); // add seconds to clock_string

We read sys_hours, we convert it from number to string and we put it in clock_string. We add ":" to the string; so far we have obtained "hh:". If sys_minutes < 10, the clock would display something like this: "23: 2" so we add a zero after ":" to make the clock show something like this: "23:02". Now we can read sys_minutes, we convert it to string, we add the minutes to clock_string and then we leave a space " " before adding the seconds. If sys_seconds < 10 we add another zero to the string and then we read sys_seconds, we convert it to string and then we add it to clock_string, obtaining something like this: "23:02 45".

Let's see the code that takes care of the alarm:

str_for_num(alarm_string, alarm_hours); // convert alarm_hours to string
str_cat(alarm_string, ":"); // add :
if (alarm_minutes < 10) // add a "0" if the minutes are displayed with a digit
{
str_cat(alarm_string, "0");
}
str_for_num(temp_string, alarm_minutes); // convert alarm_minutes to string
str_cat(alarm_string, temp_string); // add alarm_minutes to alarm_string

          if ((alarm_hours
== sys_hours) && (alarm_minutes == sys_minutes)) // sound the alarm
{
start_alarm();
}

I have set these (default) values for alarm_hours and alarm_minutes: 12 and 30. We convert alarm_hours to string, we add ":", we add a zero if alarm_minutes < 10, we convert alarm_minutes to string and then we add it to alarm_string. If alarm_hours (set by us) = sys_hours (the pc clock) and alarm_minutes (set by us) = sys_minutes (the pc clock) the alarm will start and it will run for a minute, until alarm_minutes != sys_minutes.

wait (1);
}
}

Function start_alarm is easy:

function
start_alarm()
{
exclusive_global; // don't allow multiple instance of this function to run
while (snd_playing(alarm_handle))
{wait (1);}
alarm_handle = snd_play (alarm_wav, 100, 0);
}

We don't allow multiple instances of this function to run and we play a sound in a loop, waiting for it to finish before starting to play it again.

The rest of the code is used to set the alarm time when we press one of the buttons (arrows):

function hours_up()
{
if (alarm_hours < 23)
{
alarm_hours +=
1;
}
}

function
hours_down()
{
if (alarm_hours > 0)
{
alarm_hours -=
1;
}
}

function
minutes_up()
{
if (alarm_minutes < 59)
{
alarm_minutes +=
1;
}
}

function
minutes_down()
{
if (alarm_minutes > 0)
{
alarm_minutes -=
1;
}
}

There's nothing special here; if you click the hours up arrow, alarm_hours will be incremented until it reaches 23. The rest of the functions are similar.


Untitled

Animated hud

Do you remember those good old games where the player had a little picture as a hud element? That picture was showing a devastated player face when his health was going down to zero and a happier face when the health was growing up to 100. That's exactly what this piece of code does.

panel
player_panel // displays player's health
{
pos_x = 0;
pos_y = 0;
digits = 160, 70,
3, health_font, 1, player._health;
flags = d3d, refresh, visible;
}

panel
health1_panel // displays the 1st picture
{
bmap = health1_pcx;
pos_x = 0;
pos_y = 0;
flags = d3d, refresh;
}

panel
health2_panel // displays the 2nd picture
{
bmap = health2_pcx;
pos_x = 0;
pos_y = 0;
flags = d3d, refresh;
}

panel
health3_panel // displays the 3rd picture
{
bmap = health3_pcx;
pos_x = 0;
pos_y = 0;
flags = d3d, refresh;
}

panel
health4_panel // displays the 4th picture
{
bmap = health4_pcx;
pos_x = 0;
pos_y = 0;
flags = d3d, refresh;
}

panel
health5_panel // displays the 5th picture
{
bmap = health5_pcx;
pos_x = 0;
pos_y = 0;
flags = d3d, refresh;
}

You can see that I'm using a simple panel, without any bmap to show player's health; I need a panel because the health is displayed using a "digit". The other 5 panels are nothing more than the pictures that will be displayed on the panel.

Maybe it is a good moment to tell you that if you are using the templates, player's health is stored in player._health - that's what the "digit" displays. I needed something to decrease player's health so I have created this action:

action player_killer
{
while (player == null) {wait (1);}
while (1)
{
if (vec_dist(my.x,
player.x) < 50 && player._health > 0)
{
player._health -= 0.5 * time;
}

          if (player._health
> 80)
{
health1_panel.visible = on;
}
else
{
if (player._health > 60)
{
health2_panel.visible = on;
}
else
{
if (player._health > 40)
{
health3_panel.visible = on;
}
else
{
if (player._health > 20)
{
health4_panel.visible = on;
}
else
{
health5_panel.visible = on;
}
}
}
}
wait (1);
}
}

This action is attached to the guard.mdl model that is placed near the player in my example level. Of course that you can use this code for a laser barrier, etc. We can't predict if action player_killer will run before player_walk_fight that is attached to the player. If player_killer runs first, the player isn't created so player._health in the 3rd line doesn't make sense. The first line in the action fixes the problem; player_killer will start running only after the player has been created.

If the player comes closer than 50 quants and he's still alive, his health is decreased with the speed given by 0.5 * time. Here's what happens:
- if player._health > 80 we display the health1 panel;
- if player._health = 60...80 we display health2;
- if player._health = 40...60 we display health3;
- if player._health = 20...40 we display health4;
- if player._health = 0...20 we display health5;

The action is a little more complicated; download Aum12.zip and you'll see that we need to make invisible the panels that aren't supposed to appear on the screen. The code in these articles is only for learning; the fully functional versions are zipped inside Aumxx.zip.


Untitled

Animated sprites

Every few weeks I hear this question: is it possible to have an animated sprite in a panel? Normally, I would answer "no" but this magazine proves me once again that I am wrong; we can create a sprite "entity" and place it anywhere on the screen.

First of all we create the panel:

panel
energy_pan // background panel
{
bmap = panel_pcx;
layer = 20;
pos_x = 0;
pos_y = 0;
flags = d3d, overlay, refresh,
visible;
}

Then we create the sprite entity, our animated sprite:

entity
energy_sprite
{
type = <energy+10.pcx>;
layer = 25;
view = camera;
x = 350;
y = 134;
z = 124;
flags = visible, flare, bright;
}

It is important to choose the correct x y z values because they will help you place the sprite in the place and with the size you need. All we need to do now is to change the animation frames using a simple function:

function init_animsprite()
{
while (1)
{
energy_sprite.frame += 0.3 * time;
if (energy_sprite.frame > 10)
{
energy_sprite.frame %= 10; // loop
}
wait (1);
}
}

You can see that the sprite has 10 animation frames that will run in a loop; the animation speed can be changed if you modify 0.3.


Untitled

Feature of the month: aspect

Acknex includes a lot of great features that aren't exploited properly; this month I want to show you what "aspect" does.

Practical use: flip the screen upside down (useful for space games), create impressive spell effects and more!

The keyword "aspect" determines the vertical to horizontal pixel size ratio. In other words, if you place a square model on your screen you can make it look like a rectangle if you change its "aspect". I have created two simple functions that use this great engine feature:

function
flip_level()
{
while (camera.aspect > -1)
{
camera.aspect -=
0.1 * time;
wait (1);
}
}

function
grow_level()
{
while (camera.aspect < 5)
{
camera.aspect +=
0.2 * time;
wait (1);
}
}

on_f = flip_level;
on_g = grow_level;

The first function will run when we press the "F" key; it will decrease camera.aspect from 1 (normal value at game start) to -1. When camera.aspect = -1, the image is flipped upside down.

The second function runs as soon as we press the "G" key; it will increase camera.aspect from 1 to 5, changing the aspect of the level: every pixel on x will correspond to 5 pixels on y. Look how cool are my rocks with aspect = 5! I would use this effect for a magical potion, etc.


Untitled

Blood pool

This snippet will teach you make your enemies bleed as soon as they die. I have kept the code as simple as possible:

define number_of_hits skill1;

action
enemy_blood
{
var blood_coords; // local var
my.enable_entity = on;
my.enable_impact = on;
my.event = enemy_hit;
my.number_of_hits = 0;
while(my.number_of_hits == 0) // standing
{
ent_cycle("stand",
my.skill20); // play "stand" animation
my.skill20 += 3
* time;
my.skill20 %= 100;
// loop
wait (1);
}

I have defined a local var named blood_coords. I know that you are too shy to ask but I will explain right here, right now what's with these local and global variables. A global variable is defined outside any action or function - it isn't enclosed by curly brackets. A global variable can be accessed from any other function or action. It is good to know that you can type ammo1 += 100 at the console and get 100 extra bullets for the weapon that uses ammo1, isn't it? A local variable is defined exactly like a global var, but its definition is placed inside the action or function, like the one in action enemy_blood. A local var is known only inside the action / function that has its definition inside it. If you would want to type blood_coords.x = 100; at the console it wouldn't work, get it? Local vars are just like entity skills: you can use them only within the action or function that has defined them. Let's get back to work now... The local var named blood_coords will be set to the origin of the blood spot. I could have used a global var but if you would have attached the same action to several models, every model could have changed blood_coords because this var would have been known to all the actions and functions. A local blood_coords var is recognized only inside its own action or function so the blood_coords value for one of the enemies can't interfere with other blood_coords value, used by another enemy.

The enemy is sensitive to impact; shoot it with a rocket to trigger its enemy_hit event. I have used number_of_hits as a name for skill1; as long as number_of_hits is zero, the enemy will play its "stand" animation in a loop

// the enemy was hit by a rocket - it must die
my.skill20 = 0;
while(my.skill20 < 60) // one shot animation, 60% of "death" because the last "death" frame is damaged - open the model in Med to see for yourself
{
ent_cycle("death", my.skill20); // play "death" animation
my.skill20 += 2 * time;
wait (1);
}
my.event = null; // don't react to other events from now on
// the enemy is dead here - time to create the blood pool
vec_set(blood_coords, my.x);
blood_coords.z -= 500;
trace_mode = ignore_me + ignore_models + ignore_sprites;
blood_coords.z = my.z - trace (my.x, blood_coords) + 2; // place the blood sprite 2 quants above the ground
ent_create(blood_pcx, blood_coords, create_blood);
}

When number_of_hits is greater than zero, the enemy has to die; we play its death animation (only 60% of its death frames because the last death frame is damaged - open the model in Med to see it). We set the event to null, we trace 500 quants below the enemy to get the position of the floor, we set blood_coords.z two quants above the ground and we create the blood sprite using function create_blood:

function create_blood()
{
my.passable = on;
my.oriented = on;
my.tilt = 90;
my.flare = on;
my.ambient = 100;
my.bright = on;
while (my.scale_x < 5)
{
my.scale_x += 0.05
* time;
my.scale_y = my.scale_x;
wait (1);
}
}

The blood sprite is passable, oriented and has its flare and bright flags set. The sprite will increase its size on x and y five times.

The function that triggers all this scary stuff is:

function
enemy_hit()
{
if (event_type == event_entity || event_type == event_impact)
{
if (you == player)
// collided with the player?
{
return; // do nothing
}
else // hit by
a rocket
{
my.number_of_hits += 1;
}
}
// add other events here
}

If the enemy has collided with an entity, it checks to see if that entity is the player. If the enemy was hit by a rocket, number_of_hits will be increased and the enemy will die.


Untitled

Bouncing ball

If you liked the many "Marbles" clones now you have the chance to create your own clone. We will write the code for a ball that can be controlled with the arrow keys and bounces all the time. I'm not going to use the physics engine at all, so the code will work ok with any Acknex edition.

function
main()
{
fps_max = 60;
level_load (level1_wmb);
wait (3);
while (1)
{
camera.x = ball.x - 200;
camera.y = ball.y;
camera.z = 100;
camera.tilt
= -45;
wait (1);
}
}

Every time you see function main() in one of the Aum scripts you know that we have a standalone project, right? This means that you can add more features to the existing code whenever you need them and it will (hopefully) continue to run ok. Our function limits the frame rate to 60 fps, loads a simple level, waits until it is loaded, and then sets the proper camera position and angle. Please note that the camera has a fixed height (100 quants) and stays 200 quants behind the ball all the time.

action
bouncing_ball
{
ball = my;
my.enable_block
= on;
my.enable_entity = on;
my.event = reverse_speed;
while (1)
{
if (key_cuu == on)
{
ball_speed.x = 5 * time;
my.tilt -= 8 * time;
}
if (key_cud == on)
{
ball_speed.x = -5 * time;
my.tilt += 8 * time;
}
if ((key_cuu == off) && (key_cud == off))
{
ball_speed.x =
0;
}
if
(key_cul == on)
{
ball_speed.y = 5 * time;
my.roll
+= 8 * time;
}
if (key_cur
== on)
{
ball_speed.y = -5 * time;
my.roll -=
8 * time;
}

You can see that I have given the ball pointer an original name (ball); it is sensitive to level blocks and entities, running its reverse_speed function if it collides with something. If we press the "up" arrow key, the ball will move forward with the speed given by 5 * time and it will change its tilt angle with the speed given by -8 * time because we want to create the illusion that the ball rotates. The same things happen when the player presses the "down", "left" or "right" arrow keys; of course that we use different speeds, movement axis and rotation angles.

     if ((key_cuu == off) && (key_cud == off))
{
ball_speed.x = 0;
}
if ((key_cul == off) && (key_cur
== off))
{
ball_speed.y
= 0;
}
vec_set (temp,
my.x);
temp.z -= 10000;
trace_mode = ignore_me + ignore_passable + use_box;
trace_dist
= trace(my.x, temp);
if (trace_dist > max_height)
// the ball has reached its maximum height?
{
ball_speed.z = -abs(ball_speed.z); // then give
the ball a negative speed
}
ent_move
(nullvector, ball_speed);
wait(1);
}
}

If the player isn't pressing the arrow keys, ball_speed.x and / or ball_speed.y will be reset; the ball needs to stop if we don't press the keys at all. The next few lines use a "trace" instruction to detect the floor level. I have decided to use a thick tracing ray (use_box) because I didn't want the ball to get stuck close to the margin of the ramp or in a similar situation.

If trace_dist is greater than max_height (set to 70 quants at the beginning of the script file) the ball has reached its maximum height, so it must change its speed from positive to negative because we want it to fall to the ground from now on. I chose 70 quants for the maximum height above the floor level, but you can use any other value here.

function
reverse_speed()
{
ball_speed.z *= -1;
}

The event function simply changes the sign of the speed (from positive to negative, or from negative to positive) on impact.


Untitled

Breakable windows

Read this article and you will find out how to create a window that breaks when you shoot it or when the player passes through it. The player should loose a part of its health if it does that, right?

action
breakable_glass
{
my.transparent = on;
my.enable_impact = on;
my.enable_entity = on;
my.enable_shoot = on;
my.event = glass_pieces;
}

The action above will be attached to your window entity and it will make it transparent and sensitive to impact, shoot or other entities. When one of these events is detected, function glass_pieces will run:

function
glass_pieces()
{
if (you == player) {player._health -= 20;} // decrease player's health if it breaks
the window by passing through it
my.passable = on;
snd_play (breakglass_snd, 50, 0);
my.skill10 = 0;
my.invisible = on;
while (my.skill10 < 15)
{
create (gibglass_mdl,
my.pos, gib_glass);
my.skill10 += 1;
wait (1);
}
ent_remove (me);
}

If the player passes through the window, its health is decreased with 20 points. The window is made passable because the glass giblets must pass through it, a sound is played and then the window is made invisible. You might wonder why I haven't removed the window yet - the answer is that I want to use its skill10 as a counter because we must create 15 glass fragments inside the while loop. As soon as all the giblets are created, the big window is removed.

Time to look at the gib_glass() function:

function
gib_glass()
{
my.transparent = on;
my.skill1 = random(200) - 100;
my.skill2 = (random(100) + 25) * sign (my.y - player.y);
my.skill3 = -random(25) -
10;
my.skill1 *= time; // all
the values (skill1..skill3) are multiplied with time
my.pan = random(360);
my.tilt = random(360);
my.roll = random(360);
while(my.skill40 < 10)
{
move_mode = ignore_passable;
ent_move (nullvector,
my.skill1);
my.skill3 -= 20
* time;
my.skill40 += time;
wait(1);
}
my.passable = on;
while (my.alpha > 0)
{
my.alpha -= 0.3
* time;
wait (1);
}
ent_remove (me);
}

The giblets are transparent; we use skill1..skill3 as a local var. Every glass piece will move in a random direction using ent_move; you can see that the movement on x ranges from -100 to 100 (skill1), the movement on z ranges from -35 to -25 (skill3) because the giblets have to fall down. What's with the movement on y (skill2) and why should it depend on player's position?

Let's see if an image is more efficient than 1000 words:

If the player shoots the window, many of the giblets should fall on the other side of the window. Skill2 (speed on y) will change its sign depending on the position of the player and the window.

The giblets get random pan, tilt, roll values and move inside a while loop. Skill3 (speed on z) decreases more and more, placing the gibs closer and closer to the ground with every frame. When the movement stops, we make the giblets passable and then we decrease their alpha until they disappear.


Untitled

"Buy now!"

Your game is finished and you've uploaded a great demo on your website. But haven't you forgotten something? Yes, you need to add that "Buy my game" button to your demo.

This is one of those rare situations when you will want to use the sdk; don't worry, I have included the dll, so you won't have to write a single line of code if you don't want to. If you are wondering how to use the sdk check Aum31 and you will learn how to create a dll from scratch.

Let's take a look at the cpp file:

// buynow.cpp : Defines the entry point for the DLL application.
//

#include "stdafx.h"
#include "a5dll.h"
#include
"a5funcs.h"

int nSuccess;

BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID
lpReserved)
{
return TRUE;
}

DLLFUNC fixed Exequte(long command)
{
fixed *temp =
(fixed *)a5dll_getwdlobj("temp");
A4_STRING *my_command = (A4_STRING
*)command;
nSuccess = WinExec (my_command -> chars, SW_MAXIMIZE);
return INT2FIX(nSuccess);
}

The red lines were added by me, the rest of the code being added by the MS VC++ 6.0 wizard. My "Exequte" function grabs the predefined "temp" variable (read Aum31 to see why we need to do that) and then gets a pointer named my_command to the string that is passed to the function. Finally, we use WinExec to run the string (program + arguments) that is passed to Exequte, returning the result of the operation. The variable named nSuccess will return a value that is greater than 31 only if WinExec succeeds.

Some of the advanced programmers will want to use CreateProcess because WinExec is obsolete, but it does its job great here and (that's a secret they don't want you to know) WinExec actually calls CreateProcess. Let's examine the content of the script file now:

var success = 0;
var_nsave dll_handle;

text error_txt
{
pos_x = 200;
pos_y
= 200;
layer = 30;
font = std_font;
string = "Error while trying to run the command!";
}

panel outro_pan // exit panel
{
bmap = outro_tga;
layer = 20;
pos_x = 120;
pos_y = 100;
button = 133, 100, buyme2_tga, buyme1_tga, buyme2_tga, buy_now, null, null;
button = 133, 180, nothanks2_tga, nothanks1_tga, nothanks2_tga, shut_down, null,
null;
flags = overlay, refresh, visible;
}

I have defined a simple text that displays an error message if WinExec fails. "Buy now!" should be a regular button on a panel, and it must run function buy_now when it is clicked:

function buy_now()
{
dll_handle = dll_open ("buynow.dll");
success = Exequte("C:\\Program Files\\Internet Explorer\\iexplore.exe
www.randombyte.com");
if (success <= 31) // WinExec failed?
{
error_txt.visible = on; // show an error
message
}
dll_close (dll_handle);
}

function shut_down()
{
exit;
}

The function above opens the dll, and runs Internet Explorer with my site given as parameter. Please note the C-Style notation "\\" for the paths. If WinExec fails (success <= 31) we display the error message. If the player presses the "No thanks!" button, function shut_down will shut down the engine.

That's all! You should make outro_pan visible only when the potential customer tries to shut down the game. One more thing: you can use this dll to run any application you can think of; let me try something...

success = Exequte("C:\\Program Files\\Microsoft Office\\Office10\\winword.exe office.wdl");

It works! This line starts Microsoft Word and opens the existing office.wdl file for editing.


Untitled

New debug code

I'm pretty sure that you know the rows of numbers that appear when you press the "D" key while running a level. I have to admit that I was pretty scared when I saw all those values (I'm still scared, but please keep the secret) mostly because I couldn't tell what they were. This new debug code will show a panel with many important figures and text that explains what they are. I hope that you will be able to optimize your levels with this new tool.

Let's take a look at the interesting values offered by the engine:
- num_actions, number of currently running actions;
- num_visents, number of visible entities;
- num_visentpolys, visible model and sprite polys;
- num_vismappolys, visible map and wmb polys;
- d3d_texskins, memory used by models and sprites;
- d3d_texbmaps, memory used by panels and "entity" definitions;
- d3d_texsurfs, memory used by wmb entities;
- d3d_texsmaps, memory used by the shadow maps;
- d3d_texfree, available video memory.

All these values (and a few more) will be displayed on a panel that has this definition:

panel
debug_panel
{
bmap = debug_pcx;
layer = 30;
pos_x = 0;
pos_y = 0;

digits = 40,20,5,arial10_font,1,num_actions;
digits = 40,60,5,arial10_font,1,num_visents;
digits = 40,100,5,arial10_font,1,num_visentpolys;
digits = 40,142,5,arial10_font,1,num_vismappolys;
digits = 40,186,5,arial10_font,1,temp_texskins;
digits = 40,230,5,arial10_font,1,temp_texbmaps;
digits = 40,274,5,arial10_font,1,temp_texsurfs;
digits = 40,321,5,arial10_font,1,temp_texsmaps;
digits = 40,364,5,arial10_font,1,temp_total;
digits = 40,410,5,arial10_font,1,temp_free;
digits = 40,455,5,arial10_font,16,temp_fps;

flags = overlay, refresh, d3d;
}

You can see that some of the values inside the panel definition appear without changes; the other values are calculated inside a while loop:

function
toggle_debug()
{
on_d = null; // deactivate the standard debug panel
while (1)
{
temp_texskins =
d3d_texskins / 1024;
temp_texbmaps =
d3d_texbmaps / 1024;
temp_texsurfs =
d3d_texsurfs / 1024;
temp_texsmaps =
d3d_texsmaps / 1024;
temp_total = (d3d_texskins
+ d3d_texbmaps + d3d_texsurfs + d3d_texsmaps) / 1024;
temp_free = d3d_texfree
/ 1024;
temp_fps = 0.9
* temp_fps + 0.1 / time;
if (key_d == 1)
{
while (key_d == 1) {wait (1);}
debug_panel.visible = (debug_panel.visible == off);
}
wait (1);
}
}

The first line deactivates the standard debug panel that appears when we press "D". All the values related to the video memory are expressed in Kb, so we divide them by 1024 to get them in Mb. We add all these values to get the memory used in the level and we compute the frame rate (1 / time) fast in order to see any frame rate drop / increase. The panel will appear / disappear when we press "D".

Please note that d3d_texfree can show a value that is bigger than the memory installed on your video card, because many modern 3D cards use different compression algorithms. Some of your customers will use older cards so it is better to keep an eye on "Total memory used" instead of "Texture free".


Untitled

Bow and arrow

This time I am going to take you through all the steps that are needed to create the code for a bow and arrows; please note that this example can be used to create (almost) any type of weapon and its bullets.

First of all we define an entity that will be used for the bow:

entity bow_entity

{

type = <bow.mdl>;

layer = 10;

view = camera;

x = 30;

y = 8;

z = -10;

}

We are using the same model (bow.mdl) for the bow that is being placed in the level in Wed. The bow rotates if it hasn't been picked up yet; when the player impacts with the bow, the bow will be removed and bow_entity is made visible. Bow_entity.frame = 2 will show the 2nd frame which contains the bow and the arrow.

action bow

{

my.enable_impact = me;

my.event = get_bow;

while (my != null) // or while (my)

{

my.pan += 3 * time;

wait (1);

}

}

function get_bow()

{

wait (1);

ent_remove (me);

bow_entity.frame = 2;

bow_entity.visible = on;

}

We are calling function init_bowarrow() in main:

function init_bowarrow()

{

while (1)

{

if ((mouse_left == 1 || key_ctrl == 1) && bow_entity.frame < 7)

{

bow_entity.frame += 2 * time;

}

else

{

if (bow_entity.frame >= 7 && arrow_fired == 0)

{

while (mouse_left == 1 || key_ctrl == 1) {wait (1);}

vec_set (temp, player.pos);

temp.z += 20;

ent_create (arrow_mdl, temp, move_arrow);

arrow_fired = 1; // a single arrow

}

else // the player isn't shooting

{

arrow_fired = 0; // enable firing

bow_entity.frame = 2;

}

}

wait (1);

}

}

If we press the LMB or the Ctrl key, we animate bow_entity by changing its frame parameter. If bow_entity.frame >= 7, we wait until the LMB or the Ctrl key are released (we do this because we want to disable autofire) and then we create an arrow 20 quants above player's origin. I chose this value because it looked real with my player + bow + arrow combination; you will have to try other values if you are using other models. If the player isn't shooting anymore, we enable firing and we set bow_entity's frame to 2 again.

The function associated to the arrow is:

function move_arrow()

{

wait (1);

my.enable_entity = on;

my.enable_block = on;

my.event = hurt_you;

my.passable = on;

my.pan = camera.pan;

my.tilt = camera.tilt;

my.skill12 = 50; // skill12, 13, 14 act like a var

my.skill13 = 0;

my.skill14 = 0;

my.skill12 *= time;

while (my.skill12 != 0) // moves until it hits something

{

if (vec_dist (my.x, player.x) < 100) {my.passable = on;}

// don't collide with the player

else {my.passable = off;}

ent_move (my.skill12, nullvector);

wait (1);

}

}

The arrow is sensitive to entities and level blocks; if it hits a block it will stuck in it and if it hits an entity it will disappear. I'm using skill12, 13, 14 as if they were a single var named skill12; I need to make sure that the first skill is divided by 3 (so I can start with skill3, 6, 9, 12, 15, etc). If the distance between the player and the arrow is smaller than 100 quants, the arrow is passable in order to prevent the arrow from hitting its creator.

The last function is:

function hurt_you()

{

wait (1);

if (event_type == event_entity)

{

my.skill12 = 0; // stop moving

you._HEALTH -= 80; // damage = 80;

ent_remove (me);

}

if (event_type == event_block)

{

my.skill12 = 0; // stop the arrow

my.passable = on; // it shouldn't stand in our way

}

}

If the rocket hits an entity, it will cause 80 hit points damage. If the entity is a tree sprite, it won't matter but if it is an enemy (robot2 in my example) you will see him running away after the first hit.

Here's a small tip: I have used the same model for the bow_entity and for the bow that is placed in Wed although it had an arrow and a hand attached to it all the time. Of course you wouldn't want to see the bow, the arrow and the hand attached to it waiting to be picked up. The models need to have the same number of triangles every frame, so I have decided to hide the hand and the arrow inside the bow (much smaller, of course, but they're there). I'm using the first bow.mdl frame before picking up the bow and frames 2..7 for the animated bow + arrow + hand.


Untitled

Camera follows a path

I have to be honest with you: I got this idea from Doug (I took a good look at his "bird follows path" code). The result of my work is different but he deserves credit here.

We don't want to modify the templates, so we define a new view named cutcamera. We have to place an entity (fish.mdl?) in the level and attach it the cutcamera_init action:

action
cutcamera_init
{
vec_set
(cut_center, my.pos);
my.skill20
= max_speed;
camera.visible
= off;
cutcamera.tilt -= 20;
cutcamera.x = 0;
cutcamera.y = 0;
cutcamera.size_x
= screen_size.x;
cutcamera.size_y
= screen_size.y;
ent_path("campath");
ent_waypoint(my._target_x, 1);

my.x = my._target_x;
my.y = my._target_y;
my.z = my._target_z;

cutcamera.visible = on; // make cutcamera visible

The camera will rotate to face the fish entity all the time. The fish can be placed anywhere in the level because it is invisible and passable so it won't get hurt if you do that. First of all we store the fish coordinates and the maximum speed. We disable the default (camera) view and we set the new cutcamera. The fish will move to the first waypoint in the "campath" path (that's why we store its initial coordinates).

    while(1)
{
cutcamera.x = my.x;
cutcamera.y =
my.y;
cutcamera.z =
my.z;
cutcamera.pan
= my.pan;

       vec_set(my._speed, my._target_x);
vec_sub(my._speed,
my.x);
if (vec_to_angle(my.pan, my._speed)
< 15)
{
ent_nextpoint(my._target_x);
}

       vec_set (temp, cut_center.x);
vec_sub (temp,
my.x);
vec_to_angle
(my.pan, temp);
vec_normalize(my._speed,
(max_speed * time));

if (key_s != 1)
{
ent_move(nullskill,
my._speed);
}

The fish moves on the 3D path and the cutcamera view is attached to it. There are two things that we must consider:
1) We have to set _speed so that the fish will move on the path
2) We have to rotate the fish (the view) so that it always points to cut_center.

You can see that cut_center doesn't have to be in the center of the path and the path can have any shape (don't forget that it is a 3D path, after all).

If we want to rotate the "my" object towards the "you" object we have to use 3 lines of code according to the manual:

vec_set (temp, you.x);
vec_sub (temp, my.x);
vec_to_angle (my.pan, temp)

I have used these lines (I could have used vec_rotate, too) to set the _speed vector in the direction of the next waypoint and to rotate the view towards cut_center (all the red dots point towards cut_center but I didn't want to make a mess of the picture). We normalize the _speed vector; we cut the length and keep its direction.

You might wonder what's with these lines:

if
(vec_to_angle(my.pan, my._speed) < 15)
{
ent_nextpoint(my._target_x);
}

I have used a little trick here: I am assuming that you are lazy and your path has less than a thousand points, so the camera shakes when it reaches the target (one of the points on campath) and has to change its direction (take a look).

I have zoomed a little so that you can see how player_dizzy++ every time we reach another point on the path. What can we do? The lines above are computing the distance between the fish and the next waypoint. When we get close enough to the destination point, we quit moving towards it and set the new destination point - here's the difference:
The green path is smoother; take a look at the red angles - bigger angles = smoother transitions. Before I forget, you can press S to stop the movement.

What's left to be added to this beautifully working piece of code? We could lower its speed when it reaches a certain area (let's say that we want to take a good look at the nuclear core in our level) then accelerate it back when it has left that area.

I have created this type of movement in my office.wmp example when the fish goes below 190 quants (I got this value in Wed)

if
(my.z < 190)
{
max_speed
= max (my.skill20 / 2, max_speed * 0.995);
}
else
{
max_speed = min (my.skill20, max_speed * 1.005);
}

We've stored max_speed in skill20 and now we lower it from 7 (initial value) to 3.5; please note that we aren't simply setting max_speed to 3.5 but we are constantly decreasing it (we multiply it by 0.995) until it reaches 3.5. When we exit our special area (my.z > 190) max_speed is constantly increased (multiplied by 1.005) from 3.5 to 7 which was stored in skill20, remember?

Of course that you can add multiple conditions like this:

if (my.x < 100 && my.y > 250 && my.y < 300 && my.z > 400)
{
..........................................................
}


Untitled

Car demo

This code was written for those of you that want to create a car game and need a standalone version of the player_drive action in the templates.

action my_car
{
player = me;

my.enable_block = on;
my.enable_entity = on;
my.event = increase_damage;

    while (1)
{
if (damage > 100) {return;}
if (car_speed
!= 0)
{
my.pan -=
0.2 * key_force.x * car_speed * time;
}
car_speed = 3 * key_force.y
* time + max (1 - time * 0.1, 0) * car_speed;
if (key_shiftl + key_shiftr
== 0)
{
ent_move
(car_speed, nullvector);
move_carview();
}
if (damage > 20)
{
temp
= damage / 20;
emit
(temp, player.x, car_smoke);
}
wait (1);
}
}

The car is moving as long as its damage is below 100. The car can rotate (change its pan angle) as long as car_speed != 0 - this way we obtain the "wheels" flag effect in the standard player_drive. The line that computes car_speed is similar to the one in the tutorial (for Tuesday) that comes with the A4 / A5 manual: v -> v + dv. Change 3 to change the speed and 0.2 to change the rotation speed.

If we press one of the shift keys, the car will stop (ent_move isn't executed anymore) and we can rotate the car without moving it by pressing two arrow keys (up + right, down + left, etc) at the same time. This is the only way to see my model in its full splendor so don't miss this opportunity!

The car can collide with level blocks and entities; when this happens, increase_damage is executed:

function
increase_damage()
{
if (event_type
== event_block)
{
damage += 0.006 * abs(car_speed);
}
if (event_type
== event_entity)
{
damage += 0.003 * abs(car_speed);
}
}

You can see that the damage depends on car_speed. If the car collides with blocks, the damage is bigger. Btw, I'm using abs(car_speed) in this function because if you would hit a block with the back of the car, the damage would be lowered :) because of the negative car_speed.

If damage is bigger than 20 the car will emit smoke (bigger damage = thicker smoke):

function car_smoke()
{
if (my_age
== 0)
{
my_map = carsmoke_map;
my_speed.x =
random(1) - 0.5;
my_speed.y =
random(1) - 0.5;
my_speed.z =
random(1) + 1;
my_size = random(500)
+ 300;
my_flare = on;
}
my_alpha
= 100 - my_age * 4;
my_size
= min (1000, my_size + 2 * time);
if (my_age
> 25)
{
my_action = null;
}
}

There's nothing special in this function: the particles grow transparent at the end of their life; their size grows up to 1000. You should emit smoke from one of the vertices in the back of the car. The last function is:

function
move_carview()
{
camera.x
= player.x - camera_distance * cos(player.pan);
camera.y = player.y - camera_distance * sin(player.pan);
camera.z = player.z + camera_height;
camera.pan += 0.2 * ang (player.pan
- camera.pan);
}

We've discussed this type of camera in a previous AUM edition; please note that the camera will follow the car with a little delay (change 0.2 to 0.05 or 0.5).

I almost forgot: the action stand_still is attached to every guard.mdl in the level and stops their animations.


Untitled

Cash and carry

Ever wanted to have a script that allows you to get money and buy items in your game? Here it is.

The code consists of two parts: the action + function that gets you the money and the action + function that helps you spend it.

action get_cash
{
my.enable_impact = on;
my.event = add_money;
}

function
add_money()
{
wait
(1);
if (my.skill1 == 0) {my.skill1 = 20;} //
default = 20 bucks
if (cash == 0) {cash_pan.visible
= on;}
cash += my.skill1;
snd_play (gotcash_snd,
70, 0);
ent_remove (me);
}

I have used the lampe.mdl (a lamp) for the model that gets you the cash (if you turn it off, you can save some cash after all :). When the player (or something else, like player's bullet) impacts with the lamp, function add_money() is executed. If we forget to set lamp's skill1, it will be set to 20 - this means that we get 20 bucks. We increased the cash variable, we play a sound and then we remove the entity.

action buy_items
{
my.enable_click = on;
my.event = buy_me;
}

function
buy_me()
{
wait
(1);
if (event_type != event_click || cash <
my.skill1) {return;}
cash -= my.skill1;
snd_play (spentcash_snd,
70, 0);
ent_remove (me);
}

The action that helps you spend the money (I should have named it action wife :) is similar, but uses the mouse to look more fancy. To buy an object you have to right click to bring the mouse pointer on, then left click on as many objects (fish.mdl) as you want and you will "buy" them if you have enough cash. Set skill1 for the objects that can be bought; if skill1 = 0, the objects are free (you don't need cash to buy them).

I'm using a simple panel to display the amount of cash available to the player:

panel cash_pan
{
bmap
= cash_map;
layer = 20;
pos_x = 0;
pos_y = 0;
digits = 40, 22, 4, arial_font, 1, cash;
flags = transparent, overlay, refresh,
d3d;
}

This panel is made visible as soon as you pick up the first object that gets you the cash.


Untitled

Cheat codes

This time I'm going to show you how to add cheat codes to your game. I have implemented cheats for health and armor but you can add as many cheats as you want.

First of all we define a few strings; if you take a look at the definitions below you will see that we have an empty string, a string that will store the user input, another one that will display "Unknown command" if we type something that isn't recognized and two strings that contain the cheat codes for health and armor.

string empty_str
= "
";
string cheat_str = "
";
string unknown_str = "Unknown command";
string health_str = "gimmehealth";
string armor_str = "gimmearmor";

We define two texts; one of them will be used for the cheats and the other one for the "Unknown command" message.

text
cheats_txt
{
pos_x
= 0;
pos_y = 0;
layer = 20;
font = standard_font;
string = cheat_str;
}

text
unknown_txt
{
pos_x
= 0;
pos_y = 0;
layer = 20;
font = standard_font;
string = unknown_str;
}

The function below is called in main so it will be executed at game start:

function init_cheats()
{
player._health
= 30;
while
(1)
{
if (key_alt + key_p == 2)
{
cheats_txt.visible
= on;
str_cpy
(cheat_str, empty_str);
inkey
cheat_str;
if
((player != null) && (str_cmpi (cheat_str, health_str) == 1))
{
player._health = 100;
}
if ((player
!= null) && (str_cmpi (cheat_str, armor_str) == 1))
{
player._armor = 100;
}
if (str_cmpi
(cheat_str, health_str) + str_cmpi (cheat_str, armor_str) == 0)
{
unknown_txt.pos_x = screen_size.x / 2;
unknown_txt.pos_y = screen_size.y / 2;
unknown_txt.visible = on;
beep;
waitt (32);
unknown_txt.visible = off;
}
cheats_txt.visible
= off;
}
wait (1);
}
}

We are setting player's health to 30 because we want to see it recovering :) when we type "gimmehealth". The cheats (passwords, if you like) can be typed in if we press Alt + P. If this happens, we show cheats_txt, we clear previous inputs and we show the cursor; as soon as we type Enter, the input is stored in cheat_str.

If the player exists (it could be removed from the level) and cheat_str = health_str, the player will get 100 health points; if cheat_str = armor_str the player will get 100 armor points. I am using str_cmpi which compares the strings without case sensitivity so GimmEHeAltH will work ok.

If we type something wrong (or we don't know the cheat codes) unknown_txt is placed near the middle of the screen and made visible for two seconds, then cheats_txt will also disappear. You can add as many cheats as you want by simply adding their strings and checking:

if
((player != null) && (str_cmpi (cheat_str, yourcheat_str) == 1))
{
your_cheat_here;


Clocks and string instructions

I was surprised to see the you can create a real time clock so easy; I have created two strings:

STRING clock_string, " "; // hh:mm ss = 8 chars that hold data like this: 17:34 56
STRING temp_string, " "; // 2 chars storing temporary data that will be added to clock_string from time to time

I'm using a clock_text that will show the string clock_string (the time) on screen; function start_clock does all the job.

function
start_clock()
{
WHILE (1)
{
str_for_num(clock_string, SYS_HOURS);
str_cat(clock_string, ":");
IF (SYS_MINUTES < 10)
{
str_cat(clock_string, "0");
}
str_for_num(temp_string, SYS_MINUTES);
str_cat(clock_string, temp_string);
str_cat(clock_string, " ");
IF (SYS_SECONDS < 10)
{
str_cat(clock_string, "0");
}
str_for_num(temp_string, SYS_SECONDS);
str_cat(clock_string, temp_string);
WAIT (1);
}
}

Let's go through this function line by line:

str_for_num(clock_string, SYS_HOURS);
Reads SYS_HOURS which is a number, converts it to string and puts it in clock_string (the first two characters)

str_cat(clock_string, ":");
Adds the : character to the string (in the 3rd position) to create a clock that looks like this 12:32

IF (SYS_MINUTES < 10) // add a "0" if the minutes are displayed with a digit
{
str_cat(clock_string, "0");
}
SYS_MINUTES will show 12: 7 instead of 12:07 for SYS_MINUTES below 10, so we add a 0 when needed

str_for_num(temp_string, SYS_MINUTES);
Reads SYS_MINUTES and converts it to string (we use temp_string then we will copy it in clock_string)

str_cat(clock_string, temp_string);
We add the minutes to clock_string; the minutes should be the 4th and 5th characters in clock_string

str_cat(clock_string, " ");
Add a space in the 6th position of the screen

IF (SYS_SECONDS < 10) // add a "0" if the seconds are displayed with a digit
{
str_cat(clock_string, "0");
}
We add a "0" if the seconds are below 10

str_for_num(temp_string, SYS_SECONDS);
We read SYS_SECONDS and convert it to string; the result is placed in temp_string

str_cat(clock_string, temp_string); // add seconds to clock_string
We add seconds to clock_string (the 7th and 8th position)

All this stuff is placed in a WHILE(1) loop, so it executes all the time.


Untitled

Compass

This article will tell you how to create a simple compass that keeps its size regardless of the screen resolution. If you have tried to work with panels before, you know that they change their relative size on the screen when you increase or decrease the screen resolution; the bigger video the resolution is the smaller will the panels will look. Why is that? The panels have kept their size in pixels but the number of pixels on screen has increased. You can resize the panels using the new scale_x and scale_y, which sometimes will make your panels look a bit uglier, or you can place your 2D elements in the 3D world as "entity" definitions.

entity
compass {  type = <compass.tga>; layer = 10; scale_x = 0.25; scale_y
= 0.25; x = 190; y = -85; z = -65; view = camera; flags = overlay, visible; }

entity needle { type = <needle.tga>; layer = 20; scale_x = 0.15; scale_y
= 0.15; x = 190; y = -85; z = -65; view = camera; flags = overlay, nofilter, visible;
}

These "entity" definitions use two tga files; you can see that I am setting their size, as well as their position in the 3D world (x sets the distance to the camera, y sets the position on the x axis of the screen and z sets the position on the y axis of the screen. The needle has layer = 20 so it will appear over compass' layer = 10. And the final touch: the needle has its "nofilter" flag set, which will render the needle sharper, without giving it that blurry look.

starter
set_compass()
{
 while (player == null) {wait (1);} // wait until the player
is loaded in the level
 while (1)
 {
 needle.roll = -player.pan; wait
(1);
 }
}

The starter function above waits until the player is created, and then it sets needle's roll angle to player's pan angle multiplied by -1.


Untitled

Data sticks

You've gotta love data sticks! They're nothing more than electronic pieces of paper that give you information throughout the game. While some of them might sound pretty pathetic (like "Today we've lost 2 more men. I'm all alone now!") others can prove to be really useful (like "You are reading this data stick!").

The code in this article can be used for two objects, but you can expand it and use it for an unlimited number of data sticks. First of all we define two strings and a text:

string data1_str = "The new access code is 6239. Memorize it!";
string data2_str = "The vents will be shut down tonight.";

text data_txt
{
pos_x = 200;
pos_y = 20;
font = std_font;
}

Each data stick (can be a model, a sprite, a wmb entity) has this action attached to it:

action
data_stick
{
my.ambient = -50;
my.enable_click = on;
my.event = show_data;
}

The first line of code darkens the model a little; you can use positive or negative values here, depending on the entity used as a data stick. The entity is sensitive to clicking, so press the right mouse button to show the cursor if you are using the templates. Every time we click the data stick, its event function (show_data) will run:

function
show_data()
{
if (my.skill1 == 1)
{
data_txt.string
= data1_str;
data_txt.visible
= on;
sleep (3);
data_txt.visible
= off;
}
if (my.skill1 == 2)
{
data_txt.string
= data2_str;
data_txt.visible
= on;
sleep (3);
data_txt.visible
= off;
}
ent_remove (me);
}

Every data stick has its skill1 set to a different value in Wed. If the entity that was clicked has its skill1 set to 1 (if it is the first data stick), we set data1_str as the string for the text named data_txt, we show the text for 3 seconds and then we hide it. The same thing happens if we click the second data stick (its skill1 = 2). You can add more "if" branches if you need to have more data sticks.

The last line of code in the function removes the data stick entity from the level (you've read it, so it can disappear). I'd use data sticks to tell the player the access code to a certain door, the number of enemies that guard a specific military objective, and so on.


Untitled

Disco

This snippet combines music and colored lights. I have created a standalone project but you can easily insert the code in your own projects.

Take a look at function main:

function
main()
{
d3d_lightres
= 1;
fps_max = 50;
level_load (disco_wmb);
wait (2); // wait for the level to be
loaded
set_camera();
waitt (80);
while (1)
{
if (sound_handle ==
0)
{
snd_loop
(music_snd, 80, 0);
sound_handle = result;
}
wait (1);
}
}

First of all we're setting d3d_lightres to 1; this improves the quality of the dynamic lights. We lock the frame rate at 50 fps, we load the level, we set the camera and them we play the music wav file in a loop. I have used a small music sample but you can replace it with any wave file.

function
set_camera()
{
camera.tilt = -90;
camera.y = -100;
camera.z = 800;
wait (1);
}

Function set_camera chooses a convenient position and angle for the camera; play with the values above to get a different position / angle.

But what's so special with this disco light code? I could use an invisible entity that walks on a path and generates light or similar stuff, right?
Sure you could do that, but this piece of code places the light on the floor in a position that is determined by the light spot on the wall, like in the picture below:

Every light color has two entities and two actions; we have disco_red (the action attached to the red light on the floor) and red_spot (the action attached to the red light spot), etc.

Let's take a look at the action that will create the red light on the floor:

entity* redspot_pnt;

action
disco_red
{
my.invisible
= on;
my.passable = on;
my.lightrange = 0;
my.lightred = 250;
waitt (16 + random(32));
snd_play (poweron_snd, 30, 0);
my.lightrange = 200;
while (1)
{
my.x = redspot_pnt.z
* tan (redspot_pnt.pan);
my.y = redspot_pnt.z * tan (redspot_pnt.roll);
wait (1);
}
}

The entity is invisible, passable and will generate red light (lightred = 250). After 1..3 seconds we play the power on sound and we give the light a range of 200 quants. The light will move on the floor by changing its x
and y coords depending on the position and angles of the redspot_pnt, a pointer to the red light spot.

action
red_spot
{
redspot_pnt
= me;
my.light = on;
my.lightrange = 0;
my.lightred
= 250;
waitt (80);
while (1)
{
my.pan = 30 - random(60);
my.roll = -10 - random(50);
waitt (2);
}
}

The red spot glows red; I use the same model for all the light spots (red, green, blue) and I have used different lightred, lightgreen, lightblue values to change their appearance.

The spot waits for about 5 seconds (I'm using the same value in main, remember?) and then it starts changing its pan and roll angles in a wild (1) loop ;)
The values for pan and roll depend on your level geometry so you might need to play with them. You can control any other angles (pan and tilt, tilt and roll, etc) as long as you use the same angles in action disco_red. Before I forget, here's the math behind the code:

The code for the green and blue spots is similar.


Untitled

Dust

You don't see too much dust in today's games, isn't it? All this computer power keeps the streets clean but maybe sometimes, when you are traveling in a desert, you could use some dust; if this is the case, read on.

The dust is created using particles that start at player's feet level and rise up, slowly fading away. Of course that you can add the same effect to your monsters or (even better) to your vehicles.

We begin with a starter function, a function that runs by itself at game start:

var pos1;
var pos2;
var start_pos;

starter init_dust()
{
while (player == null) {wait (1);}
while (1)
{
vec_set (pos1,
player.pos);
wait (1);
vec_set (pos2,
player.pos);
if (pos1 != pos2)
{
start_pos.x = player.x;
start_pos.y = player.y;
start_pos.z = player.z - 30;
effect (dust, 1, start_pos, normal);
}
}
}

The code waits for the player to be created and then we store the current player position in a var named pos1. We wait for a frame and then we store player's position again, this time in a var named pos2. There are two possibilities:
a) The player hasn't moved during the last frame. If this is the case, nothing happens;
b) The player has moved during the last frame. If this is true, pos1 and pos2 hold different positions so we set start_pos to a position close to player's feet and we start to emit particles using the function named dust:

function
dust()
{
temp.x = (random(2) - 1) * 0.3;
temp.y = (random(2) - 1) * 0.3;
temp.z = (random(1) + 1) * 0.2;
vec_add (my.vel_x, temp);
my.alpha = 10 + random(5);
my.bmap = dust_pcx;
my.size = random(5) + 10;
my.flare = on;
my.move = on;
my.lifespan = 20;
my.function = fade_dust;
}

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

The dust has a random speed on x and y and a random but positive speed on z to make sure that the dust moves upwards. The bitmap used for the dust particles is dust_pcx and its transparency factor, as well as its size ranges from 10 to 15. The function that runs the particles (fade_dust) decreases their alpha until they are made invisible and then it removes them.

You can play with the size of the particle, its alpha factor, its speed and of course with the particle bitmap to create the dust that fits your needs. Oh, and press F7 several times to switch the camera to the side view if you want to be able to admire the dust.


Untitled

Energy balls

Hundreds of years ago, an evil wizard lived in a cave. He used to throw energy balls all day long but after a few years he ran out of balls and died. His art was lost forever... wait, don't loose your hope! I have found the cave and the script was written on its walls! Ok, it used old synthax wdl code but I have brushed it and it looks ok now :)

action
wizard
{
clip_size
= 0;
while (1)
{
while (my.skill10 <
95)
{
ent_cycle
("attack", my.skill10); // play the attack animation frames
my.skill10
+= 3 * time;
wait (1);
}
eball_pos.x = my.x;
eball_pos.y = my.y;
eball_pos.z = my.z
+ 25; // the energy ball appears betwen wizard's hands
ent_create (energyball_mdl, eball_pos,
energy_ball);
snd_play (energy_snd,
70, 0);
waitt (16);
while (my.skill10 >
2)
{
ent_cycle
("attack", my.skill10); // play the attack animation frames
my.skill10
-= 2 * time;
wait (1);
}
waitt (32);
}
}

The first line in the action tells the engine to draw all the triangles for all the models. The wizard will move his hands ("attack"); the energy ball is created 25 quants above wizard's origin. The wizard will move his hands again (this time in a reversed "attack" animation) and after 2 seconds everything will repeat.

function
energy_ball()
{
wait (1);
my.enable_entity = on;
my.enable_block = on;
my.event = remove_eball;
my.pan = you.pan;
my.tilt = you.tilt;
my.lightred = 250;
my.lightgreen = 150;
my.lightrange = 200;
eball_speed.x = 20;
eball_speed.y = 0;
eball_speed.z = 0;
eball_speed *= time;
while (my != null)
{
my.roll += 20 * time;
move_mode = ignore_you
+ ignore_passents;
ent_move (eball_speed, nullvector);
wait (1);
}
}

function
remove_eball()
{
wait (1);
if (you == player)
{
player._health -= 60; // decrease player's
health
}
ent_remove
(me);
}

The ball is sensitive to entities and level blocks. If the ball collides with an entity (the player, an elevator, etc) or with a level block (a wall, etc) its remove_eball event is executed. The ball has the same pan and tilt with its creator (the wizard) and it generates orange light up to 200 quants around it. As long as the ball exists, it moves with the speed given by eball_speed.x and rotates by changing its roll angle.

If the ball collides with something, it checks if it collided with the player. If this is true, it decreases player's health by 60 points. At the end of its event function, the ball is removed.


Untitled

Escalator

If you need to transport the player using an escalator then this snippet is just what you need.

We've got a simple action that is attached to our wmb escalator:

var escalator_speed = 2;

action
escalator
{
while (1)
{
my.v += escalator_speed * time;
wait (1);
}
}

This action simply changes the "v" for the wooden texture that was applied to the escalator entity, creating the illusion that it moves. I could have used another (better looking) texture, but all of us have got the "woodflhori" texture inside standard.wad.

starter trace_below()
{
var trace_pos;
while (player == null) {wait (1);}
while (1)
{
vec_set(trace_pos.x, player.x);
trace_pos.z -= 1000;
trace_mode = ignore_me + ignore_passable + ignore_models + ignore_sprites + scan_texture;
trace (player.x, trace_pos.x);
if (str_cmpi ("woodflhori", tex_name))
{
player.y += escalator_speed * time;
player.z += 0.5 * time; // experimental
value
}
wait (1);
}
}

The function above waits until the player is created, and then traces 1000 quants below player's feet every frame. If the trace detects the texture named "woodflhori" below player's feet, it increases player's x and z. Please note that player's y is increased with the same amount that was used for "v" in order to keep the player and the escalator in sync. You might need to use negative values for y, or replace the movement on the y axis with movement on the x axis, depending on the orientation of the escalator. Check the pictures below to see what I mean.

We increase player's height (z) too; this helps it to move upwards easily with a small amount and lets the gravity code to do the rest of the job, pulling the player close to the floor level. Play with 0.5 until you are happy with the result and don't forget to tweak the escalator_speed variable as well.


Untitled

Exploding barrels

These barrels are lethal; they will explode when they are hit by bullets, rockets, etc; more than that, they will explode if another barrel has exploded nearby. Can you imagine what you can do at a fuel deposit by firing a single bullet? Don't get to close too one of these barrels because the enemies can shoot them too.

You can have as many barrels as you want; place their models in your level and attach them the barrel action:

action
barrel
{
my._DAMAGE
= 300;
my._SIGNAL = 0;
my.enable_scan = on;
my.enable_shoot = on;
my.enable_detect
= on;
my.event = barrel_explodes;
}

I'm using some template stuff here; _DAMAGE looks much better than skill16, isn't it? Next line, my._SIGNAL = 0; tells the enemy that the barrel is not the player, so it shouldn't attack the barrel :). The barrel is sensitive to scan_entity and bullets, can detect entities and if its event is triggered it will run the barrel_explodes function:

function
barrel_explodes()
{
if ((event_type
== event_scan && indicator == _EXPLODE)
|| (event_type == event_shoot &&
indicator == _GUNFIRE))
{
waitt (3);
range =
my._DAMAGE * 2;
damage
= my._DAMAGE;
temp.pan
= 360;
temp.tilt
= 180;
temp.z
= range;
indicator
= _EXPLODE;
scan_entity
(my.x, temp);
morph (barrel_sprite, me);
while (my.frame
< 7)
{
my.frame
+= 3 * time;
wait
(1);
}
actor_explode();
}
}

If the barrel is hit by a rocket (or a grenade, etc) or by a bullet, we waitt(3) to avoid the dangerous instruction warning and to get a nice delay, useful for chained explosions. We prepare for a full area scan (pan = 360, tilt = 180, distance = 600 quants) and we set indicator to _EXPLODE - this way we tell the others that this isn't a friendly scan (to open doors or similar).

I have defined a barrel explosion sprite (it's the explo+7 sprite that comes with the templates but I have changed its name) and we replace barrel.mdl with barrel_sprite using morph. The explosion sprite plays its animation frames and then it explodes using the predefined actor_explode() action in the templates.


Untitled

Fade in and out

Why would you need to fade in and out? Well, maybe because you want to create a smooth transition between two different scenes, display the credits, show the player dieing and so on. The good news is that the code inside this article is simple and doesn't use too much video memory (less than 3Kb). Don't use panels; they will need much more memory!

This is beginner's corner so the idea is simple: we create an entity, we place it in front of the camera and we set a variable alpha factor for it. Let's take a look at the code that creates the entity:

entity
fader_ent
{
type = <fader.mdl>;
layer = 40;
alpha = 0;
pan = 90;
flags = transparent, visible;
view = camera;
x = 250; // 250 quants ahead
of the view
y = 0;
z = 0;
}

I have used a model named fader.mdl and I have changed its pan angle to make sure that it covers the view. The entity is transparent and visible and it is placed 250 quants in front of the view. Now let's see the functions that create the fade in and fade out effects:

function
fade_in()
{
while (fader_ent.alpha > 5)
{
fader_ent.alpha
-= 5 * time;
wait (1);
}
fader_ent.alpha = 0;
}

function
fade_out()
{
while (fader_ent.alpha < 95)
{
fader_ent.alpha
+= 5 * time;
wait (1);
}
fader_ent.alpha = 100;
}

on_i fade_in;
on_o fade_out;

The first function (fade_in) runs when we press the "i" key on the keyboard and it decreases fader_ent.alpha until its value is below 5, then sets it to zero (as if the entity wouldn't exist). Use this function when you have used fade_out, the screen is completely black and you want to return to the game. Function fade_out will darken the image, hiding the screen progressively; it increases fader_ent.alpha until its value becomes bigger than 95 and then sets is to 100 (completely dark). Start your level and press "o" to fade out and then press "i" to fade in. Add one of these lines of code to an action or function used in your game:

fade_in();

fade_out();

to fade in or out when you need to. Of course that you can change the color of the skin for the entity model to simulate blood, water, slime, etc. Take a look at the picture below to see an example that uses a fader model with a completely red skin.


Untitled

First AI demo

Do you remember the standard patrol action that comes with the templates? You had to place some positions in the level and adjust their pan to create a virtual enclosed path. I like this action a lot but it is way too complicated to be explained here, so I had to rewrite it. I have added some AI code to it and the result is the FAID, the demo that works with A4 and A5.

Before I start explaining what I did, please load the aidemo1 level in wed and take a look at it: there are only four positions close to the corners and a lot of boxes in guard’s way. Can he make it? Can he avoid all these obstacles? Why don’t you run that level right now and see for yourself? Run aidemo1.wdl.

I took a good look (for about three laps) and I’m still happy with the result. I have used a simple idea: the guard has two “antennas” that are placed like this:

If at least one of the antennas (front_right or front_left) is inside a solid block (content_solid), the guard avoids the obstacle until the antenna gets out of the solid block.

Now you should load the aumai1 level; don’t forget to run the ai1.wdl file. Move the block close to the guard and see how (and if) it manages to avoid the block. You can see the antennas – please note that they aren’t needed because we use vectors (not entities) to check “content”; I have created these visible antennas to show you how this algorithm works. When you want to remove the antennas simply comment these two lines in ai1.wdl:

create (<marker.mdl>, front_right, frlaction);
create (<marker.mdl>, front_left, frlaction);

I have tried to use decent, meaningful names for variables so if you want to use several "intelligent" units you’ll have to rename a few variables to my.skillxx, copy their values to temp (if necessary) and so on.

Please note that the following explanations are referring to ai1.wdl. The function init_demo() is called in main and sets the camera position and angles according to our needs:

function init_demo()
{
camera.x = 0;
camera.y = 0;
camera.z = 440;
camera.tilt = -90;
camera.pan = 270;
}

All the interesting stuff is in action guard_patrol. First of all we create the block that is used for tests:

block_pos.x
= 0;
block_pos.y = 0;
block_pos.z = 0;
create (<block.wmb>, block_pos, move_block);

We place the cube (block.wmb) in the origin at game start; the action that controls its movement (move_block) is really easy so I won’t explain it here.

Function create_markers() creates the “antennas”:

function
create_markers()
{
while (1)
{
front_right.x = guard_syn.x + 50
* cos(guard_syn.pan) + 20 * sin(guard_syn.pan);
front_right.y = guard_syn.y + 50
* sin(guard_syn.pan) - 20 * cos(guard_syn.pan);
front_right.z = guard_syn.z;

       front_left.x = guard_syn.x + 50 *
cos(guard_syn.pan) - 20 * sin(guard_syn.pan);
front_left.y = guard_syn.y + 50 *
sin(guard_syn.pan) + 20 * cos(guard_syn.pan);
front_left.z = guard_syn.z;

       fright_marker = content(front_right);
fleft_marker
= content(front_left);
if (fright_marker
== content_solid)
{
obstacle_right
= 1;
}
else
{
obstacle_right
= 0;
}
if (fleft_marker == content_solid)
{
obstacle_left
= 1;
}
else
{
obstacle_left
= 0;
}
wait (1);
}
}

Please don’t forget that I’m using simplified versions of functions and actions to make the things easier for you.

We create the antenna vectors (front_right and front_left) in front of the player then we check if their “content” is content_solid. If one of the antennas is inside a solid block, the corresponding variable (obstacle_right or obstacle_left) is set to 1.

Here’s the rest of the guard_patrol action:

my_pos.x
= destination.x - my.x;
my_pos.y = destination.y - my.y;
result = vec_to_angle(my_angle, my_pos);
if (result < 30)
{
vec_set (my_pos, destination);
my_angle.pan = my.skill12;
temp.pan = 60;
temp.tilt = 90;
temp.z = 2000;
scan_pos (my_pos,my_angle,temp);
if(result > 0)
{
vec_set (destination,
my_pos);
my.skill12
= my_angle.pan;
}
}
my.skill13 = ang (my_angle.pan - my.pan);
if (obstacle_right != 1 && obstacle_left !=
1)
{
my.pan += 0.2 * my.skill13
* time;
}

The guard computes the distance between its coordinates and the next position, rotates towards it and if it is close enough to the destination (result < 30) it will search for a new position. If the guard finds a new position (result > 0) and the antennas aren’t inside a solid block it will gently rotate towards the target. Skill13 holds the difference between guards’ pan and target’s pan.

if (obstacle_right
== 1)
{
my.skill30 = my.pan;
while (my.pan < my.skill30 + 80)
{
marker_distance
= 0;
my.pan +=
5 * time;
move_guard();
wait (1);
}
}
if (obstacle_left == 1)
{
my.skill30 = my.pan;
while (my.pan >
my.skill30 - 80)
{
marker_distance
= 0;
my.pan -=
5 * time;
move_guard();
wait (1);
}
}
if (obstacle_right == 1 && obstacle_left ==
1) {my.pan = random(360);}
if (marker_distance <
50) {marker_distance += 2 * time;}
move_guard();

If one of the antennas is inside a solid block, we store guard’s pan in skill30 and we add (or substract) 80 degrees to it in a while loop. You can try to add smaller or bigger pan values depending on your levels. We have to set marker_distance to 0 for a short period to improve the detection around the corners (if you run the test level and play with the block you’ll understand why). If both antennas are inside a solid block, the guard chooses a random pan angle that will get one of the antennas out of the solid block. You know that marker_distance is the distance between the guard and its antennas. While this distance is smaller than 50, it is slowly increased.

function
move_guard()
{
move_mode = ignore_you + ignore_passable
+ activate_trigger;
ent_move(temp, nullvector);
ent_cycle("walk",my.skill11);
my.skill11 += 5 * time; // skill11 = animation
frame
if (my.skill11 >=
100) {my.skill11 = 0;}
}

This function moves and animates the player. I have used similar functions in Aum2 and Aum3 so you know how this works.

I wanted to show the antennas so I have created two models that are placed at front_right and front_left vector coordinates.

create (<marker.mdl>, front_right, frlaction);
create (<marker.mdl>, front_left, frlaction);

function
frlaction()
{
my.passable = on;
waitt (2);
remove me;
}

Function frlaction creates the models for the antennas, makes them passable (to allow them to penetrate solid blocks), displays them for 1/8 seconds then removes the antennas.

Is this First AI demo perfect? I don’t think so; if it would be perfect I would have named it Final AI demo. Here are its weak points:
- The code won’t work that great in crowded areas (you can change marker_distance and marker_width to make it work better for your levels);
- If the area near one of the positions is blocked, the guard will try to get to it all day long;
- The guard will get stuck from time to time;

If I see that you are interested in it, I will continue to improve this code.


Untitled

Flares

I have to admit that a darker level looks better than a well lit level most of the time. You can add even more drama by making a small area of the level completely black, and maybe give the player a few flares that will generate light for a limited number of seconds, and this is exactly what we're going to create in this article.

starter
init_flares()
{
while (player == null) {wait (1);}
while (1)
{
if (key_f
== 1)
{
while (key_f == 1) {wait (1);}
ent_create (flare_pcx, player.x, create_flare);
}
sleep (0.1);
}
}

The function above will run by itself at game start; it waits until the player is created, and then creates a flare every time we press the "F" key. The last line of code instructs the while loop to stop for 100ms, so we can't create more than 10 flares a second, which is a good thing according to your keyboard :).

function
create_flare()
{
my.burn_time = 10; // this flare lasts for
10 seconds
my.passable = on;
my.flare = on;
my.ambient = 100;
my.pan = camera.pan;
my.tilt = camera.tilt;
my.enable_entity = on;
my.enable_impact = on;
my.enable_block = on;
my.event = show_flare;

Every flare will burn for 10 seconds; burn_time is just another name for skill20. The sprite used for the flare is passable at the moment of creation, has its flare flag set and its ambient is 100. The flare will inherit the pan and tilt angles used by the camera, and it will be sensitive to impact with other entities and level blocks. The event function associated to the flare is function show_flare.

  while (my.skill10 == 0)
{
if (vec_dist (my.x, player.x) > 50) {my.passable = off;}
my.skill1 = 50 * time;
my.skill2 = 0;
if (my.skill40 < 100)
{
my.skill3 = 0;
}
else
{
my.skill3 = - 5 * (my.skill40 / 100) * time;
}
move_mode = ignore_you + ignore_passable;
my.skill30 = ent_move (my.skill1, nullvector); // get the distance that was covered
this frame in skill30
my.skill40 += my.skill30;
// store the distance covered so far in skill40
wait (1);
}
}

The flare will move until its skill10 is set to a non-zero value, and it will become impassable as soon as the distance to its creator (the player) is bigger than 50 quants. The sprite moves with a speed given by 50 * time; its movement will start to include gravity (negative speed on z, set by skill3) if the flare has covered at least 100 quants. Take a look at the picture below to see what I mean.

The flare ignores any passable entity that might come in its way (water, etc). The distance that is covered by the flare in a single frame is stored in skill30, and the total distance covered by the flare is stored in skill40. If this value is bigger than 100 quants, we start decreasing the height of the flare sprite. What happens when the flare hits something? Its show_flare event function will run:

function
show_flare()
{
my.skill10 = 1; // stop moving
my.passable = on; // become passable
while (my.burn_time
> 0)
{
my.lightred = 255;
my.lightgreen = 200 + random(55);
my.lightblue = 230;
my.lightrange = 200 + random(50);
my.scale_x = 0.9 + random(1) / 10; // scale = 0.9 to
1
my.scale_y = my.scale_x;
my.scale_z = my.scale_x;
my.burn_time
-= time / 16;
wait (1);
}

As soon as the flare hits a level block or another entity, it sets its skill10 to 1; this way, the while loop in the previous function is terminated, so the flare stops moving. We make the flare passable because we don't want it to stand in player's way, and then we let it generate a dynamic white light with slight variations of range and color. The scale of the flare sprite changes from 0.9 to 1, and we subtract "1" from burn_time every second. Please note that I am changing scale_z too, so you can use a 3D model for the flare as well.

while (my.scale_x > 0.1)
{
my.scale_x -= 0.1 * time;
my.scale_y = my.scale_x;
my.scale_z = my.scale_x;
my.lightrange -= 3 * time;
wait (1);
}
ent_remove (me);
}

This new loop will start to run when burn_time goes below zero. The scale of the sprite and its lightrange are decreased continuously until scale_x goes below 0.1, and then the flare sprite is removed. Don't forget to experiment with different sprites, colors and light ranges for your flares.


Untitled

Flashlight with batteries

The regular flashlight code uses this idea: create an invisible object at a big distance in front of the player and trace between the player and the invisible object. If something is hit (a wall, etc) move the object at hit point and let it generate light, like in the picture.

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

var max_dist = 10000; // use it for distances smaller than 10,000 quants

action flashlight_pickup
{
while (player == null) {wait
(1);}
my.passable = on;
while (vec_dist (player.x,
my.x) > 100) {wait (1);}
my.invisible = on;
my.lightred = 250;
my.lightgreen = 250;
my.lightblue = 170;
while (1)
{
while (energy >
0 && key_f == 1) // press the "F" key to use the flashlight
{
my.lightrange = 200;
temp.x = max_dist * cos(player.pan) * cos(camera.tilt) + player.x;
temp.y = max_dist * sin(player.pan) * cos(camera.tilt) + player.y;
temp.z = max_dist * sin(camera.tilt) + player.z;
trace_mode = ignore_models + use_box;
my.skill1 = trace (player.x, temp);
if (my.skill1 != 0)
{
temp.x = (my.skill1 - 30) * cos(player.pan) * cos(camera.tilt) + player.x;
temp.y = (my.skill1 - 30) * sin(player.pan) * cos(camera.tilt) + player.y;
temp.z = (my.skill1 - 30) * sin(camera.tilt) + player.z;
vec_set (my.pos, temp);
}
energy -= 0.5 * time;
wait (1);
}
my.lightrange =
0;
wait (1);
}
}

The flashlight model is passable; as soon as the player comes close enough, it will become invisible. If we have enough energy (batteries) left and we press the "F" key, we compute a position 10,000 quants in front of the player. If you like math you can check the formulas; I'll explain the easiest one:

temp.z = max_dist * sin(camera.tilt) + player.z;

We trace between the player and the flashlight object and we store the result in skill1. If the result isn't zero we have hit something, so we compute a new position for the flashlight spot, replacing max_dist with the distance returned by trace. We don't want our light spot to be placed inside the wall, so we substract 30 quants from the distance given by skill1. Please note that I am using the flashlight model itself for the "flashlight object"; you can use other object if you want to (but why would you want to do that?).

The energy decreases if the "F" key is pressed; if we release "F" the lightrange is set to zero -> the flashlight is extinguished.

The action assigned to the batteries is simple:

action
battery_pickup
{
my.passable = on;
while (player == null) {wait (1);}
while (vec_dist (player.x, my.x) > 100) {wait (1);}
energy += battery;
ent_remove (me);
}

When the player comes close, the energy is increased and the battery object is removed.


Untitled

Flying birds

I remember myself playing Unreal1 and getting past the first indoor levels. I was imprisoned and then I managed to escape and I got out of a cave. I was amazed! The huge outdoor areas with trees and springs and birds flying up high in the sky... Man, those were the days! The snippet below will teach you to create the code for a bird that can fly and take care of itself. Let's stare at it for a second:

action move_bird
{
var init_pos; // initial position
of the bird
var bird_speed; // flying speed
var flying_distance;
flying_distance = my.skill1;
if (flying_distance == 0) {flying_distance = 2000;} // default flying distance
bird_speed = my.skill2;
if (bird_speed == 0) {bird_speed
= 5;} // default flying speed
my.passable = on;
vec_set (init_pos.x, my.x); // store the initial position of the bird
while (1)
{
temp.x
= bird_speed * time;
temp.y = 0;
temp.z = 0;
move_mode
= ignore_passable;
my.skill40 += ent_move (temp,
nullvector); // move in the direction given by the pan angle in Wed
if (my.skill40 > flying_distance)
{
my.skill40
= 0;
vec_set
(my.x, init_pos.x);
}
ent_cycle("walk", my.skill1); // animate the bird
my.skill1 += 3 * time; // animation speed
my.skill1 %= 100; // loop animation
wait (1);
}
}

We have defined a few variables; init_pos will store the initial position of the bird, bird_speed will store the speed of the bird and flying_distance will store the distance that must be covered by the bird before returning to its initial position. You can set flying_distance using skill1 and bird_speed using skill2 in Wed; however, if you forget to do that the code will use the predefined values of 2000 quants and 5. We make the bird passable (not really needed) and then we store the initial position of the bird in init_pos (that's the position you have given to your bird in Wed) before entering the while loop. The loop keeps bird's speed time-corrected and moves the bird, storing the distance that was covered in skill40; if that value gets bigger than flying_distance, the bird is returned to its initial position and the process repeats. The last few lines animate the bird; mine had a weird name for its animation (walk) but you can put the name of your bird's animation frames here.

How do you use these birds? Place them in the level at slightly different heights, choose their orientation and set a proper speed and flying distance. The distance should be big enough because you don't want the player to notice the trick (the movement in a loop); make sure that the birds start flying in an area that can't be seen by the player and end their journey in another place that can't be seen by the player. Have fun!


Untitled

Fogger

I simply love fog! It looks good and it is extremely useful if you want to create huge worlds. But how do we set its color and strength? We open the level in Wed, we choose File -> Map Properties -> Fog and then we set the colors. We build the level, we run it and we don't like what we see so we have to start over... This piece of code allows you to set the fog at runtime; click a few buttons and when you like what you've got, simply copy the values to your script!

Let's take a better look at the panel that controls the fog:

Everything looks normal, isn't it? We can change 5 values (red, green, blue, start and end) using 5 pairs of buttons that increase or decrease the values. This simple yet nice interface was created using a text and a panel:

text
fog_txt
{
font = terminal9_font;
pos_x = 10;
pos_y = 10;
string = "red (d3d_fogcolor1.red):\ngreen
(d3d_fogcolor1.green):\nblue (d3d_fogcolor1.blue):\nstart (camera.fog_start):\nend
(camera.fog_end):";
flags = d3d, visible;
}

The
text has a long string; it displays red (d3d_fogcolor1.red):
and then \n (newline) moves to the next line. The process
repeats and we get 5 different lines of text using a single string. Let's take
a look at the panel definition:

panel
fog_pan
{
pos_x = 0;
pos_y = 0;
button = 190, 10, arrowup_pcx,
arrowup_pcx, arrowup_pcx, change_fog, null, null;
button = 202, 10, arrowdown_pcx,
arrowdown_pcx, arrowdown_pcx, change_fog, null, null;
button = 190, 22, arrowup_pcx,
arrowup_pcx, arrowup_pcx, change_fog, null, null;
button = 202, 22, arrowdown_pcx,
arrowdown_pcx, arrowdown_pcx, change_fog, null, null;
button = 190, 34, arrowup_pcx,
arrowup_pcx, arrowup_pcx, change_fog, null, null;
button = 202, 34, arrowdown_pcx,
arrowdown_pcx, arrowdown_pcx, change_fog, null, null;
button = 190, 46, arrowup_pcx,
arrowup_pcx, arrowup_pcx, change_fog, null, null;
button = 202, 46, arrowdown_pcx,
arrowdown_pcx, arrowdown_pcx, change_fog, null, null;
button = 190, 58, arrowup_pcx,
arrowup_pcx, arrowup_pcx, change_fog, null, null;
button = 202, 58, arrowdown_pcx,
arrowdown_pcx, arrowdown_pcx, change_fog, null, null;
digits = 155, 10, 3, terminal9_font,
1, d3d_fogcolor1.red;
digits = 155, 22, 3, terminal9_font, 1, d3d_fogcolor1.green;
digits = 155, 34, 3, terminal9_font, 1, d3d_fogcolor1.blue;
digits = 145, 46, 5, terminal9_font, 1, camera.fog_start;
digits = 145, 58, 5, terminal9_font, 1, camera.fog_end;
flags = overlay, refresh, d3d, visible;
}

This panel might look complicated but it isn't; its definition creates:
- 10 buttons (5 pairs) that can change the 5 values;
- 5 digits, used to display the 5 fog values.

You can see that I'm using only 2 bitmaps (arrowup_pcx and arrowdown_pcx) and all the buttons run the same function when we click them: change_fog.

function change_fog(button_number)
{
if (button_number == 1)
{
d3d_fogcolor1.red
= min(255, (d3d_fogcolor1.red + 5));
}
if (button_number == 2)
{
d3d_fogcolor1.red
= max(0, (d3d_fogcolor1.red - 5));
}
if (button_number == 3)
{
d3d_fogcolor1.green
= min(255, (d3d_fogcolor1.green + 5));
}
if (button_number == 4)
{
d3d_fogcolor1.green
= max(0, (d3d_fogcolor1.green - 5));
}
if (button_number == 5)
{
d3d_fogcolor1.blue
= min(255, (d3d_fogcolor1.blue + 5));
}
if (button_number == 6)
{
d3d_fogcolor1.blue
= max(0, (d3d_fogcolor1.blue - 5));
}
if (button_number == 7)
{
camera.fog_start
= min(5000, (camera.fog_start + 50));
}
if (button_number == 8)
{
camera.fog_start
= max(0, (camera.fog_start - 50));
}
if (button_number == 9)
{
camera.fog_end
= min(10000, (camera.fog_end + 50));
}
if (button_number == 10)
{
camera.fog_end
= max(0, (camera.fog_end - 50));
}
}

If we click the first button on the panel (the button associated to the first "button" definition in fog_pan) button_number will be set to 1. If we click the 6th button, button_number will be set to 6 and so on. Don't thank me for that, J.C. Lotter has created (inside the engine) the code for these smart panels. This way we can use a single function for all our buttons and then we can make them do different things depending on button_number.

If we click the first button, d3d_fogcolor1.red will be increased until its value equals 255. Every mouse click will add 5 to d3d_fogcolor1.red but if you want smaller steps, use a smaller value here. If we click the second button, d3d_fogcolor1.red will be decreased until it reaches zero. The same thing happens for the rest of the buttons: they set d3d_fogcolor1.green, d3d_fogcolor1.blue, camera.fog_start and camera.fog_end. The last two values can be really big so every mouse click will increase or decrease these values by 50.

You might have noticed that we are playing with d3d_fogcolor1 - this is the first of the four fog colors in Wed. We need to do one more thing; we have to tell the engine that we want to use the first fog color and this tiny function does exactly what we need:

starter
init_fog()
{
fog_color = 1;
}

This "starter" function is a special function; it will run by itself when you run the level.

Run your level and then set the proper fog values. When you like what you have got, write down the values and then place them at the end of your main function:

function main()
{
..............
..............
fog_color = 1; // use the first fog color
d3d_fogcolor1.red = 85; // your values here
d3d_fogcolor1.green = 45; // your values here
d3d_fogcolor1.blue = 95; // your values here
camera.fog_start = 400; // your values here
camera.fog_end = 2500; // your values here
}

If you don't want to use buttons, you can replace them with sliders.


Untitled

Fog of war

If you are creating a strategy game then you might need to add some "fog of war" to it. This article will teach you to create a decent effect that doesn't eat too many resources; in fact, it is up to you to set the quality versus performance ratio, depending on the PC that is targeted as "system requirements" for your game.

How does this thing work? We create an opaque layer that covers the entire map; we build it using small sprites, and we make transparent only the sprites that are really close to the player.

Do you see all those small red squares? Each one of them is a sprite. If you believe that I have placed them in the level by hand you must think that I'm a patient person. Well, I'm not that patient...

starter
create_fog_sprites()
{
while (player == null) {wait (1);}
var row_index = 0;
var column_index = 0;
while (row_index < 32)
{
while (column_index < 32)
{
column_index += 1;
temp.x
= column_index * 128 - 2000; // -2000 to 2000 on x
temp.y = row_index * 128 - 2000; // -2000 to 2000 on y
temp.z = -1000;
ent_create
(fogsprite_tga, temp.x, manage_fog);
}
column_index = 0;
row_index += 1;
}
}

The code waits until the player is created, and then uses two nested "while" loops to place all the sprites above the ground. My demo level has about 4000 x 4000 quants and I have used a sprite of 128 x 128 pixels for the fog, so we should create 4000 / 128 = 31.25 (I chose 32) rows and columns. We've got two variables that increase their values from 0 to 31 (for a total of 32 steps), and we set the proper temp position by multiplying the current index with the width and height of the sprite (which are equal in my example). Temp.z = -1000 places the fog sprites above player's head (that's another experimental value), and then the next line creates the fog sprites at the position given by temp and attaches them the "manage_fog" function:

function
manage_fog()
{
my.tilt = 90;
my.alpha = 100;
my.oriented = on;
my.transparent = on;
my.passable = on;
while (1)
{
if (vec_dist(player.x, my.x) < 350)
{
my.alpha = 0;
}
else
{
my.alpha = 100;
}
wait (1);
}
}

The function above sets the proper orientation for the sprite, makes it passable and sets its alpha value depending on the distance between the sprite and the player. Feel free to play with "350", and don't forget that you can get a more realistic fog effect if you use smaller sized sprites.


Untitled

Volumetric fog

Acknex uses a simple fog system that is supported by all the 3D cards that are available on the market, even by the older ones. The engine is mapping the z value of the pixels to a function that controls the fog color, strength, etc. The result is the good looking fog we all know and like (read Aum25 for more info). But what if we want to put some fog only in a certain area of our level, for example on the floor of that slimy dungeon? We would need to use volumetric fog.

I'm sure that some of you have asked this questions many times: how can I add volumetric fog to my levels? This article will teach you how to fake volumetric fog using the Acknex engine. The results look good and the performance hit is practically inexistent.

1) Create a simple model that looks like this in Med:

Add a cube in Med and change its size until it fits your needs. Don't forget to make it extremely thin. Use a plain color for the skin; I have used a white texture of 32 x 32 pixels.

2) Place the model in your level, close to the floor, adjust its scale (if you need to) and attach it the action named fog_layer.

3) Add more models that have the same action attached to them, using a different z value for each one of them. The picture below shows 5 "layers" (5 models) placed above the floor level; my demo level uses 10 layers.

Time to take a look at the code:

action
fog_layer
{
my.passable = on;
my.transparent = on;
while (1)
{
my.alpha = max
(0, (camera.z - my.z) / 20);
wait (1);
}
}

I know it might sound weird, but that's all the code that it is needed in order to create volumetric fog! The model is made transparent and passable; its alpha changes depending on the distance (on the z axis) between the camera and the model used as a fog layer. When the camera comes closer to a layer, that layer will become more and more transparent, until it disappears.

You can adjust the transparency factor by changing "20", you can change the color of the fog by simply changing the color of the skin, you can create several actions with different names and different "20" values if you need several different fog effects in the same level, you can use more or fewer models (thinner or thicker) to adjust the quality of the fog, you can... well, you can do a lot of stuff.


Untitled

Footprints

Use this code snippet and you'll be able to see warlock's footprints as he walks on the snow. You need to start init_footprints() in main:

function
init_footprints()
{
while
(player == null) {wait (1);}
vec_set(temp,
player.x);
temp.z -= 500;
trace_mode = ignore_me + ignore_passable
+ ignore_models + ignore_sprites;
leftfoot_coords.z
= player.z - trace (player.x, temp) + 5; // place the footprints 5 quants above
the ground
rightfoot_coords.z
= leftfoot_coords.z; // no need to trace twice here
while (1)
{
leftfoot_coords.x
= player.x - left_dist * sin(player.pan);
leftfoot_coords.y
= player.y + left_dist * cos(player.pan);
rightfoot_coords.x
= player.x + right_dist * sin(player.pan);
rightfoot_coords.y
= player.y - right_dist * cos(player.pan);
if
(player_pos1 != player_pos2) // the player is moving
{
if (anim_str == anim_walk_str)
{
if (player.frame % 5 < 0.2)
{
ent_create(footprint_pcx, rightfoot_coords, create_footprints);
}
if (player.frame % 7 < 0.2)
{
ent_create(footprint_pcx, leftfoot_coords, create_footprints);
}
}
}
vec_set
(player_pos1, player.pos);
wait
(1);
vec_set
(player_pos2, player.pos);
}
}

This function is started in main so we have to make sure that the player exists before running it; that's what the first line does. We run a trace to get the surface height, like in this picture:

Trace returns the distance between the player and the ground; we substract this distance from player's height and we get the height of the ground. We add 5 quants to the result to make sure that our footprint bitmap is placed above the ground. Pretty simple, huh? We don't need to do another trace for the right foot, so we copy the distance from leftfoot_coords.

Now that we know the z coordinates for the footprints, we need to calculate the x and y coordinates. Warlock.mdl is a weird model because the distance between its feet and the origin isn't equal, but I wanted to use this model although it required more work:

I have used a simple formula to calculate the xy positions for the footprints; you will have to choose different left_dist and right_dist values if you change the model in order to make the footprints look natural.

You know that I don't like to mess with the templates (unless I am forced to do it) so I am using my original method to check if the player is moving; I store its coordinates in a vector and compare them with the coordinates that are stored one frame later. If these values are equal, the player stands still; if not, the player is moving.

if
(anim_str == anim_walk_str)
{
if
(player.frame % 5 < 0.2)
{
ent_create(footprint_pcx,
rightfoot_coords, create_footprints);
}
if (player.frame % 7 < 0.2)
{
ent_create(footprint_pcx, leftfoot_coords, create_footprints);
}
}

If the player is walking (anim_str = anim_walk_str) and its frame parameter is close to 5 (the right foot will touch the ground soon) we create the right footprint. The same thing happens with the left footprint when player.frame comes close to 7. But why haven't we used if (player.frame == 5)? The answer is simple: player.frame might not be equal to 5; we could have player.frame = 4.98 and after a frame player.frame = 5.02 so the footprint would never be created.

These footprints are only created when the player is walking; you will have to add similar code for run, crowch, etc

if (anim_str == anim_run_str)
{
....................
}

and use correct frame values for every type of animation.

Le'ts take a look at the function that creates the footprints:

function create_footprints()

{
my.passable = on;
my.oriented = on;
my.scale_x = 0.15;
my.scale_y = 0.15;
my.tilt = 90;
my.pan = player.pan;
my.flare = on;
while (my.alpha > 0)
{
my.alpha
-= 0.5 * time;
wait
(1);
}
ent_remove
(me);
}

Our footprints are passable and have the same pan angle with the player so they look natural. I have used bigger bitmaps for the footprints and I have scaled them down a lot to make sure that they look good. The footprints will fade away slowly and when they become invisible we remove them.

Hey! What if my player climbs a hill? The height for these footprints is calculated only once!

You will have to move the trace code inside the while loop and everything will be ok.


Untitled

Footstep

If you've ever wanted to change the sound of player's steps depending on the surface he walks onto, this piece of code is all you need. I have used two types of surface in my example (grass and stone) but you can add as many surface types as you want.

First of all, we need to get rid of the default foot step sound. Open camera.wdl in your template folder, find this line:

play_sound(thud,30);

and comment it by placing // in front of it. Include footstep.wdl in your main game file and call texture_scan in function main.

function
texture_scan()
{
while
(player == null) {wait (1);}
while
(1)
{
vec_set(temp, player.x);
temp.z
-= 100;
trace_mode
= ignore_me + ignore_passable + ignore_models + ignore_sprites + scan_texture;
trace (player.x,
temp);
if (str_cmpi
("grass1", tex_name))
{
texture_sound(grass);
}
else
{
texture_sound(stone);
}
step_length += 1 + 2 * key_shift;
waitt (4);
}
}

We wait for the player to be created and then we start tracing four times a second (that's what waitt (4) does). We trace between player's position and a position located 100 quants below the player. After trace, tex_name holds the name of the floor texture. We compare the floor texture name with "grass" and we call function texture_sound(grass) or texture_sound(stone) depending on the type of surface.

Function texture_sound is called four times a second, but we don't want to have four steps a second unles our player is running like Gabriela Szabo (never heard of her? never mind...). We are using the step_length variable for that; we are adding 1 to its initial value (zero) every second or 3 if shift is pressed, four times a second.

Let's take a look at:

function
texture_sound(sound_type)
{
vec_set (player_pos1, player.pos);
wait (2);
vec_set (player_pos2, player.pos);
if (player_pos1 != player_pos2)
{
if ((sound_type == grass) &&
(step_length % 6 == 0))
{
play_sound
(grass_snd, 60);
}
if ((sound_type == stone) &&
(step_length % 6 == 0))
{
play_sound
(stone_snd, 50);
}
if (step_length >= 6) {step_length
= 0;}
}
}

First of all, we need to know if the player is moving. I didn't want to modify the templates so I have decided to use a simple method, storing player's position in player_pos1 and again player's position in player_pos2 two frames later. If these values are equal, the player is standing still but if player_pos1 != player_pos2, the player is moving and we need to play the step sounds.

If the player steps onto grass and step_length % 6 = 0 (this means that step_length = 0, 6, 12, ...) we play the grass_snd sound. The same thing happens when the floor is made of stone. What's with this step_length % 6 thing? You remember that function texture_sound is called four times a second, but we don't need four step sounds a second. I chose 6 because it is divided by 1 (step_length += 1 when shift isn't pressed) and 3 (step_length += 3 when shift is pressed). You can use step_length % 3 to double the number of steps if you want to. With step_length % 6, you will hear a step sound every 6 / 4 = 1.5 seconds.

When step_length is bigger than 6, it will be set to 0; this way we make sure that we hear the step sounds even if step_length is 4 and we constantly add 3 to it (step_length % 6 = 0 could never happen).


Untitled

High scores

Some games don't need complicated load / save code; this snippet consists of a simple game that saves the high score to a file. You can use my example to learn how to save high scores, game settings, etc; you could even replace the save / load instructions that are implemented in the A5 engine.

The goal of the game is to get the high score by clicking on as many panels as possible. The panels appear at random positions so you need to be fast.

Here is my main function:

function
main()
{
fps_max =
50;
level_load (black_wmb);
wait (2); // wait for the level to be
loaded
init_mouse();
start_game();
}

I'm loading a dummy level and after 2 frames I initialize the mouse and start the game. Function init_mouse is common:

function
init_mouse()
{
mouse_map
= arrow_pcx;
mouse_mode = 2;
while (1)
{
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
wait (1);
}
}

We
choose the pointer, we set the mouse_mode to a value that shows the cursor and
then we allow the mouse to move freely using a while loop.

function start_game()
{
highscore_handle = file_open_read("hiscore.dat");
max_clicks
= file_var_read (highscore_handle);
while (timer
> 0)
{
timer -= 1;
target_pan.pos_x = random(736); // 800
- 64
target_pan.pos_y =
random(536); // 600 - 64
target_pan.visible = on;
waitt (16);
target_pan.visible
= off;
waitt (32);
}
if (current_clicks
> max_clicks) // new high score
{
highscore_handle =
file_open_write("hiscore.dat");
file_var_write (highscore_handle, current_clicks);
file_close(highscore_handle);
}
while (mouse_right == 0) {wait (1);}
current_clicks = 0;
timer = 20;
main();
}

Function start_game does (almost) all the job. First of all, it opens the hiscore.dat file and then it sets max_clicks to the value that was read from hiscore.dat. This file is a simple text file, just like your regular wdl file. I have created it in a text editor, I have named it hiscore.dat and I have typed a 0 (zero) inside it. This means that when you run the game for the first time, the high score is 0.

I have set timer to 20 so the while loop below will run 20 times:

while (timer > 0)
{
timer -= 1;
target_pan.pos_x =
random(736); // 800 - 64
target_pan.pos_y = random(536); // 600
- 64
target_pan.visible
= on;
waitt (16);
target_pan.visible
= off;
waitt (32);
}

The code inside the loop generates a random position for the panel and makes it visible for 1 second. If the player clicks on the panel, the function associated to the panel (on_click) is executed:

function add_clicks()
{
current_clicks += 1;
}

This function simply adds another click to the current_clicks var. At the end of the game the target panel has been displayed 20 times and we go on:

if (current_clicks > max_clicks) // new high score -> save it
{
highscore_handle = file_open_write("hiscore.dat");
file_var_write (highscore_handle, current_clicks);
file_close(highscore_handle);
}
while (mouse_right == 0) {wait (1);}
current_clicks = 0;
timer = 20;
main();
}

At this point the game is over; if we have got a high score it will be written to the hiscore.dat file, overwriting the existing data in it. The game will wait until we click the right mouse button and then it will reset the score (not the high score!) and restart.

You can encrypt the data that is being saved to the file if you want to; the easiest thing to do is to convert the numbers to strings, use a simple algorithm to encrypt them and save the strings on the hard disk. When you start the game, read the strings, decrypt them, convert them to numbers and you're ready to go.


Untitled

Head shots

If you are looking for a simple method to make your models react in different ways depending on the bullet impact area, you came to the right place. It is quite normal to have an enemy soldier dead if we have managed to curl its hair with a head shot, right? On the other hand, it isn't normal to kill an enemy by shooting its leg once.

I'm going to reveal my method - it is fast and it works flawlessly. I like to call it the (put something here) method.

The idea is simple: we set some key vertices on our enemy model and then we get their height in a while loop. When a bullet hits the model, we get the height of the bullet and then we compare this value with the height of every key vertex. The closest vertex will trigger its specific animation. The code is as simple as it sounds (is this good news or bad news?)

I will use a text to display a string on the screen and two panels: one for the crosshair and one for the key vertices and for bullet's coordinate.

text info_txt
{
layer = 20;
pos_x = 0;
pos_y = 0;
font = _a4font;
string = "head vertex:\nmiddle
vertex:\nleg vertex:\nhit coord:";
flags = visible;
}

panel
crosshair_pan
{
bmap = crosshair_pcx;
layer = 20;
pos_x = 0;
pos_y = 0;
flags = transparent, refresh,
d3d, visible;
}

panel
coords_pan
{
layer = 20;
pos_x = 80;
pos_y = 0;
digits = 0, 0, 4, _a4font,
1, vertex_coords.x;
digits = 0, 9, 4, _a4font, 1, vertex_coords.y;
digits = 0, 18, 4, _a4font, 1, vertex_coords.z;
digits = 0, 27, 4, _a4font, 1, bullet_pos.z;
flags = refresh, d3d, visible;
}

Function main disables the "D" (debug panel) key because we will use WSAD to move the player and then it loads the level:

function
main()
{
on_d = null;
level_load (damage_wmb);
}

This is a standalone project so we need an action for the player:

action player_moves
{
my.invisible = on; // no need to see the player because we are in 1st person mode
crosshair_pan.pos_x = (screen_size.x
- bmap_width(crosshair_pcx)) / 2;
crosshair_pan.pos_y = (screen_size.y - bmap_height(crosshair_pcx))
/ 2;
while (1)
{
vec_set (camera.pos,
my.pos);
camera.tilt +=
20 * mouse_force.y * time;
camera.pan -= 20
* mouse_force.x * time;
my.pan = camera.pan;
my.tilt = camera.tilt;
player_speed.x
= 15 * (key_w - key_s) * time; // move forward / backward
player_speed.y
= 10 * (key_a - key_d) * time; // strafe left / right
vec_set (temp,
my.x);
temp.z -= 1000;
trace_mode = ignore_me
+ use_box;
player_speed.z
= -trace (my.x, temp);
move_mode = ignore_you
+ ignore_passable;
ent_move(player_speed,
nullvector);
if (mouse_left
== 1)
{
ent_create (bullet_mdl, player.pos, shoot_bullet);
snd_play (bullet_wav, 50, 0);
while (mouse_left == 1) {wait (1);}
}
wait (1);
}
}

We don't need to see the player because we are in 1st person mode; the crosshair panel is placed in the center of the screen. The camera is placed at player's position and changes its pan and tilt by moving the mouse. The player inherits its pan and tilt from the camera and moves using the keys WSAD. The movement code includes gravity; we trace 1000 quants below player's feet and then we move the player with its feet on the ground. If we press the left mouse button (LMB) we create a bullet, a sound is played and then we wait until the mouse button is released because we want to disable autofire.

Let's see the code associated to the enemy:

action enemy
{
my.enable_impact = on;
my.enable_shoot = on;
my.enable_entity = on;
my.event = compute_shot_pos;
while (my.skill5 == 0)
{
ent_cycle ("idle", 0);
vec_for_vertex(temp, my, 222); // enemy's head vertex
my.head_pos = temp.z;
vertex_coords.x = my.head_pos;
vec_for_vertex(temp, my, 235); // enemy's belly vertex
my.middle_pos = temp.z;
vertex_coords.y = my.middle_pos;
vec_for_vertex(temp, my, 171); // enemy's leg vertex (one of the legs)
my.leg_pos = temp.z;
vertex_coords.z = my.leg_pos;
wait (1);
}
}

The enemy is sensitive to impact, shoot or other entities; if one of these events happens, it will run function compute_shot_pos. As long as the enemy hasn't been shot (skill5 = 0), it will stand still. I have used a single frame for "idle" but you can use a real "idle" animation here. I chose 3 key vertices for head, belly and leg using Med but you can choose as many as you want; their z coords are stored in head_pos (another name for skill1), middle_pos (skill2) and leg_pos (skill3). We need to get these values in a while loop because if the enemy moves (jumps, crawls, etc) these coords will change.

Ok, let's imagine that our bullet has hit the enemy -> function compute_shot_pos() will run:

function
compute_shot_pos()
{
my.skill5 = 1;
vec_set(bullet_pos, you.pos);
wait (1);
my.event = null;

Skill5 will be set to 1, stopping the while loop in action enemy. We store bullet's coords before they get lost (before the bullet disappears), we wait for a frame and then we set my.event to null.

    if (abs(bullet_pos.z - my.head_pos) < abs(bullet_pos.z
- my.middle_pos)) // we've got a head shot, no need to check the legs
{
my.skill5 = 0;
while (my.skill10
< 80) // skip the last frame
{
ent_cycle("head", my.skill10);
my.skill10 += 4 * time;
wait (1);
}
}

If the distance between the bullet and the head vertex is smaller than the distance between the bullet and the middle vertex, we have got a head shot so we play the "head" animation.

     else // middle or leg shot
{
if (abs(bullet_pos.z
- my.middle_pos) < abs(bullet_pos.z - my.leg_pos)) // middle shot
{
my.skill10 = 0;
while (my.skill10 < 80) // skip the last frame
{
ent_cycle("middle", my.skill10);
my.skill10 += 2 * time;
wait (1);
}
}
else // leg shot
{
my.skill10 = 0;
while (my.skill10 < 80) // skip the last frame
{
ent_cycle("leg", my.skill10);
my.skill10 += 4 * time;
wait (1);
}
}
}
}

If the distance between the bullet and the head vertex is bigger than the distance between the bullet and the middle vertex, it could be a middle (belly) shot or a leg shot so we play the correct animation. I have animated the model so beware... you'll see some scary stuff!

The last two functions create the bullets and remove them - here's the code:

function shoot_bullet()
{
my.enable_entity = on;
my.enable_block = on;
my.event = remove_bullet;
my.passable = on;
my.pan = player.pan;
my.tilt = camera.tilt;

The bullet is sensitive to other entities and level blocks. When it collides with another entity (our enemy) or a level block, it will be removed after a frame, allowing us to get its coords before it disappears. The bullet has the same orientation with the player and the camera.

my.skill20 = 0;
my.skill1 = 300;
my.skill2 = 0;
my.skill3 = 0;
my.skill1 *= time;

     while (my.skill20 < 250)
{
if (my.skill20
< 1)  // don't collide with the player
{
my.passable = on;
}
else
{
my.passable = off;
}
my.skill20 += 1
* time;
ent_move (my.skill1,
nullvector);
wait (1);
}
remove_bullet();
}

function
remove_bullet()
{
wait (1);
ent_remove(me);
}

We set skill20 to zero and then we increase this value inside a while loop until it reaches 250. If the bullet hasn't hit anything when skill20 >= 250, it will be removed because we don't want to waste cpu power with it. Skill1..3 are used as a local var to move the bullet. A small trick: the bullet is created inside the player model so it has to be made passable at the beginning because otherwise it would collide with the player. When skill20 >= 1 we can assume that the bullet is far enough and then we will make it impassable.

You can use more key vertices if you like; you could add another key vertex to the other leg, 2 more on the arms, etc.


Untitled

"House of terror" elevator

How can you create a huge elevator that operates at huge speeds? You can have a huge level and use (read loose) additional memory and frame rate or you could use the code below.

If you have loaded the office.wmp example that came with this piece of code you have noticed that the two floors are real close. I'm using a trick here: I'm moving the textures on the elevator walls upwards so the player things that he is moving downwards. I could move the textures for hours and the player would think that he has traveled tens of miles.

action
platform
{
my.skill11
= 100;
my.skill10 = 100;
while (vec_dist (player.x, my.x) >
50) {wait (1);}
while (player.z > 50)
{
snd_tune (snd_handle, 100, my.skill10,
0);
if (snd_handle == 0)
{
snd_loop
(elevator_snd, 100, 0);
snd_handle = result;
}
my.z -= start_speed
* time * 1.1;
my.skill10 += 5 * time;
// increase the frequency
player.z = my.z + 40;
wait (1);
}
while (my.skill11
> 0)
{
my.skill11 -= 1 * time;
elevator_pnt.v += max_speed
* time;
wait (1);
}
while (my.z > -210)
{
snd_tune (snd_handle, 100, my.skill10,
0);
my.z -= start_speed
* time * 1.1;
my.skill10 -= 5 * time;
// decrease the frequency
player.z = my.z + 40;
wait (1);
}
snd_stop (snd_handle);
}

The platform will start to move if the distance between it and the player goes below 50 quants; this assures me that the player will come close to its center. The platform will start moving downwards until player's z coordinate goes below 50 (a point near the middle of the elevator in my example). I'm changing the sound frequency for the elevator (with snd_tune and skill10) to make everything look even more "real". The platform is moved using brute force (my.z instead of ent_move) so the player must be moved together with the platform; this is why the player is placed 40 quants above the platform (an approximate value; you might need to change it if you use another player model). So what have we got so far? Regular platform / elevator code. When player.z goes below 50 this loop starts to run:

while (my.skill11 > 0)
{
my.skill11 -= 1 * time;
elevator_pnt.v += max_speed
* time;
wait (1);
}

I have defined a pointer to the elevator walls and I'm moving the textures on the walls upwards by changing their v. This part gives the "length" of the effect: if you set skill11 to bigger values at the beginning of the action, the elevator will go deeper; use smaller skill11 values for smaller elevators. When the time has passed the final while loop runs:

while (my.z > -210)
{
snd_tune (snd_handle,
100, my.skill10, 0);
my.z -= start_speed * time * 1.1;
my.skill10 -= 5 * time;
// decrease the frequency
player.z = my.z + 40;
wait (1);
}

The elevator will slow down until it reaches a certain point where it will stop (-210 in my example, get this value in Wed). The "high speed" elevator sound is reverted back to "normal speed" sound.

You should cover the elevator otherwise your client will see the trick; I've used a simple platform to allow you to see how it works.


Untitled

Infinite levels

This article will tell you how to create a level that never ends. Well, you can end the game whenever you want to, but the action can run for ever if you want that. I call this system a "treadmill" and here's how it works:

- We create a level with 9 prefabs that are placed like in the picture below. Each prefab has a different texture (named "one" ... "nine" in my demo) in fact, each prefab texture in my demo has a figure printed over it, allowing you to see how the treadmill works. Please note that the green squares and the big figures don't appear on the screen; they were added with a painting program.

- We place our player (a car in my example) in the middle (the 5th prefab in my demo).

- The code does the rest :)

If you want to create a new level you will have to add at least a level block to it; otherwise the engine will display an error message because a level can't consist only of wmb entities.

Let's examine the code for a moment:

function
main()
{
level_load(start_wmb);
fps_max =
60;
}

entity*
tile1;
entity* tile2;
entity* tile3;
entity* tile4;
entity* tile5;
entity*
tile6;
entity* tile7;
entity* tile8;
entity* tile9;

action
tile_1 // just name these prefabs
{
tile1 = me;
}

..................

action
tile_9
{
tile9 = me;
}

Function main is common. Each tile has a simple action attached to it; that action gives the tile (the prefab) a meaningful name (tile1... tile9).

action
car
{
var car_speed;
while ((tile1 == null)
|| (tile1 == null) || (tile3 == null) || (tile4 == null) || (tile5 == null) ||
(tile6 == null) || (tile7 == null) || (tile8 == null) || (tile9 == null)) {wait
(1);}
player = me;
while (1)
{
if(car_speed != 0) // can't rotate the car
if it isn't moving
{
my.pan -= 0.2 * key_force.x * car_speed * time;
}
car_speed = 3 * key_force.y * time + max
(1 - time * 0.2, 0) * car_speed;
ent_move (car_speed,
nullvector);
if (key_t == 0) // normal 3rd
person camera view
{
camera.x = player.x - 300 * cos(player.pan);
camera.y = player.y - 300 * sin(player.pan);
camera.z = player.z + 100;
camera.pan += 0.2 * ang (player.pan - camera.pan);
camera.tilt = -10;
}
else // press and hold "T" to get a top view
{
camera.x = player.x;
camera.y = player.y;
camera.z = 8000;
camera.pan
= player.pan;
camera.tilt
= -90;
}
if (total_frames % 31) // trace twice a second, starting with the 1st frame
{
trace_below(); // trace the name of the texture below the car
}
wait (1);
}
}

The action above is attached to our player (a car); it waits until all the nine level tiles are loaded, and then it allows us to control the car using the arrow keys. I have used a simple car action because I wanted to allow you to travel and notice the changes quickly; you should use your player action instead. Oh, and you can press and hold the shift key if you want to double the speed of the car.

The action includes 2 different cameras: an isometric view and a top view; press and hold the "T" key to see the action from a bird's eye perspective.

The same "action car" calls the function named "trace_below" twice a second:

function
trace_below()
{
vec_set(temp.x, player.x);
temp.z -= 10000; // trace 10,000 quants below the car
trace_mode
= ignore_me + ignore_passable + ignore_models + ignore_sprites + scan_texture;
trace (player.x, temp);
if (str_cmpi ("one",
tex_name)) // if the texture below the car is named "one" in wad
{
tile2.x = tile1.x;
tile2.y = tile1.y - 2048;
tile3.x = tile1.x
- 2048;
tile3.y = tile1.y - 2048;
tile4.x = tile1.x + 2048;
tile4.y = tile1.y;
tile5.x = tile1.x + 2048;
tile5.y = tile1.y - 2048;
tile6.x = tile1.x - 2048;
tile6.y = tile1.y;
tile7.x = tile1.x + 2048;
tile7.y = tile1.y + 2048;
tile8.x = tile1.x;
tile8.y = tile1.y + 2048;
tile9.x = tile1.x - 2048;
tile9.y = tile1.y
+ 2048;
}
.....................
if (str_cmpi ("nine", tex_name)) // if the texture below the car is named
"nine" in wad
{
tile1.x
= tile9.x + 2048;
tile1.y = tile9.y - 2048;
tile2.x = tile9.x;
tile2.y = tile9.y - 2048;
tile3.x = tile9.x
- 2048;
tile3.y = tile9.y - 2048;
tile4.x = tile9.x + 2048;
tile4.y = tile9.y;
tile5.x = tile9.x - 2048;
tile5.y = tile9.y + 2048;
tile6.x = tile9.x - 2048;
tile6.y = tile9.y;
tile7.x = tile9.x + 2048;
tile7.y = tile9.y + 2048;
tile8.x = tile9.x;
tile8.y = tile9.y + 2048;
}
}

This function does all the hard work, trust me! It traces between player's origin and a point that is located 10,000 quants below, returning the name of the texture that is hit by the tracing ray. If the name of the texture is "one", the player is driving the car on the first prefab, so some of the other prefabs behind the player are moved in front of it. Each prefab has 2048 x 2048 quants, so if I would add that amount to a tile it would replace another tile, get it? The things repeat for all the 9 prefabs and the code moves the proper prefabs every time, creating the illusion (or the reality!) of a level that never ends. If you want to understand how the treadmill works simply run the level, press and hold the "T" key and then move the car around using the arrow keys.

The player starts from the 5th tile (prefab); here's what happens if it moves over one of the neighboring prefabs:

Are you a bit confused? Ok, let's discuss the example in the first picture. The player is on the 5th tile and, at a certain moment, while it is driving the car, it moves over the 1st tile. At that moment, the tiles behind its back (3, 6, 9, 8 and 7), which can't be seen, are moved in front of the player - these are the red squares 3, 6, 9, 8, 7. This means that the player won't see anything wrong and we will get what we wanted - an infinite level. What happens if the player returns to the 5th tile? Nothing special; all the pieces are moved back just the way they were before.

Hold on, I've got two more tricks:

action
sky_sphere
{
my.passable = on;
while (player
== null) {wait (1);}
while (1)
{
my.x = player.x;
my.y
= player.y;
wait (1);
}
}

The first idea is to have a huge sky sphere that moves together with the player, creating the illusion of a huge level. The passable sphere will wait until the player is loaded, and then it will get its x and y coordinates every frame.

function toggle_fog() // toggles fog if you press the "F" key
{
fog_color = (fog_color == 0); // set fog_color to 0 (no fog) or 1
camera.fog_start = 500; // the fog starts at 500 quants
camera.fog_end = 2500; // and ends at 2500 quants
}

on_f toggle_fog;

The second trick is the usage of fog; the fog is your friend because it can hide the transitions. In my example the fog can be toggled with the "F" key. If your tiles are bigger than 2048 quants (and they should be bigger) you will be able to use thinner fog (bigger camera.fog_end values) and get better results.

Where do we go from here? Well, we could use tilable terrains for our level prefabs and / or we can create different level parts that use the same tiles. Let me rewind a little.

Let's assume that our car is placed on the first tile (tile1) and moves towards right, as indicated by the yellow arrow. At a certain moment, tile1 will be moved at 1', which includes 3 trees (those big blue dots) and then, as the player advances, tile1 will be moved from 1' to 1", which includes 4 trees. Those trees were placed in Wed, at the proper height, in the air, but when tile1 moves below them (to its new position) they'll look natural. You can add trees, houses, whatever you'd like to use in your level. A good level design method would be to copy and paste tile1 several times, place the level details on the copies as needed, and then remove all the tile1 prefabs excepting the original tile1.


Untitled

Intelligent rain

We love particles, don't we? They look cool and they're so fast! The bad news is that they can't collide with anything - that's why they're so fast. What happens if you want to have rain or snow only outside the house? You could use several particle generators (and small ranges) carefully placed around the house or you could use the intelligent rain code presented below.

The idea is pretty simple: we don't use particles, but sprites and we check to see if they are penetrating something solid. If this is true, the drops stop. Let's take a look at the action that generates the rain drops:

define area_x skill1;
define area_y skill2;
define speed skill3;
define number_of_drops skill4;

action
rain_maker
{
rain_ptr =
me;
my.invisible = on;
my.passable = on;
if (my.skill1 + my.skill2 + my.skill3
+ my.skill4 == 0)
{
my.area_x = 1000;
my.area_y = 1000;
my.speed = 20;
my.number_of_drops
= 300;
}
while (my.skill5 < my.number_of_drops)
{
temp.x = my.x
+ my.area_x / 2 - random(my.area_x);
temp.y = my.y + my.area_y / 2 - random(my.area_y);
temp.z = my.z;
ent_create(rain_pcx,
temp, create_drops);
my.skill5 += 1;
wait (1);
}
}

You can attach the rain_maker
action to any entity because it will be made invisible and passable. I have set
some decent names for skills1..4: the raining area on x and y (related to the
rain_maker entity), the drop speed and the number of rain drops. If we forget
to set these skills in Wed, their sum is zero so the code switches to defaults:
1000 quants on x and y, speed = 20 and 300 rain drops.

Temp stores a random position in a rectangular area set by area_x and area_y; a new drop sprite will be created at this position. Skill5 is increased with every drop so the while loop will continue to run until all the drops are created. The function that runs for every drop is:

function create_drops()
{
my.scale_x
/= 2;
my.transparent
= on;
my.passable
= on;
my.flare
= on;
while (1)
{
my.z -= (random(rain_ptr.speed)
+ rain_ptr.speed) * time;
if (content(my.x) == content_solid)
{
my.z
= rain_ptr.z;
}
wait (1);
}
}

You can use any sprite but it can't have a width below one pixel. I think that a pixel is a little too thick when you are close to the drop so the first line halves the width of the sprite. The drop is transparent, passable and has its flare flag set. The sprite moves in a while (1) loop by changing its z. I'm using a random value so different drops have slightly different speeds.

If the drop hits a brush (a ceiling, etc) its content will be content_solid; the drop will be moved up (we won't be able to notice anything) and we will let it fall again. This way we don't have to create all the drops again and again and we will get a frame rate increase.

Make sure that you don't use thousands of sprites for your rain drops and don't forget to increase the nexus as you add more particles. If you plan to use 400 rain drops your nexus needs to be at least 50.

If some of your drops penetrate the ceiling (this happens with some of the drops in the office level) you can make the ceiling thicker, the drops smaller or you could decrease the drop speed.


Untitled

Instant debugger

"Instant debugger" sounds too good to be true, isn't it? I'll let you decide if this tool is useful or not: run the engine, press "I" to display the debugger, right click to show the mouse pointer and then move the cursor over any entity in the level. You will see its x, y, z, pan, tilt, roll, red, green, blue, ambient, alpha, skill1... skill8 and frame. If one of these values changes, you will see the change in real-time; this way you can test and improve your code without having to use other painful tools. I'm a shy guy but I could have added the rest of the skills, the flags and many more things on the same panel, trust me!

If you like this debugger you might be anxious to see how it works; however, if you expect to see a lot of complicated code, you will be disappointed. The code includes a text and a panel:

text
info_txt
{
font = terminal9_font;
pos_x = 10;
pos_y = 10;
string = "x:\ny:\nz:\npan:\ntilt:\nroll:\nred:\ngreen:\nblue:\nambient:\nalpha:\nskill1:\nskill2:\nskill3:\nskill4:\nskill5:\nskill6:\nskill7:\nskill8:\nframe:";
flags = d3d, visible;
}

The panel has a huge string because I display all the text with the same string, using newline (\n). I use a panel with digits to display the values:

panel info_pan
{
pos_x = 0;
pos_y = 0;
digits = 50, 10, 5, terminal9_font,
1, mouse_ent.x;
digits = 50, 22, 5, terminal9_font, 1, mouse_ent.y;
digits = 50, 34, 5, terminal9_font, 1, mouse_ent.z;
digits = 50, 46, 5, terminal9_font,
1, mouse_ent.pan;
digits = 50, 58, 5, terminal9_font, 1, mouse_ent.tilt;
digits = 50, 70, 5, terminal9_font, 1, mouse_ent.roll;
digits = 50, 82, 5, terminal9_font,
1, mouse_ent.red;
digits = 50, 94, 5, terminal9_font, 1, mouse_ent.green;
digits = 50, 106, 5, terminal9_font, 1, mouse_ent.blue;
digits = 50, 118, 5, terminal9_font,
1, mouse_ent.ambient;
digits = 50, 130, 5, terminal9_font, 1, mouse_ent.alpha;
digits = 50, 142, 5, terminal9_font, 1, mouse_ent.skill1;
digits = 50, 154, 5, terminal9_font, 1, mouse_ent.skill2;
digits = 50, 166, 5, terminal9_font, 1, mouse_ent.skill3;
digits = 50, 178, 5, terminal9_font, 1, mouse_ent.skill4;
digits = 50, 190, 5, terminal9_font, 1, mouse_ent.skill5;
digits = 50, 202, 5, terminal9_font, 1, mouse_ent.skill6;
digits = 50, 214, 5, terminal9_font, 1, mouse_ent.skill7;
digits = 50, 226, 5, terminal9_font, 1, mouse_ent.skill8;
digits = 50, 238, 5, terminal9_font, 1, mouse_ent.frame;
flags = refresh, d3d, visible;
}

I'm using a lot of "digits" instructions. All the values are displayed using up to five figures, with a font named terminal9_font. The values are multiplied by 1 (so they aren't changed). Now comes the weird part: all the values look like this: mouse_ent.x, mouse_ent.roll, mouse_ent.skill2... what's with this mouse_ent thing? According to the manual, mouse_ent is the entity that has the mouse pointer placed over it. If we place the mouse pointer over a door, mouse_ent will point to the door so we will get to see the x, y, z, pan, tilt, roll, ... for that door. The process works the same for any entity in the level; if we don't touch an entity with the mouse pointer, all the values will be zero.

I'm using a single function, but I'm using a "starter" because I need it to run at game start:

starter
debug_init()
{
while (1)
{
if (key_i == 1)
{
while (key_i == 1) {wait (1);}
info_txt.visible = (info_txt.visible == off); // toggle visibility
info_pan.visible = (info_txt.visible == on); // same thing here
}

If we press the "I" key we will show / hide the debugger (the text and the panel that displays the figures). We have to wait until the "I" key is released, otherwise the debugger would appear and disappear every frame until we release the "I" key.

         if (mouse_ent != null)
// entity detected?
{
if (total_frames % 100 == 1) // play a sound every 100 frames
{
snd_play(entity_wav, 10, 0);
}
}
wait (1);
}
}

If we move the mouse pointer over an entity, mouse_ent is assigned to that entity so it isn't null. If this happens, we play a small sound every 100 frames, notifying the player that the pointer is placed over an entity so the values shown by the debugger are good.

If you want to display more values, add more "digits" instructions to the panel definition. That's all!


Untitled

Landmines

Let's imagine this situation: you are being chased by way too many enemies. Your health is lower and lower and every bullet gets you closer and closer to a dishonorable death. And this hallway looks like it will never end! (Although you are hurt, you can't stop thinking that Acknex is such a great engine...) The enemies are right behind you, but wait, there's a door over here! You open the door and then you hear two short beeps. A spectacular jump saves your live. The landmine explodes without hurting you at all! But this isn't true for your enemies...

If this sounds good to you, rejoice: this article will teach you how to create a landmine that can kill you or your enemies. You can make your enemies follow you and if they get close to the landmine they will die.

The code isn't complicated at all:

action
landmine
{
my.skill1 = 0;
my.enable_detect = on;
my.event = hurt_them;

We are setting skill1 to zero; its value will remain zero as long as the mine hasn't detected an entity. We set enable_detect to on; this way the mine will be sensitive to any entity with its enable_scan set: the player and the "normal" enemies (actor_ai_one, for example). If one of these entities approaches the landmine, function hurt_them will run.

     while (my.skill1 == 0)
{
temp.pan = 360;
temp.tilt = 180;
temp.z = 200;
scan_entity(my.x,
temp);
if ((result >
0) && (result < 200))
{
my.ambient = 100;
ent_playsound (my, beephigh_wav, 500);
sleep (0.3);
my.ambient = 0;
sleep (0.3);
my.ambient = 100;
ent_playsound (my, beephigh_wav, 500);
my.skill1 = 1; // get out of the while loop
}
sleep (0.2); //
scan 5 times a second
}

As long as skill1 = 0, we scan everything around the landmine, like in the picture below, five times a second:

Nothing can escape our scan; more than that, we can simply set temp.z to a bigger value to increase the scanning range. If the mine has detected an entity, it increases its ambient, plays a sound then waits for 0.3 seconds. The ambient is set back to zero, we wait for 0.3 seconds and the sequence repeats. As soon as the second beep is played, we set skill1 to 1 so that we can get out of the while loop. Sleep (0.2) will make the engine scan 5 times a second; if you think that your enemies are faster than that (if you are creating a space game, for example) you can lower this value.

You can see that we didn't hurt anybody until now; the code that does that is located in function hurt_them():

function
hurt_them()
{
sleep (1);
you._health -= 200;
}

We wait for a second; the entity is already dead by now so we give the illusion that he / she / it might escape. This can't happen because the next line decreases 200 health points from entity's health. A landmine should kill everybody, isn't it?


Untitled

Level changing code

Acknex uses a simple level loading system; a single line of code allows you to move from one level to another. If you have tried to do that you have noticed that the new level "erases" the weapons, items, etc so you need to create them again somehow. There are only two golden rules so you'd better learn them right now:
1) Global variables aren't erased;
2) A function will continue to run after you load a new level only if you add this line of code to it:

me = null;

What are global variables? They are nothing more than your normal variables defined at the beginning of your C-Script files, OUTSIDE any function definition:

var number_of_mice;

These vars keep their values even after a level_load instruction so you can use them to store the number of bullets, the number of lives, etc - they will survive the level loading.

What is it with those functions? When you load a new level, all the entities in the old level are removed (sounds fair to me). This means that the actions or functions attached to any entity would give some error messages because their parent entity has disappeared. My = null will make the function say to the engine: "Hey, I'm on my own here! I don't have any entity attached to my tail so don't shut me down!"

I have used the standard office.wmp as the first level and a slightly modified version of it (including a huge "2" figure) for the second level. The first level (office.wmp) includes 2 weapons and an entity that will trigger the level change; let's take a look at the code for the weapons first:

action
pistol
{
my.__rotate = on; // rotates before being picked up
my.__repeat = off; // disable auto fire
my.__silent = on; // don't
show any pickup message
my._offs_x = 30; // position of the pistol
my._offs_y = -10; // on x, y, z
my._offs_z = 15;
my._ammotype = 1.20; // uses ammo type 1, comes with 20 bullets
my._weaponnumber = 1; //
press "1" to select this weapon
my._bulletspeed = 1000.01; // speed.recoil
my._firetime = 8; // time to reload
my._firemode = damage_shoot + hit_hole + gunfx_brass + gunfx_animate + 0.10; //
hit holes on the walls, damage = 10 points / shot
gun(); // call the real gun action
}

action
machine_gun
{
my.__rotate = on; // rotates before being picked up
my.__repeat = on; // auto fire
my.__silent = on; // don't
show any pickup message
my._offs_x = 35; // position of the gun
my._offs_y = 10; // on x, y, z
my._offs_z = 15;
my._ammotype = 2.99; // uses ammo type 2, comes with 99 bullets
my._weaponnumber = 2; //
press "2" to select this weapon
my._bulletspeed = 5000.01; // speed.recoil
my._firetime = 2; // time to reload
my._firemode = damage_shoot + hit_hole + gunfx_brass + gunfx_animate + 0.20; //
hit holes on the walls, damage = 20 points / shot
gun(); // call the real gun action
}

I have created a pistol that comes with 20 bullets and a machine gun that comes with 99 bullets. Press "1" and "2" to select the weapons after you have picked them up. The entity that triggers the level change has this piece of code attached to it:

action
level_12
{
my.enable_impact = on;
my.event = change_levels_12;
}

function
change_levels_12
{
wait (1);
if (you != player) {return;}
me = null;
level_load (level2_wmb);
sleep (1);
restore_12();
}

The entity is sensitive to impact; when any other entity collides with it, its change_levels_12 event function will run. If the entity that has collided with the trigger (you) isn't the player, we won't change the level; otherwise we set me = null to make sure that this function will survive the level_load instruction, we wait for a second and then we call function restore_12() which is supposed to restore the items taken from level 1 to level 2.

function restore_12()
{
while (player == null) {wait (1);}
if (weapon_1 == 1) // if the player has picked up weapon1
{
ent_create (weapon1_mdl, nullvector, pistol); // create the pistol again
}
if (weapon_2 == 1) // if the player has picked up weapon1
{
ent_create (weapon2_mdl, nullvector, machine_gun); // create the gun again
}
gun_loaded = 1; // make sure that it is loaded
gun_select(); // and select it
}

This function waits for the player to be created; if the player has picked up weapon1 in level1, the weapon is created again and runs the same action (pistol); if the player has got weapon2 in level1, that weapon will be created too. We make sure that the gun is loaded and then we select the last weapon that was used in the first level.

I have used a few variables that are defined inside weapons.wdl to keep the things simple. If you plan to write your weapon / inventory / etc code from scratch here's what you need to do to make sure that you can carry all the items in all the levels:
1) Set a certain variable to 1 when you have picked up a certain object (weapon_1 = 1 or key_blue = 1, etc);
2) Add me = null to the body of the function that does the level changing BEFORE the new level_load instruction;
3) Wait a little (sleep (1) in my example but you could wait (2) etc);
4) Call the function that restores the items;
5) The function that restores the items will check which items were picked up in the previous level (key_blue = 1 or 0?) and will create them again (using ent_create) if their associated vars were set to 1.
6) Repeat the same process for all your levels (restore_23, restore_34, etc).


Untitled

Light gate

Whenever you start a new Half-Life (HL from now on) game, you can't skip the intro. At a certain moment there's a gate on the left; as this vertical door opens, you see the "light" coming out of the door - and this is what I'm going to do here.

I think that this effect should be used for cut scenes or for places that can't be reached by the player. I can hear you asking "Why?". Well... because everything has to be faked the way they did it in HL.

HL uses a sprite for the light; we will use a model because it is a 3d object and it will look much better if you use a good texture for its skin. I have used a flat white texture but I'm not a modeler; in fact, I could easily teach you how "not to" model.

The idea is simple: when the gate moves upwards, we move the light model to the right; when the gate is lowered, we move the light model to the left. Let's take a look at the action attached to the light model first:

entity* light_ptr;

action
light_model
{
light_ptr = me;
my.skill1 = my.y;
my.transparent = on;
my.passable = on;
my.alpha = 30;
}

I have created a pointer to the light model; this way I can use the light model in any other action or function. The model moves on the y axis in my example level; we can't be sure that by moving back and forth several times the model will return to its starting position, so we store it in skill1. The model is transparent, passable (the other entities should pass through the light, right?). The transparency factor is set to 30.

The
door does the rest of the job - let's see its code:

action hl_gate
{
my.skill1 = my.z;
while (light_ptr == null) {wait (1);}
while (player == null) {wait (1);}
while (1)
{
while (vec_dist
(player.x, my.x) > 600) {wait (1);}
while (my.z <
my.skill1 + 200)
{
my.z += 3 * time;
light_ptr.y -= 4 * time;
wait (1);
}
while (vec_dist
(player.x, my.x) < 700) {wait (1);}
while (my.z >
my.skill1)
{
my.z -= 5 * time;
light_ptr.y += 7.5 * time;
wait (1);
}
my.z = my.skill1;
// correct the displacement
light_ptr.y = light_ptr.skill1;
wait (1);
}
}

We store the z coordinate for the gate in skill1 because we must place it at the correct position every time. We wait for the light model and for the player model to be loaded and then we enter a while (1) loop - a loop that runs forever.

As long as the distance between the player and the gate is bigger than 600 quants, the gate does nothing. When the player has come closer, the gate will move upwards until its height is 200 quants bigger than its original height. At the same time, the light it moved towards the player with a speed given by 4 * time; play with 4 until you get the best results. The gate has opened, the light model has appeared, now what? As long as he player is closer than 700 quants to the gate, nothing happens. When the player moves away from the gate, the gate is lowered until its height is close to the starting position. At the same time, the light is moved away from the player (to the left in the picture above) with a speed given by 7.5 * time.

The last lines correct the displacement for the gate and for the light model. If the gate has moved from (just an example) 200 to 400 quants several times, it could go down to 199.2 instead of 200 quants. These lines set the gate and the light model at their starting positions.

This effect looks great at night! Why can't I use this effect for all the doors in my level? What if the player passes through the doors?
It wouldn't look that great if the player passes through the light model. All the triangles that compose the light model must be 100% visible to be rendered. My model consists of only a few triangles but if you really want to use this effect here are some <tested> ideas:
- split the light model in as many faces as possible: open Med, choose triangle mode, select a face - press "subdivide". This way you will get much smaller triangles but don't forget to keep an eye on the poly count;
- make the light model more transparent when you approach it;
- use white fog for the walls inside the room and increase camera.fog depending on the distance between the player and the light model.


Untitled

Mastering cameras

I know that some of you could use some simple camera code snippets so here they are. Just make sure that you include cameras.wdl then run your level and press 1…4 to change the view: we have top view, side view, isometric view and marker view. Marker view is used for one of the “antennas” in First AI demo and I thought that you might find it useful in one of your projects.

I’m not sure if you have noticed that all my demos are standalone projects or files that don’t modify the templates – this is why I have disabled the default (camera) view. I have created a new view named aum_camera – its parameters are set in this function:

function
set_cameras()
{
camera.visible = off;
aum_camera.size_x =
screen_size.x;
aum_camera.size_y =
screen_size.y;
aum_camera.pos_x =
0;
aum_camera.pos_y = 0;
aum_camera.visible = on;
select_camera();
}

You can see that the size for aum_camera is read from screen_size; therefore we don’t need to know the screen resolution. We make the new view visible and we call select_camera; a simple while(1) loop checks if we have pressed the keys 1...4 and changes the view accordingly:

Top view

if
(camera_number == 1)
{
aum_camera.x = player.x;
aum_camera.y = player.y;
aum_camera.z = player.z
+ 300;
aum_camera.pan = player.pan;
aum_camera.tilt = -90;
aum_camera.roll = 0;
}

The first camera is really simple: it follows the player all the time, it is placed above the player (aum_camera.z = player.z + 300) and it looks down at the player (aum_camera.tilt = -90)

Side view

if
(camera_number == 2)
{
aum_camera.x = player.x + 200 * sin(player.pan);
aum_camera.y = player.y
- 200 * cos(player.pan) ;
aum_camera.z = player.z + 10;
aum_camera.pan = player.pan
+ 90;
aum_camera.tilt = 0;
aum_camera.roll = 0;
}

This camera requires a little trigonometry, so I’d better let an image speak for me:

You can replace 200 with other values: smaller numbers will make the camera come closer to the player. I have lifted the camera a little above the ground (player.z + 10) and I have rotated it towards the player (player.pan + 90).

Isometric view

if
(camera_number == 3)
{
aum_camera.x = player.x - 200 * cos(player.pan);
aum_camera.y = player.y
- 200 * sin(player.pan);
aum_camera.z = player.z + 200;
aum_camera.pan = player.pan;
aum_camera.tilt = -30;
aum_camera.roll = 0;
}

This is your regular 3rd person camera. It works fine but it hasn’t any form of collision detection. If you are interested in a good 3rd person camera code (the one that is used in Rune is the best I’ve seen so far) maybe I’ll write the code for it in one of the future AUM editions. Here’s the additional picture:

I have lifted the camera a lot this time because I wanted to tilt it down (aum_camera.tilt = -30)

AI demo (marker) view

if
(camera_number == 4)
{
aum_camera.x = player.x + 200 * cos(player.pan)
+ 150 * sin(player.pan);
aum_camera.y = player.y + 200 * sin(player.pan)
- 150 * cos(player.pan);
aum_camera.z = player.z;
aum_camera.pan = player.pan
+ 130;
aum_camera.tilt = 0;
aum_camera.roll = 0;
}

This is the last camera; if you are looking at your monitor right now (I hope you are, although I’d be proud to know that you have memorized AUM4 :) this camera would be placed in the upper right corner of your monitor, looking at you all the time and moving with you. I have used similar code for the two “antennas” in First AI demo – here are the pictures:

Now that you have heard about the antennas (the camera is placed as if it was the front right antenna in the next picture), you are prepared to see the… first AI demo


Untitled

Working with materials

One of A6's most powerful features is its ability to use materials. The difference between A5 and A6 might not appear too obvious when it comes to materials, but it certainly is big. Take a look at the screenshot below:

Can you believe that all the models above are clones of the "normal" crystal, but are using different materials? This means that I have used the same model over and over, without touching its skin and I have changed its appearance by setting the values of a few variables. Sky is the limit, really! Now here's the other good news: the project allows you to create any type of material you can think of.

Let's see what this demo can do for us; open materials.wmp, build it and run it using materials.wdl. You will see the picture with the four crystal models; I think that the author of the model is TMW, but he didn't include a readme file with the model, so I can't be too sure about that. Well, the first "normal" model hasn't got any material assigned to it, the second model uses Conitec's "smaragd" material, and I have created the other two materials ("lava" and "marble").

Take a good look at those models, and then press the "R" (rotate) key once to move on:

You can see the same "normal" model (without using a material), a "shiny" model and a "dark" model. Once again, I have used the same model with the same skin and all these dramatic changes are nothing more than different materials used for the model. Ready for more fun? Press the "R" key again.

This time you can see the "normal" model and two predefined materials ("metal" and "unlit"). Press "R" one more time to see our "material" creation lab:

This is where you will want to place your model in order to create the perfect material for it, by adjusting all the parameters on the panel. Click every arrow to see the changes; some of them will produce noticeable effects only under certain circumstances, but we'll be getting there soon.

Some of you might have noticed the black model surrounded by the yellow circle in the upper left corner of the screen. I drew the yellow circle to make the model more visible in my picture; the model is used for a dynamic light that can be dragged around the screen.

Most of the arrows that appeared to be useless show their power in the presence of the light coming from the sun or from a regular dynamic light. You can see that the position of the mouse pointer and that of the light don't match; I have decided to use this method because I didn't want you (or me) to lose the light entity by moving it behind the panel or outside the boundaries of the screen.

What is it with these materials, after all? They are nothing more than a collection of parameters that control the light reflection properties of a model, sprite, terrain or surface. The light that affects the entity can be received from the level or can be generated by the entity itself.

The introduction is over now, so get ready for some easy to understand code. Let me show you the panel definition first:

panel
material_pan
{
bmap = menu_pcx;
pos_x = 0;
pos_y = 0;
layer = 10;

     button = 90, 12, up_pcx, up_pcx, up_pcx, change_material,
null, null;
button = 105, 12, down_pcx, down_pcx,
down_pcx, change_material, null, null;
button = 90,
37, up_pcx, up_pcx, up_pcx, change_material, null, null;
button = 105, 37, down_pcx, down_pcx, down_pcx, change_material, null, null;
button = 90, 62, up_pcx, up_pcx, up_pcx, change_material, null, null;
button = 105, 62, down_pcx, down_pcx, down_pcx, change_material, null, null;

...................................

digits = 125, 12, 3, arial_font, 1, e_red; // emissive_red
digits = 125, 37, 3, arial_font, 1, e_blue; // emissive_blue
digits = 125, 63, 3, arial_font, 1, e_green; // emissive_green

....................................

flags = overlay, refresh, visible;
}

The panel uses a button for each black arrow, so we've got 30 buttons and 15 digits that show the values for every material parameter. Please note that when we click a button (any button) we run a single function: change_material().

Function main is simple:

function
main()
{
fps_max = 30;
sun_light = 1;
level_load (materials_wmb);
mouse_map = pointer_pcx;
mouse_mode = 2;
while (1)
{
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
wait (1);
}
}

The function above limits the frame
rate to 30 fps, enables the sun light and then loads the materials_wmb level.
We set pointer_pcx as the mouse pointer, we show the pointer and we allow it to
move by setting its mouse_pos.x and mouse_pos.y coordinates in a "while" loop.

You can be sure that materials.wdl contains definitions for many materials, but we will discuss only a few of them. The reason is simple: they use similar definitions and different numerical values. Let's start with the material used for the lava:

material
mat_lava
{
emissive_red = 200;
emissive_green = 70;
emissive_blue = 20;
ambient_red = 255;
ambient_green = 150;
ambient_blue = 50;
diffuse_red = 200;
diffuse_green = 150;
diffuse_blue = 50;
specular_red = 255;
specular_green = 255;
specular_blue = 255;
alpha = 60;
albedo = 70;
power = 10;
}

The first 3 (emissive) parameters set the power and the color of the light that is generated by the entity itself. You can be sure that the "shiny" material in one of the pictures above was created by setting the emissive parameters to their maximal values (255), while the "dark" material was obtained by setting the emissive parameters to the smallest value (0).

The next set of parameters (ambient) set the color and the strength of the light that will be received by the entity from the static lights inside the level, its own "ambient" or the "ambient" of the camera. Let's pretend that your game takes place on Mars and most of the objects, including those precious crystals, ought to look red. So your sun is red, the ground is painted in red, but those crystals aren't quite red. You can use those 3 "ambient" parameters to create a material that absorbs more red from the static light sources in the level; take a look at what I've got by playing with the "ambient" values:

You can notice that this material isn't using its emissive parameters, because it isn't generating light; it simply receives more red from the white, static light sources. Of course that you can (and you should) combine proper "emissive" and good "ambient" values.

The next set of parameters (diffuse) sets the influence of the dynamic lights (including the sun light). Let's make a little experiment: run materials.wdl, set diffuse_green to 255 and take a good look at the "user defined" crystal:

I couldn't see any change, no matter how hard I tried! Now let's click the black model (the dynamic light) and let's drag it close to the crystal model:

You can see that my white dynamic light casts green light on the crystal model; the material ignores the red and blue components of the white light because diffuse_red and diffuse_blue were set to zero. The last set of parameters (specular) is similar to "diffuse", but this time the effect depends on the camera angles.

This time I have set specular_blue to 255. The result is obvious: only a part of the crystal model is affected by the dynamic light; the big specular_blue value colors the crystal in blue, but you can see the effect only when the camera has a certain pan angle. I encourage you to press the "zero" key while the engine is running; this way you will be able to move and rotate inside the level. One more thing: you can click the "user defined" crystal to start / stop its rotation around the pan angle.

The last 3 parameters ("power", "alpha" and "albedo") are pretty straightforward: they set the power / sharpness of the effect (0...10), the opacity of the material (0...100) and the influence of the sun light (0...100).

You will notice that some of the material definitions inside material.wdl don't include all the parameters, because sometimes we won't need them all. Ok, so now we know how to define a material. How do we use it?

action test_lava
{
my.material = mat_lava;
..................................
}

It's really that simple, isn't it? We create the action that will be attached to our entity, and then we use a single line of code to assign our previously defined material to the entity. The engine knows how to work with several predefined materials like mat_flat, mat_shaded, mat_metal and so on; please note that you can change the values for them too. Here's the action attached to the "metal" crystal in the demo:

action test_metal
{
my.metal = on;
mat_metal.emissive_red = 50;
mat_metal.emissive_green = 50;
mat_metal.emissive_blue = 80;
}

The first line of code sets metal = on for my crystal; this way mat_metal is set for my model automatically. Now I can control all the parameters of the predefined mat_metal material; I chose to make it a little more metallic by adding a bit more light (especially blue light) to it. Let's take a look at the action attached to the dynamic light now:

action
dynamic_light
{
my.lightrange = 80;
my.red
= 100;
my.green = 100;
my.blue = 100;
my.enable_click = on;
my.event = move_me;
}

The light has a range of 80 quants and it generates white light because its red, green and blue components have the same value (100). The entity is sensitive to clicking; click it once and its move_me event function will run:

function
move_me()
{
while (mouse_left == on)
{
temp.x = pointer.x;
temp.y = pointer.y;
temp.z = 100;
vec_for_screen (temp, camera);
my.x = temp.x;
my.z = temp.z;
wait (1);
}
}

This function moves the entity that generates light for
as long as the left mouse button is kept pressed. Take a look at the picture below
to see the area that can be covered by the light emitter for temp.z = 100; change
that value if you want to cover a different area. The coordinates stored in temp
are converted to 3D world coordinates using vec_for_screen, and the entity is
moved to its new position.

Now here's the code for the action attached to the "user defined" model:

action
test_user
{
my.material = mat_user;
my.enable_click
= on;
my.event = rotate_me;
}

function rotate_me()
{
rotation += 1;
while ((rotation % 2) == 1)
{
my.pan += 5 * time;
wait (1);
}
}

The model uses the mat_user material, is sensitive to clicks and runs its event function rotate_me when you click it using the left mouse button. The model will rotate around its pan angle for as long as rotation has an odd value: 1, 3, 5, 7, etc.

Do you remember that huge panel definition we've talked about at the beginning of the article? Every button (arrow) on the panel runs the same function when we click it:

function
change_material(button_number)
{
if (button_number == 1)
{
while (mouse_left == 1)
{
e_red += 1;
e_red
= min(max(e_red, 0), 255);
wait (1);
}
wait (1);
}
if (button_number == 2)
{
while (mouse_left == 1)
{
e_red -= 1;
e_red = min(max(e_red, 0), 255);
wait (1);
}
wait (1);
}

......................
}

This function is pretty big because it is used by all those 30 buttons (arrows) on the panel. I have decided to show you the code for the first two buttons here because the rest of the code is similar.

If you are an avid AUM reader you have learned that function change_material returns "1" when we press the first button on the panel, "2" when we press the second button on the panel and so on. The first two buttons increase and decrease e_red, a variable that controls the emissive_red parameter.

If we press the first button (arrow) on the panel, we keep incrementing e_red for as long as the left mouse button is pressed. The line of code below:

e_red = min(max(e_red, 0), 255);

makes sure that the value for e_red is kept in this range: 0...255. If we press the second button (arrow) on the panel, e_red is decremented for as long as the left mouse button remains pressed.

Take a look at our "user defined" material definition:

material mat_user
{
emissive_red = 0;
emissive_green = 0;
emissive_blue = 0;
ambient_red = 0;
ambient_green = 0;
ambient_blue
= 0;
diffuse_red = 0;
diffuse_green = 0;
diffuse_blue = 0;
specular_red = 0;
specular_green
= 0;
specular_blue = 0;
power = 0;
}

All these mat_user parameters are controlled by a starter function:

starter set_material()
{
while (1)
{
mat_user.emissive_red = e_red;
mat_user.emissive_green = e_green;
mat_user.emissive_blue = e_blue;
mat_user.ambient_red
= a_red;
mat_user.ambient_green = a_green;
mat_user.ambient_blue = a_blue;
mat_user.diffuse_red = d_red;
mat_user.diffuse_green
= d_green;
mat_user.diffuse_blue = d_blue;
mat_user.specular_red = s_red;
mat_user.specular_green = s_green;
mat_user.specular_blue
= s_blue;
mat_user.power = m_power;
mat_user.alpha = m_alpha;
mat_user.albedo = m_albedo;
wait (1);
}
}

The while loop above updates all the material parameters using the values of the variables that are set on the panel. The last function is the one that rotates the camera with 90 degrees every time we press the "R" key:

function
rotate_camera()
{
camera.pan -= 90;
}

on_r = rotate_camera;

That's all, folks! Now go and replace the "user defined" model with your own models, push some buttons and amaze the world with your materials!


Untitled

Minimap

3DGS allows us to have several views on the screen at the same time; I have used this great feature to create a top view of the screen.

Let's see the view definition first:

view
top_view
{
layer = 15;
pos_x = 0;
pos_y = 0;
size_x = 200;
size_y = 150;
tilt = -90;
arc = 80;
}

The view has 200 x 150 pixels and appears in the upper left corner of the screen. The view loks down because its tilt is set to -90 and its zooming factor was set to 80 (play with this value to see more / less from your level on the top view).

I have used a "starter" function (a function that runs by itself when we run the level) to show / hide the minimap:

starter
init_topcam()
{
while (player == null) {wait (1);}
while (1)
{
if (key_t == 1)
{
while (key_t == 1) {wait (1);}
if (top_view.visible == on)
{
top_view.visible = off;
}
else
{
top_view.visible = on;
}
}
top_view.x = player.x;
top_view.y = player.y;
top_view.z = 1000;
// play with this value
wait (1);
}
}

We wait until the player appears in the level and then we show or we hide the minimap when the "T" key is pressed. The view will have the same x and y with the player and it will be placed at a height of 1,000 quants. Your levels could have a different height so you should play with this value.

You should add a small frame to the minimap view (use 2 or 4 thin panel stripes).


Untitled

Vertical mirrors

If you own A5 pro, you are familiar with the horizontal mirror that is placed on the floor in the office level. The code for the vertical mirror is similar, but there are a few tips:
1) The mirror shouldn't be rotated at all in your level. If you want to place a mirror on the wall that faces south, the mirror should be horizontal.

2) The mirror code changes depending on mirror's orientation. Here's the mirror code used in my example:

function init_mirror()
{
camera.portal = mirror;
mirror.noshadow = on; // suppress shadows in the mirror
mirror.noparticle = on; // suppress particles
in the mirror
mirror.portalclip = on; //
clip at portal plane
while (1)
{
proc_late(); //
place it at the end of the function list
mirror.genius =
camera.genius;
mirror.aspect =
camera.aspect;
mirror.arc = -camera.arc;
mirror.fog = camera.fog;
mirror.x = camera.x;
mirror.y = camera.portal_y
- (abs(camera.portal_y - camera.y));
mirror.z = camera.z;
mirror.pan = -camera.pan;
mirror.tilt = camera.tilt;
mirror.roll = camera.roll;
wait(1);
}
}

If you want to place the mirror on your western wall, the code should be modified this way:

function
init_mirror()
{
camera.portal = mirror;
mirror.noshadow = on; // suppress shadows in the mirror
mirror.noparticle = on; // suppress particles
in the mirror
mirror.portalclip = on; //
clip at portal plane
while (1)
{
proc_late(); //
place it at the end of the function list
mirror.genius =
camera.genius;
mirror.aspect =
camera.aspect;
mirror.arc = -camera.arc;
mirror.fog = camera.fog;
mirror.x = camera.portal_x
- (abs(camera.portal_x - camera.x));
mirror.y = camera.y;
mirror.z = camera.z;
mirror.pan = -camera.pan;
mirror.tilt = camera.tilt;
mirror.roll = camera.roll;
wait(1);
}
}

3) When you create your mirror entity, set "flat" for the faces that aren't interesting and "mirror" for the face that will be used as the mirror.

4) The back of your mirror should have enough "space" because what you see in the mirror is "created" on the other side of the wall, through the hole, using an imaginary "camera" view.

5) It might be a good idea to place an invisible block in front of your mirror if the player can reach it; if the player penetrates the mirror a little you won't be able to see it. I did that in my example level.

6) Don't forget to set to "none" any surface in the level that surrounds the mirror.

I have included a folder with a vertical mirror demo; copy the \mirrors folder inside your Gamestudio folder, open mirrors.wmp (not mirror.wmp!), build it and run it.

Happy mirroring!


Untitled

Intelligent music

Try to imagine this scenario: you enter a dark hallway, where a few torches create more shadows than light. The music continues to be gentle, so you don't feel too scared. A few more steps and... bang! The door closes behind you and the music changes from gentle to tense, while a hord of monsters attacks you. The music continues to remain tense until you see the daylight again.

I know that we've got a lot of drama here, but the code that plays the music this way is quite simple - see for yourself!

action music_trigger
{
my.passable = on;
music_handle = snd_loop (gentle_wav, 50, 0);
while (vec_dist
(player.x, my.x) > 100) {wait (1);}
temp_volume = 50;
while (temp_volume > 5)
{
snd_tune (music_handle, temp_volume, 0, 0);
temp_volume -= 1 * time;
wait (1);
}

I have used a guard model for the entity that triggers the change; you should make this model invisible when you are happy with the result. I chose the snd_loop instruction to play the soundtrack because media_play creates a small gap. The first while loop waits until the player has come closer than 100 quants, and then uses snd_tune inside another while loop to decrease the volume of the soundtrack. We use a variable named temp_volume that decreases its value from 50 (the initial volume of the soundtrack) to 5, and this fades out the soundtrack nicely.

    snd_stop (music_handle);
music_handle = snd_loop (tense_wav, 50, 0);
while (vec_dist
(player.x, my.x) < 1000) {wait (1);}
temp_volume = 50;
while (temp_volume > 5)
{
snd_tune (music_handle, temp_volume, 0, 0);
temp_volume -= 1 * time;
wait (1);
}
snd_stop (music_handle);
wait (1);
music_handle = snd_loop (gentle_wav, 50, 0);
}

Gentle_wav has stopped for good now, so we can start tense_wav now. This track will play until the distance between the player and the guard model is bigger than 1000 quants (close to the end of the hallway in my demo). When the player gets away from the dangerous area, tense_wav is faded out nicely and the good old gentle_wav soundtrack is played again.


Untitled

Hook me up with the network

This article will help you to connect 2 PCs using the phone, a serial cable, a local area network or the internet; we will learn how to run the battleship multiplayer clone.

The bad news
If you want to create / test / play a multiplayer game you will need 2 computers; if you have access to a single computer you won't be able to create too many multiplayer related things.

The good news
If you own a single computer you can connect to a friend's computer over the phone or (if he lives close to you) using a network cable. We will take a look at these options too.

1) I have a single computer and I want to connect to my grandmother's computer using the phone line.
You and your granny must have PCs with modems. I will use battleship as an example:

Your PC is faster so it should run as server and client.

2) I have a single computer and I want to connect to my grandmother's computer (she lives in the same building) using a serial cable.
This is the cheapest "network" because you don't need modems or network cards.

3) I have two or more computers and I want to connect them using a local area network.
This is the fastest and the most reliable network setup. I'm not going to teach you how to create a network (I would need to write a book about it) but here are some pointers:
- You need a network card in every computer that connects to the lan (local area network);
- You can use coax cable or utp cable. Coaxial cable will connect as many computers as you need without requiring extra hardware. Utp cable can connect 2 computers or (if you buy a hub) more computers;
- I ran some tests a few years ago and I found out that the speed is a little bigger (+ 20%) for coaxial cable. Other people will tell you the opposite but at least I ran some tests;
- You need to have a network protocol installed. If you can read this magazine from your computer, tcp / ip is installed and it should be enough. You can add ipx if you want but tcp / ip works ok;
- Your firewalls (software or hardware) might try to stop the acknex server or clients from communicating over the network. Allow acknex to pass through those firewalls.

Here are the network properties for two of my computers:

Let's see how we can run battleship over a lan:

4) I have a computer and I want to play battleship over the internet.
You need a computer with a modem. There are two possibilities:
a) You have a server connected to the internet all the time. If this is the case, you have a fixed IP address and you should know it so you can send it to the client that connects to you through email, etc.
b) You don't have a permanent connection to the internet and you want to use your computer as a server over the net; in this case, the IP address changes every time you connect to the internet.

Acknex uses the tcp / ip protocol by default so the steps that are needed in order to run the game are exactly like in the picture above. The problem here is to get the dynamic IP address if we're in the b) situation because we need to send it to the client using the email every time we connect to the internet. The following info was tested on Win98 and Win2000 PCs but it should work ok on Win ME and WinXP.

To get your IP address in Windows 98, Press Start -> Run, type "winipcfg" (without the quotes) and press OK. You will see the following program window:

You can see that the IP address is 0.0.0.0; this happens because this PC isn't connected to the internet for the moment. Always start your internet connection first and only after a successful connection get and send the IP address! If you have a network card installed in your PC be careful: you need to get the IP address for your PPP Adapter, like in the picture.

If you need to get your IP address in Windows 2000, press Start -> Programs -> Accessories -> Command prompt. Type "ipconfig" (without quotes) and press Enter; you should see this window:

This time I have connected to the internet first so I have got a good IP address. I need to send this address to the client that will connect to my server using an email, a messenger client, etc. All we need to know is server's IP address because the clients connect to it; if you are a client you don't need to know or to send your IP address.

Another method that allows you to get your IP address works ok on any PC, regardless of the operating system. Go to http://www.whatismyipaddress.com/ and you will see your IP address like in the picture below:

Ok, the last method can be used if you are a programmer: LocalIP returns the IP address of the local machine using the winsock activex control.

IMPORTANT INFO: The server has to run first. If the client runs before the server, it will fail to connect and all you can do is to exit and restart after the server has started to run.


Untitled

Night vision

Imagine yourself walking inside a dark level - but you've got the night vision binoculars! Let's see how it is done!

We create panels for the common resolutions:

panel
nvision640_pan
{
bmap = nvision640_map;
layer = 20;
pos_x = 0;
pos_y = 0;
flags = overlay, refresh,
d3d;
}

panel
nvision800_pan
{
bmap = nvision800_map;
layer = 20;
pos_x = 0;
pos_y = 0;
flags = overlay, refresh,
d3d;
}

panel
nvision1024_pan
{
bmap = nvision1024_map;
layer = 20;
pos_x = 0;
pos_y = 0;
flags = overlay, refresh,
d3d;
}

Then we create a sprite entity:

entity
fog_sprite
{
type = <fog+5.pcx>;
layer = 15;
x = 200;
y = 0;
z = 0;
flags = flare, bright;
}

The
key function is called from main at game start:

function init_nvision()
{
while (1)
{
if (key_n == 1)
// press and hold "N" for night vision
{
if (video_mode == 6) // 640x480
{
nvision640_pan.visible = on;
nvision800_pan.visible = off;
nvision1024_pan.visible = off;
}
if (video_mode == 7) // 800x600
{
nvision640_pan.visible = off;
nvision800_pan.visible = on;
nvision1024_pan.visible = off;
}

if (video_mode == 8) // 1024x768
{
nvision640_pan.visible = off;
nvision800_pan.visible = off;
nvision1024_pan.visible = on;
}
camera.ambient = 100;
fog_sprite.visible = on;
if (random(1) > 0.97) // every few seconds
{
fog_sprite.frame = random(5); // pick a random "fog" frame number
}
}
else
{
nvision640_pan.visible = off;
nvision800_pan.visible = off;
nvision1024_pan.visible = off;
fog_sprite.visible = off;
camera.ambient = 0;
}
wait (1);
}
}

If we press the "N" key, we make the proper panel visible and the other 2 panels invisible, depending on the screen resolution. The "night vision" effect is obtained by setting camera.ambient to 100.

Do you remember the fog_sprite entity that was defined a few lines above? Its role is to give the greenish appearance of the lenses. It is a sprite with several frames that is placed in front of the player; from time to time its frame changes in order to simulate noise, etc. You can add green fog to the level to make the things look "real".

If we release the "N" key, all the panels are made invisible, the sprite disappears and camera.ambient is set to zero again.


Untitled

Painting skins and sprites

A6 has many interesting features, one of them being the ability to draw anything on any bitmap using C-Script instructions. I chose to paint a red cross over guard's skin for this article:

bmap* heros_skin; // bitmap pointer, will be used for the skin

action
my_hero
{
heros_skin = bmap_for_entity(my, 0); // get a pointer
to guard.mdl's skin
temp.red = 255; // paint the skin using the
red color
temp.green = 0;
temp.blue = 0;
format = bmap_lock (heros_skin, 0); // lock the skin bitmap
pixel = pixel_for_vec(temp, 100, format);
draw_cross(31, 108);
bmap_unlock (heros_skin); // unlock the bitmap
// put
the rest of the code for your hero here
}

This is beginner's corner, so the code can't be too complicated: we set the previously defined bitmap pointer named heros_skin to guard.mdl's skin (that's what bmap_for_entity does) and then we set temp.red to 255. This means that we will do all our pixel drawing using a red color. We lock the skin using bmap_lock, and then we create a pixel that has a red color, a transparency factor of 100 (completely opaque) and the right pixel format for our painting. The last two lines call the function that draws the red cross and unlock the skin.

Here's the code for the scary draw_cross function:

function draw_cross(coords_x, coords_y)
{
pixel_to_bmap(heros_skin, coords_x + 3, coords_y + 0, pixel);
pixel_to_bmap(heros_skin, coords_x + 4, coords_y + 0, pixel);
pixel_to_bmap(heros_skin, coords_x + 5, coords_y + 0, pixel);
pixel_to_bmap(heros_skin, coords_x + 6, coords_y + 0, pixel);
pixel_to_bmap(heros_skin, coords_x + 3, coords_y + 1, pixel);
pixel_to_bmap(heros_skin, coords_x + 6, coords_y + 1, pixel);
pixel_to_bmap(heros_skin, coords_x + 3, coords_y + 1, pixel);
pixel_to_bmap(heros_skin, coords_x + 6, coords_y + 1, pixel);
pixel_to_bmap(heros_skin, coords_x + 0, coords_y + 2, pixel);
pixel_to_bmap(heros_skin, coords_x + 1, coords_y + 2, pixel);
pixel_to_bmap(heros_skin, coords_x + 2, coords_y + 2, pixel);
pixel_to_bmap(heros_skin, coords_x + 3, coords_y + 2, pixel);
pixel_to_bmap(heros_skin, coords_x + 6, coords_y + 2, pixel);
pixel_to_bmap(heros_skin, coords_x + 7, coords_y + 2, pixel);
pixel_to_bmap(heros_skin, coords_x + 8, coords_y + 2, pixel);
pixel_to_bmap(heros_skin, coords_x + 9, coords_y + 2, pixel);
pixel_to_bmap(heros_skin, coords_x + 0, coords_y + 3, pixel);
pixel_to_bmap(heros_skin, coords_x + 9, coords_y + 3, pixel);
pixel_to_bmap(heros_skin, coords_x + 0, coords_y + 4, pixel);
pixel_to_bmap(heros_skin, coords_x + 9, coords_y + 4, pixel);
pixel_to_bmap(heros_skin, coords_x + 0, coords_y + 5, pixel);
pixel_to_bmap(heros_skin, coords_x + 1, coords_y + 5, pixel);
pixel_to_bmap(heros_skin, coords_x + 2, coords_y + 5, pixel);
pixel_to_bmap(heros_skin, coords_x + 3, coords_y + 5, pixel);
pixel_to_bmap(heros_skin, coords_x + 6, coords_y + 5, pixel);
pixel_to_bmap(heros_skin, coords_x + 7, coords_y + 5, pixel);
pixel_to_bmap(heros_skin, coords_x + 8, coords_y + 5, pixel);
pixel_to_bmap(heros_skin, coords_x + 9, coords_y + 5, pixel);
pixel_to_bmap(heros_skin, coords_x + 3, coords_y + 6, pixel);
pixel_to_bmap(heros_skin, coords_x + 6, coords_y + 6, pixel);
pixel_to_bmap(heros_skin, coords_x + 3, coords_y + 6, pixel);
pixel_to_bmap(heros_skin, coords_x + 6, coords_y + 6, pixel);
pixel_to_bmap(heros_skin, coords_x + 3, coords_y + 7, pixel);
pixel_to_bmap(heros_skin, coords_x + 4, coords_y + 7, pixel);
pixel_to_bmap(heros_skin, coords_x + 5, coords_y + 7, pixel);
pixel_to_bmap(heros_skin, coords_x + 6, coords_y + 7, pixel);
}

Yes, I know it's a long function, but did you notice that all the lines are almost identical? Let's examine the first one in detail:

pixel_to_bmap(heros_skin, coords_x + 3, coords_y + 0, pixel);

This instruction uses the patented :) pixel_to_bmap instruction to draw over heros_skin, at the coordinates given by (coords_x + 3, coords_y + 0), using that red, 100% opaque pixel value. So our call to function draw_cross:

draw_cross(31, 108);

draws a pixel for every line inside the function, with the first line drawing a red pixel to x = 34, y = 108 (31 + 3, 108 + 0). How did I get those values for the draw_cross call? Take a look at the skin:

The rest is simple: you decide what you want to display (a star, a symbol, etc) and you draw it on something, even on paper. Here's how my cross looked like on paper (those grey pixel borders were drawn in order to make the things easier for you):

Use a pixel_to_bmap instruction for every pixel, and don't forget that you can paint on sprites too! This means that you can create a digital clock entity, weird signs and many more things using this piece of code.


Untitled

NPC Talk

This piece of code will make a talkative guy out of any model! All you have to do is to approach the model and it will start saying "lines" one by one. The lines are stored in strings; there are 5 strings in my example (npc1_str... npc5_str) but you can add many more, of course. The strings are displayed using a text:

text npc_txt
{
layer
= 22;
pos_x = 0;
pos_y = 0;
font
= standard_font;
strings = 5;
string = null;
flags
= d3d;
}

Action npc1 will be attached to the model (guard.mdl):

action
npc1
{
clip_size
= 0;
while (1)
{
temp.x = player.x - my.x;
temp.y
= player.y - my.y;
temp.z
= 0;
vec_to_angle(my.pan, temp);

Clip_size = 0 will show all the triangles for all the models, but decreases the speed a little. The other lines are used to rotate the npc towards the player all the time (you wouldn't like to talk to the npc and see it looking in another direction)

        ent_cycle("stand",my.skill9);
my.skill9 += 1.5 * time;
if (my.skill9
>= 100) {my.skill9 = 0;} // play "stand" animation

The lines above are used to animate the guard using its stand animation; I've explained how this works several times.

        my.skill10 = vec_dist(my.x,
player.x);
if (my.skill10
< 200)
{
if (my.skill11 == 0)
{
npc_talks();
my.skill11 = 1; // the npc says a single "line" (fake exclusive_global)
}
}
else
{
my.skill11 = 0;
}
wait (1);
}
}

When the player comes closer than 200 quants to the player, the npc will say a single line (npc_talks is executed only once) because my.skill11 will be set to 1 immediately after the approach, so my.skill11 = 0 isn't true anymore, like in the picture:

But why did we choose to make the code more complicate? Wouldn't it have been better to use something like this?

if
(my.skill10 < 200)
{
npc_talks()
}

The answer is no. When the player comes closer than 200 quants, npc_talks() would be executed all the time and the npc would "say" all its lines one by one in a few moments. What we want is to make the npc say a line, then wait for us to leave, then say a new line and so on.

Function npc_talks() is simple:

function
npc_talks()
{
if (my.skill12 < 5) {my.skill12 += 1;}

We're using 5 strings for this npc; if the player approaches the npc more than 5 times, the npc will repeat its last "line".

      if (my.skill12 == 1) {npc_txt.string =
npc1_str;}
if (my.skill12 == 2)
{npc_txt.string = npc2_str;}
if (my.skill12 == 3) {npc_txt.string =
npc3_str;}
if (my.skill12 == 4)
{npc_txt.string = npc4_str;}
if (my.skill12 == 5) {npc_txt.string =
npc5_str;}

npc_txt.visible = on;
waitt (48);
npc_txt.visible = off;

}

Npc_text is showed (visible = on) for 3 seconds.


           Panels, texts and buttons

The panels, texts and buttons are used by the 2D engine inside A4 / A5 to add some flavor to 3D games; don't underestimate them - you can create full 2D games or fake 3D games with the 2D engine. I have created a dumb, educative game that makes heavy use of panels, texts and buttons and guess what? I named it Guessme...

The main function is really simple:

function
main()
{
D3D_PANELS
= ON;
LOAD_LEVEL (<guess.wmb>);
start_game();
}

I have set D3D_PANELS to ON to enable hardware accelerated panels. I'm also loading a simple level (not necessary, but useful if you want to add more stuff or if your main panel doesn't cover the whole screen.

I have created a panel that covers the whole screen (800x600 pixels) and has its VISIBLE flag set, so it will appear at game start. I have two buttons used to start the game and to quit. If you remember the button definition in the manual, when I move the mouse over one of the buttons, it will change its bitmap to starthi_map or quithi_map. When I click one of the buttons, its corresponding action (start_guessing or quit_game) is executed.

PANEL
guessmain_pan
{
BMAP guess800_map;
LAYER 10;
POS_X 0;
POS_Y 0;
BUTTON 20,
20, start_map, start_map, starthi_map, start_guessing, NULL, mouse_over;
BUTTON 20,
70, quit_map, quit_map, quithi_map, quit_game, NULL, mouse_over;
FLAGS OVERLAY,
REFRESH, VISIBLE;
}

Function quit_game is really simple - it shuts down the engine; function start_guessing is more complex. Before we move on, please note that texts are pretty similar to panels; they have layers and can be moved on the screen.

function start_guessing()
// simplified version
{
intro_text.VISIBLE
= ON; // display the text stored in intro_string ("Try to guess a number...")
WAITT (16); // wait a second, then start
scrolling the text upwards
WHILE (intro_text.POS_Y > -30) // make
sure that all the three rows have disappeared
{
intro_text.POS_Y
-= TIME * 0.05 * (570 - intro_text.POS_Y); // we modify intro_text's POS_Y to
scroll faster and faster
WAIT (1);
}
intro_text.VISIBLE
= OFF; // remove the intro text
random_number = int(RANDOM(9)+1); // pick
a random number between 1 and 9
click_text.VISIBLE = ON; // display the
text stored in click_string ("Click the keypad...")
keypad_pan.VISIBLE = ON; // display the
keypad panel
WHILE (success == 0) // if I guess the
number, success = 1
{
IF (random_number
== your_guess) // got it
{
smaller_pan.VISIBLE = OFF; // remove the "arrow" panels
bigger_pan.VISIBLE = OFF;
PLAY_SOUND thanks_snd, 100; // plays the thank you sound
success = 1; // get out of the while loop
}
ELSE
{
IF (random_number < your_guess) // I chose a bigger number
{
bigger_pan.VISIBLE = OFF;
smaller_pan.VISIBLE = ON; // so make the smaller arrow panel visible
}
ELSE
{
smaller_pan.VISIBLE = OFF; // I chose a smaller number
bigger_pan.VISIBLE = ON; // so make the bigger arrow panel visible
}
}
WAIT (1);
}
click_text.VISIBLE = OFF;
// remove the "Click the keypad..." text
keypad_pan.VISIBLE = OFF; // and the rest of
the panels excepting guessmain
smaller_pan.VISIBLE = OFF;
bigger_pan.VISIBLE = OFF;
}

Finally, there's start_game()

function
start_game() // simplified version
{
MOUSE_MAP = cursor_map;
// we choose the mouse pointer, place it in the center of the screen and make
the cursor freely movable
MOUSE_POS.X = SCREEN_SIZE.X / 2;
MOUSE_POS.Y =
SCREEN_SIZE.Y / 2;
MOUSE_MODE = 2;
WHILE (1)
{
MOUSE_POS.X
= POINTER.X; // move the mouse on x and y
MOUSE_POS.Y
= POINTER.Y;
IF
(MOUSE_LEFT == 1) // if the left mouse button (lmb) is pressed, we check to see
if it was over the keypad_pan.
{
IF (MOUSE_POS.X > 650 && MOUSE_POS.X < 700)
{
IF (MOUSE_POS.Y > 0 && MOUSE_POS.Y < 50)
{
your_guess = 1;
}
IF (MOUSE_POS.Y > 50 && MOUSE_POS.Y < 100)
{
your_guess = 4;
}
IF (MOUSE_POS.Y > 100 && MOUSE_POS.Y < 150)
{
your_guess = 7;
}
}
IF (MOUSE_POS.X > 700 && MOUSE_POS.X < 750)
{
IF (MOUSE_POS.Y > 0 && MOUSE_POS.Y < 50)
{
your_guess = 2;
}
IF (MOUSE_POS.Y > 50 && MOUSE_POS.Y < 100)
{
your_guess = 5;
}
IF (MOUSE_POS.Y > 100 && MOUSE_POS.Y < 150)
{
your_guess = 8;
}
}
IF (MOUSE_POS.X > 750 && MOUSE_POS.X < 800)
{
IF (MOUSE_POS.Y > 0 && MOUSE_POS.Y < 50)
{
your_guess = 3;
}
IF (MOUSE_POS.Y > 50 && MOUSE_POS.Y < 100)
{
your_guess = 6;
}
IF (MOUSE_POS.Y > 100 && MOUSE_POS.Y < 150)
{
your_guess = 9;
}
}
}
WAIT (1);
}
}

You might wonder why did I use this type of checking for keypad and not regular buttons. First of all, this is a tutorial and you must learn as many tricks as possible. More that that, you can create a "button" of virtually any shape and size and use this type of code to click on it.

You can see that I have placed the keypad panel in the upper right corner.

IF (MOUSE_LEFT == 1) // if the left mouse button (lmb) is pressed, we check to see if it was over the keypad_pan.
{
IF (MOUSE_POS.X > 650 && MOUSE_POS.X < 700) // first column (1, 4, 7) starts at 650 and ends at 700 pixels on X
{
IF (MOUSE_POS.Y > 0 && MOUSE_POS.Y < 50) // first row (1, 2, 3) starts at 0 and ends at 50 pixels on Y
{
your_guess = 1; // you have clicked on 1
}
IF (MOUSE_POS.Y > 50 && MOUSE_POS.Y < 100) // second row (4, 5, 6) starts at 50 and ends at 100 on Y
{
your_guess = 4;
}
IF (MOUSE_POS.Y > 100 && MOUSE_POS.Y < 150) // third row (7, 8, 9) starts at 100 and ends on 150 on Y
{
your_guess = 7;
}
}
..............................................

We don't need a keypad panel for this type of action; keypad_pad is placed over the screen to show the player where to click.


Untitled

Password request code

This is the code for your npc (non-playing character). You come close to it, it asks you "What's the password?", you type the password in and if you know it, you can open certain doors or trigger any other actions or functions.

I'm using three strings: the first one displays the password request string, the second will store my keystrokes (ten characters, but you can increase the number) and the final string holds the predefined password.

string
passreq_str = "What's the password?";
string pass_str[10];
string password_str = "geronimo";

I'm using a classical test definition to display the strings on the screen and a simple request_pass action that will be attached to my npc.

text
pass_txt
{
pos_x = 20;
pos_y = 20;
layer = 15;
font standard_font;
}

action
request_pass
{
my.enable_impact = on;
my.enable_push = on;
my.event = ask_pass;
}

The interesting stuff happens in function ask_pass:

function
ask_pass ()
{
if (you == player)
{
if (pass_txt.visible
== on) {return;}
pass_txt.string
= passreq_str;
pass_txt.visible
= on;
waitt (32);
pass_txt.string
= pass_str;
while (pass_txt.visible
== on)
{
inkey pass_str;
if(result == 13)
{
pass_txt.visible = off;
}
}
if (str_cmpi (pass_str,
password_str) == 1)
{
key1 = 1;
new_event();
}
}
}

This function will run only if the entity that collided with the npc is the player itself. If you have a decent computer, multiple events can run at the same time and you don't want that. We can use exclusive_global or my method:

if (pass_txt.visible == on) {return;}

If pass_text is visible already, the function stops so only one instance will run (the first one that makes the text visible). We display the passreq_str string for two seconds and then we wait for player input; this will happen as long as the player doesn't press Enter (result is 13 if Enter is pressed). When we have finished typing the passowrd and we press Enter, the text is made invisible and a simple str_cmpi instruction is performed. If pass_str == password_str (user input = predefined password) and we are using the templates, key1 = 1 means that we can open doors that can be unlocked if we have key1, key2 = 1 means that we can open doors that are using key2 and so on. We're also calling the new_event function that can be used to start other functions, etc.


Untitled

U sing several physics objects

If you have bought A6 commercial you might find that "1 physics object only" limit pretty annoying. Fortunately, there are legal ways to overcome this obstacle, and this article will teach you how to do that. I promise to entertain the A6 pro owners too, because we will also learn to create shootable light sources.

I wanted you to see these lights in action even if you don't own A6 commercial or professional, so I have created a small movie; make sure that you have Media Player 9 installed on your PC and then click here to see it.

The article includes 2 projects, so load physics1.wmp, build it and run it using physics1.wdl. You will see a dark hallway; move the mouse a little until you see the first lamp:

Make sure that the crosshair is placed over the lamp, and then fire an orange ball by pressing the left mouse button; you will see that the lamp starts to move, creating nice lights and shadows on the floor and on the walls:

There are 3 lamps like this in the first demo level, and each one of them controls a radius of 700 quants, acting like a "physics" object when the player approaches it. I invite you to explore the level; I'm going to show you Wed's top view of the first demo.

Let's take a good look at the code now. Function main simply loads the physics1_wmb level:

function
main()
{
level_load (physics1_wmb);
}

The crosshair is a simple 6 x 6 pixels bitmap that was defined as a panel and placed in the center of the screen:

panel
crosshair_pan
{
bmap = pointer_pcx;
pos_x
= 397;
pos_y = 297;
flags = visible, overlay;
}

Time to take a look at the action used for the player:

action
player1
{
player = my;
my.invisible = on;
while (1)
{
vec_set (camera.pos, my.pos);
camera.pan =
my.pan;
camera.tilt += 10 * mouse_force.y
* time;
my.pan -= 10 * mouse_force.x * time;
temp.x = 10 * (key_w - key_s) * time;
temp.y = 5 * (key_a - key_d) * time;
temp.z
= 0;
ent_move (temp, nullvector);
wait (1);
}
}

The player model is made invisible; the camera inherits the x y z position and the pan angle from the player. We can change the tilt angle of the camera by moving the mouse vertically, and the pan angle of the camera by moving the mouse horizontally. The player can move forward / backward by pressing the "W" and "S" keys and strafes with the "A" and "D" keys. These values are stored in temp every frame and move the player using the ent_move instruction. You can see that temp.z is set to zero, so the player isn't using gravity in this project.

What happens when the player presses the left mouse button? Take a look!

on_mouse_left = fire_bullet;

function fire_bullet()
{
ent_create (bullet_mdl, camera.x, move_bullet);
}

Every time we press the left mouse button, function fire_bullet() is run. This function creates a bullet_mdl entity at the position given by the camera and makes it run the function named move_bullet():

function
move_bullet()
{
my.unlit = on;
my.enable_block = on;
my.enable_entity = on;
my.enable_impact = on;
my.pan = camera.pan;
my.tilt = camera.tilt;
my.event = remove_bullet;

This bullet entity isn't affected by the lights in the level, because we want to see it even if it is moving in the darkest corners of the level. The bullet is sensitive to level blocks and entities; it inherits the pan and tilt angles of the camera and runs its remove_bullet() function every time it collides with a level block or with another entity.

while(1)
{
if (vec_dist (my.x,
camera.x) < 50)
{
my.passable = on;
}
else
{
my.passable = off;
}
temp.x = 30 * time;
temp.y = 0;
temp.z = 0;
move_mode = ignore_you + ignore_passable + glide;
ent_move (temp, nullvector);
wait(1);
}
}

The bullet mustn't collide with the player entity, so it will be made passable until the distance between it and the camera grows above 50 quants. The bullet moves with the speed set with temp.x, ignoring passable objects. Let's see what happens when the bullet hits a wall or one of the lamps:

function remove_bullet()
{
wait (1);
ent_remove(me);
}

This simple function waits for a frame and then removes the bullet. Let's see the most interesting piece of code - it's the one attached to every lamp model:

action physics_lamp
{
while (player == null) {wait
(1);}
my.unlit = on;
my.ambient = 100;
my.lightrange = 300;
my.spotlight = on;
vec_set (d3d_spotlightcone, vector (75, 30, 4));

The lamp waits until the player is created, then turns its "unlit" flag on and sets its ambient to 100. We did this because we need to see our lamps glowing in the dark; otherwise our bullets might miss them most of the time! The lamps will generate dynamic light on a range of 300 quants and have their "spotlight" flag turned on. We set d3d_spotlightcone to a convenient value given by our vector (75, 30, 4); you can play with these values from vector (1, 1, -5) to vector (90, 90, 5).

    ph_setgravity (vector (0, 0, -386));
phent_settype
(my, ph_rigid, ph_box);
phent_setmass (my, 10, ph_box);
phent_setdamping (my, 20, 50);
my.skill20 = phcon_add (ph_ball,
my, 0);
phcon_setparams1 (my.skill20, vector (my.x, my.y,
(my.z + 35)), nullvector, nullvector); // play with "35"

The lines above turn our regular lamp into a physics entity. First of all we set the gravity to 9.81 m/s2 if we agree to use 1 quant = 1 inch, and then we tell the engine that our lamp is to be treated as a physics object, with its collision hull being similar to a box. The lamp will have a mass of 10 and its rotational behavior will be similar to a box. Finally, we set the linear damping for our lamp to be 20 and the angular damping to 50.

We need to add a constraint to our lamp entity, because we want it to be able to swing just like in the picture below:

    my.skill20 = phcon_add (ph_ball, my, 0);
phcon_setparams1 (my.skill20, vector (my.x, my.y, (my.z + 35)), nullvector, nullvector);
// play with "35"

I have copied the two lines of code again so that you can take a better look at them. The first line sets a ball-like constraint on the lamp entity and stores the id for the constraint in skill20 for further manipulation; the second line of code sets the parameters of the constraint whose id was stored in skill20. We use a vector that gets the x and y values from our lamp and adds 35 quants on the z axis; these will be the coordinates of the swinging point for the lamp. The rest of the parameters are set to nullvector because (according to the manual) ph_ball needs to have only its anchor point set.

That was all the physics-related stuff! For those of you who continue to feel a bit foggy, here's a more friendly explanation: we tell the lamp that it is going to move as if it would rotate around a sphere, and we set the center of the sphere with vector (my.x, my.y, (my.z + 35). This means that the center of the rotation will have the same x and y positions with the lamp, being placed 35 quants above the lamp (the red point in the picture above).

    while (1)
{
if (vec_dist (player.x, my.x) < 700) // 700 = experimental value
{
phent_enable
(my, 1);
}
else
{
phent_enable (my, 0);
}
wait (1);
}
}

The while loop above enables or disables the physics entity depending on the distance between the player and the lamp. Feel free to remove the while loop if you own A6 pro because that loop won't help you at all; the rest of the article is useful especially for the A6 commercial owners.

All the lamps checks the distance between them and the player every frame; if the player comes closer than 700 quants to a lamp, that lamp starts to act like a "physics" object, while the other lamps freeze. This wouldn't look too good if the player could see all the lamps at the same time, but that's why I have created the level this way. You should play with "700"; I chose a smaller value because I wanted you to observe the behaviour of the lamps; I feel that "1000" is a decent value for this demo level.

The method above allows you to use several physics objects throughout the level; use a lamp here, maybe a barrel or a vehicle there (and so on) but make sure that they aren't visible / used at the same time. Run distance checks for all the physics objects and set phent_enable to 1 or 0 depending on the distance from them to the player.

Ok, I know I've told you that you can use several "physics" entities at the same time even with A6 commercial, and the trick above didn't quite do it, because those lamps can't be used at the same time. So why don't you load the physics2.wmp level, build it and run physics2.wdl? You should see a room with 2 lamps:

These 2 lamps are true physics objects and can be used at the same time, even with A6 commercial! Now how can this be?

Most of the code inside physics2.wdl is similar with the one inside physics1.wdl, so we will only discuss the changes here. First of all, we have a starter function that looks like this:

starter
multiple_objects()
{
while (1)
{
ent_number = 1;
wait
(2);
ent_number = 2;
wait (2);
}
}

That's a simple function! It sets a variable named ent_number to 1 for two frames, then sets ent_number to 2 for two frames and so on. This variable is used inside the action attached to the lamps:

action
physics_lamp
{
my.unlit = on;
my.ambient
= 100;
my.lightrange = 300;
my.spotlight = on;
vec_set (d3d_spotlightcone, vector (75, 30, 4));

   ph_setgravity (vector (0, 0, -386));
phent_settype (my, ph_rigid, ph_box);
phent_setmass (my, 10, ph_box);
phent_setdamping (my, 20, 50);
my.skill20 = phcon_add (ph_ball,
my, 0);
phcon_setparams1 (my.skill20, vector (my.x, my.y, (my.z
+ 35)), nullvector, nullvector);

All the code above is identical with the one used in physics1.wdl, so we won't discuss it again.

  while (1)
{
if ((my.skill1 == 1) && (ent_number == 1))
{
phent_enable
(my, 1);
}
if ((my.skill1 == 2) && (ent_number == 2))
{
phent_enable
(my, 1);
}
wait (1);
phent_enable (my, 0);
}
}

This time the while loop makes the first lamp an active physics object for 2 frames (that's how much time ent_number is set to 1) and then it allows the second lamp to become the active physics object for 2 frames, because every lamp has its skill1 set to a different value (1, 2, ...) in Wed. If we need to have 3 physics objects running at the same time, we could use something like this:

starter multiple_objects()
{
while (1)
{
ent_number = 1;
wait (2);
ent_number = 2;
wait (2);
ent_number = 3;
wait (2);
}
}

action physics_lamp
{

   ....................................................
while (1)
{
if ((my.skill1 == 1) && (ent_number == 1))
{
phent_enable
(my, 1);
}
if ((my.skill1 == 2) && (ent_number == 2))
{
phent_enable
(my, 1);
}
if ((my.skill1 == 3) && (ent_number == 3))
{
phent_enable
(my, 1);
}
wait (1);
phent_enable (my, 0);
}
}

That's how it works! We continue to have a single physics object (that's the limit set by A6 commercial) but it is shared by several entities really fast, as if we would run several physics objects at the same time!

How many different objects can we have at the same time? Well, this depends on the nature of your project; if the game runs at 80 fps and we have 4 lamps that are visible at the same time, each one of them will move with 80 fps / 4 = 20 fps, which is ok. If you plan to create a pool game, you could create all the balls for it as physics objects if they run at 10 fps.

My tests show that you can use at least 3-4 physics objects on a decent PC, which I consider to be a big step ahead when compared to the 1 object limit set by A6 commercial. Don't forget that you can combine the techniques that were used in these 2 projects to get even more physics objects in your level.


Untitled

Pianissimo

Pianissimo is a small stereo piano that can be played using the mouse; it uses real sound samples taken from my Yamaha synthesizer. Use it to tune your musical instruments (guitars, etc) or to impress your neighbors.

This project is standalone - let's see its main function:

function
main()
{
level_load (dummy_wmb);
wait (2);
mouse_map = mouse_pcx;
mouse_mode = 2;
while (1)
{
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
wait (1);
}
}

We are loading a dummy level, we set the mouse pointer bitmap, we make the cursor visible and we allow the mouse movement in a while loop.

The piano is in fact a panel with several buttons:

panel
main_pan // main panel
{
bmap = main_pcx;
pos_x = 190; // close to
the center of the screen
pos_y = 120; // on x and y
layer = 10;
button = 24, 54, keypressed_pcx,
key_pcx, key_pcx, key_pressed, null, null;
button = 45, 54, keypressed_pcx,
key_pcx, key_pcx, key_pressed, null, null;
button = 67, 54, keypressed_pcx,
key_pcx, key_pcx, key_pressed, null, null;
button = 89, 54, keypressed_pcx,
key_pcx, key_pcx, key_pressed, null, null;
button = 110, 54, keypressed_pcx,
key_pcx, key_pcx, key_pressed, null, null;
button = 132, 54, keypressed_pcx,
key_pcx, key_pcx, key_pressed, null, null;
button = 154, 54, keypressed_pcx,
key_pcx, key_pcx, key_pressed, null, null;
button = 175, 54, keypressed_pcx,
key_pcx, key_pcx, key_pressed, null, null;
button = 197, 54, keypressed_pcx,
key_pcx, key_pcx, key_pressed, null, null;
button = 219, 54, keypressed_pcx,
key_pcx, key_pcx, key_pressed, null, null;
flags = refresh, d3d, visible;
}

The panel is placed close to the center of the screen and has 10 buttons - the 10 keys on the keyboard. When you click one of the keys with the mouse, function key_pressed will run:

function
key_pressed(key_number)
{
if (key_number == 1)
{
snd_play(do_wav,
70, -100);
}
if (key_number == 2)
{
snd_play(re_wav,
70, -70);
}
...........................................
if (key_number == 10)
{
snd_play(mi2_wav,
70, 100);
}
}

When we press one of the buttons on the panel, key_number is set to a certain value: 1 for the first button, 2 for the second button and so on. This way we can use a single function for all the buttons (all the keys on the keyboard). If we press one of the buttons a note is played using a simple snd_play instruction. The stereo effect is obtained by setting different balance values for the keys, starting with -100 for the first key and going up to 100 for the last key.


Untitled

Random tunes

Many games use a few soundtracks, playing them in a random order in every level. This snippet will show you how to do the same thing for your game.

starter
random_tunes()
{
var current_track = 0;
randomize();
while (1)
{
current_track
= int(random(5)) + 1;
if (current_track ==
1)
{
media_play("track1.mid", null, 100);
}
if (current_track == 2)
{
media_play("track2.mid",
null, 100);
}
if (current_track == 3)
{
media_play("track3.mid", null, 100);
}
if (current_track == 4)
{
media_play("track4.mid",
null, 100);
}
if (current_track == 5)
{
media_play("track5.mid", null, 100);
}
while (snd_playing(media_handle) != 0) {wait (1);}
sleep (1);
}
}

We've got a starter function here, so it will start running as soon as the level is loaded. We define a local variable named current_track, and then we make sure that we will generate a different random number series every time we run the script by calling the built-in "randomize" function. The first line of code inside the while(1) loop generates a random number that ranges from 1 to 5; we will play one of the 5 sound tracks depending on the "current_track" value. The last few lines of code wait until the last song stops and create a 1 second pause between the tracks. Feel free to use any other value here, including wait (1) if you don't want to put a small pause between any 2 consecutive soundtracks.


Untitled

Feature of the month: remote cameras

This month I want to show you a "pro" feature that is extremely useful in action games, sport games, adventure games, strategy games... in fact, I think that any game could use a remote camera.
Acknex has improved a lot lately and this is one of the hot new additions to the engine: the ability to render a view on a sprite.

Practical use: display your football game on a big screen, keep an eye on your fortress' entrance, create rear view mirrors for your car and so much more!

First of all, we define a view:

view
remote
{
pos_x = 0;
pos_y = 0;
size_x = 128; // size of
the view on x and y
size_y = 64;
layer = 5;
flags = visible;
}

We must set its size on x and y to the size of the sprite (128 x 64 pixels in my example). I chose layer = 5 but you can choose any small value. This will be our virtual "screen" but we also need a camera to watch what's happening:

action camera1
{
my.invisible = on;
my.passable = on;
remote.x = my.x;
remote.y = my.y;
remote.x = my.x;
remote.pan = my.pan;
remote.tilt = my.tilt;
remote.roll = my.roll;
}

You can use any model for the camera: place it in Wed, set its position and angles and they will be transmitted to the remote view. This should be all... oh, wait, we need to show the image captured by the "screen" using a sprite:

action monitor
{
my.oriented = on; // oriented sprite
remote.bmap = bmap_for_entity (my, 0);
}

I am using an oriented sprite; the view will be rendered on "my" bitmap = the bitmap used for the sprite with this action attached to it.


Untitled

Scripted cut scenes

The world is changing! The good old 256-colors movies that were used for the cut scenes a few years ago are replaced with scripted cut scenes that are created using the game engine itself! And this article will discuss the most used techniques.

Let's begin with the scenario:

1) A few soldiers are discussing close to the 2 trucks that they are supposed to protect;

2) A chopper lands silently nearby without being seen. Two masked men get out of the chopper and run towards the trucks;

3) A van passes by, creating noise and attracting the soldiers' attention;

4) One of the masked men sneaks behind one of the trucks, plants the explosive and then runs away;

5) The trucks explode and all the soldiers are killed.

All these things were created using simple actions and functions:

function
main()
{
fps_max = 40;
level_load (cutscene_wmb);
wait (3);
camera.x = -800;
camera.y =
-1100;
camera.z = 50;
camera.pan = 115;
camera.tilt = -10;
}

It is highly recommended to lock the frame rate during the cut scene; you'll see why in a minute. We load the level, we wait until it is loaded and then we set the initial camera position:

This is the model that has action soldier_1 attached to it:

action
soldier_1
{
soldier1 = my;
while (vec_dist
(my.x, soldier2.x) > 150)
{
ent_cycle("stand", my.skill20);
my.skill20
+= 0.25;
if (my.skill20 == 19)
{
snd_play (radio_wav,
20, 0);
}
wait (1);
}
my.skill20 = 0;
snd_play (allquiet_wav, 50, 0);
while (my.skill20 < 95)
{
ent_cycle("salut", my.skill20);
my.skill20 += 5 * time;
wait (1);
}
snd_play (isee_wav, 60, -30); // bigger volume for
the left channel

The first soldier will wait until the distance between him and the higher ranked soldier2 is smaller than 150 quants, playing its "stand" animation in a loop. Please note that I'm not using a time-corrected value for its animation, but a fixed value (0.25) because I wanted to play the radio_wav sound at a specific animation frame. We don't need to use time-corrected values because the frame rate is locked.

The officer has approached now (see the first picture) so the first soldier says its allquiet_wav line, and then it salutes the officer. The same soldier plays officer's isee_wav line, with a bigger volume for the left channel because of the -30 balance factor. The point I'm trying to make is this: it doesn't matter who does the job, as long as it looks real.

    sleep
(2);
media_play("soundtrack.wav", null, 20); // start the soundtrack
chopper_moves = 1;
camera.x = 300;
camera.y
= -2700;
camera.z = 50;
while (start_van ==
0) {wait (1);}
while (van1.x < 1000)
{
vec_set(temp, van1.x);
vec_sub(temp, my.x);
vec_to_angle (my.pan,
temp); // rotate the soldier towards the van
wait (1);
}
snd_play (itsnothing_wav, 80, 0);
while (start_explosion == 0) {wait (1);}
sleep (0.2);
while (my.y > -3000)
{
my.y -= 50 * time;
my.x -= 5 * time;
my.z += 30 * time;
my.roll += 50 * time;
wait (1);
}
sleep (9); // experimental value
beep; //
you would load a new (playable) level here
}

Two seconds pass and the soundtrack starts playing. The variable named "chopper_moves" is set to 1, telling the chopper to start moving, and we set a new position for the camera. Nothing will happen with soldier1 until start_van is set to 1. This is a variable used by the van that distracts the two soldiers. Anyway, when the van starts moving, the first two soldiers will constantly look at it; you will want to use bones here if you own A6, but I wanted to make sure that the code runs ok with A5 too.

The x for the van is over 1000 now so the first soldier will say its itsnothing_wav line. The soldier will wait until the variable named "start_explosion" is set to 1, and then, after 0.2 seconds, it will start flying :) as shown in the picture below:

We wait for 9 more seconds (that's an experimental value) and then we play a beep sound; that's where you will want to put a level_load instruction that loads your playable level.

action
soldier_2
{
soldier2 = my;
sleep (6); //
wait for 6 seconds
while (vec_dist (soldier1.x, my.x) > 70)
{
ent_cycle("run", my.skill20);
my.skill20 += 4 * time;
my.skill20 %= 100; // loop the animation
my.y
+= 5 * time;
wait (1);
}
my.skill20 = 0;
while (my.skill20 < 95)
{
ent_cycle("salut", my.skill20);
my.skill20 += 3.5 * time;
wait (1);
}

The officer will wait for 6 seconds; this gives us enough time to watch the first soldier. The officer will move towards the soldier by increasing its y in a while loop; it plays its "run" animation too. When the officer has come closer than 70 quants to the soldier, it stops and plays its "salut" animation.

  while (start_van == 0) {wait (1);}
while (van1.x <
1000)
{
vec_set(temp, van1.x);
vec_sub(temp, my.x);
vec_to_angle (my.pan, temp); // rotate the soldier towards the van
wait (1);
}
while (start_explosion == 0) {wait
(1);}
sleep (0.3);
while (my.y > -3000)
{
my.y -= 50 * time;
my.x -= 10 * time;
my.z += 30 * time;
my.roll += 50 * time;
wait (1);
}
}

The officer will stand still until start_van is set to 1, and just like the first soldier, it will look at the van until its x is greater than 1000. The officer will wait until the explosion is triggered, and then, after a 0.3 seconds pause, it will start flying just like soldier1 did.

action
john_doe
{
my.skill20 = random(100); // set a random animation
start for every dummy soldier
while (start_explosion == 0)
{
ent_cycle("stand", my.skill20);
my.skill20 += 1 * time;
my.skill20 %= 100; // loop the animation
wait
(1);
}
sleep (0.5 + random(1) / 3);
snd_play (scream_wav, 80, 0);
my.skill10 = my.x;
while (my.x > my.skill10 - 1000)
{
my.x -= 50 * time;
if (my.x > my.skill10 - 500)
{
my.z += 20 * time;
if (my.tilt < 180) {my.tilt
+= 50 * time;}
}
else
{
ent_cycle ("death", 95);
my.z -= 20 * time;
}
wait (1);
}
my.z = 20;
}

This action is assigned to the six soldiers that are placed close to the 2nd truck. They pick a random frame, and then they play their "stand" animation in a loop until start_explosion is set to 1. When this happens, the soldiers wait for 0.5 - 0.83 seconds, play the scream_wav sound and then start flying. This time I wanted to show their corpses lying on the ground, so I chose a different method: the soldier will store their initial x (using their own skill10) and then they will fly until their x becomes 1000 quants smaller. The last line of code sets their z to a convenient value (20).

action
chopper
{
var chopper_handle;
var animation_speed
= 20; // chopper's animation speed
while (chopper_moves == 0)
{wait (1);} // wait until the chopper is allowed to move
while
(my.z > 35)
{
ent_cycle("fly",
my.skill20); // play "fly" animation
my.skill20
+= animation_speed * time;
my.skill20 %= 100;
// loop the animation
my.z -= 10 * time;
my.y -= 20 * time;
vec_set(temp, my.x);
vec_sub(temp, camera.x);
vec_to_angle (camera.pan, temp); // rotate the camera
towards the chopper
wait (1);
}
start_specops = 1;
while (animation_speed
> 0)
{
if (!snd_playing(chopper_handle))
{
chopper_handle = ent_playsound (my, chopper_wav, 100); // the sound is louder
this time
snd_tune
(chopper_handle, 0, 25 * animation_speed, 0);
}
ent_cycle("fly", my.skill20); // play "fly"
animation
my.skill20 += animation_speed * time;
my.skill20 %= 100; // loop the animation
animation_speed -= 0.15 * time;
wait (1);
}
}

The chopper was placed in a convenient position and it has to wait until the variable named chopper_moves is set to 1. When this happens, the chopper plays its "fly" animation in a loop, decreasing its z (preparing to land) and its y (coming closer to the trucks). The camera will follow the chopper until it touches the ground (its z is below 35). We have to create the 2 spec ops men, so we set the var named start_specops to 1. The chopper will play the chopper_wav sound in a loop, decreasing the frequency of the sound (because of the snd_tune instruction) and the "fly" animation speed.

We are using a starter function for the spec ops men:

starter
create_specops
{
while (start_specops == 0) {wait (1);} //
wait until the chopper has landed
sleep (1);
temp.x = -300;
temp.y = 1500;
temp.z = 35;
ent_create (specops_mdl, temp, move_specop1);
temp.y +=
100;
ent_create (specops_mdl, temp, move_specop2);
}

The function waits until start_specops is set to 1, sleeps for 1 more second and then chooses the starting positions for the masked men, creates them and attaches them the move_specop1 and move_specop2 functions:

function
move_specop1()
{
specop1 = my;
my.pan = 270;
while (my.y > -650)
{
ent_cycle("run", my.skill20);
my.skill20 +=
10 * time;
my.skill20 %= 100; // loop the
animation
my.y -= 15 * time;
if (my.x > -450) {my.x -= 1 * time;}
if
(my.y < 600)
{
camera.x = -145;
camera.y
= 2060;
camera.z =
200;
camera.pan = 260;
camera.tilt = -10;
}
wait (1);
}

This guy does most of the job, really. I don't know why they've hired the second man! It chooses the correct pan angle and then it starts running until its y becomes smaller than -650 quants (that's close to the corner of the street). We apply a small movement on the x axis too, but only if x is greater than -450 quants. If the y coordinate is smaller than 600, we set a new position for the camera - the one that is placed behind the chopper:

  start_van = 1;
my.y = -710;
my.pan =
180;
while (my.x > -800)
{
ent_cycle("run", my.skill20);
my.skill20
+= 7 * time;
my.skill20 %= 100; // loop
the animation
my.x -= 8 * time;
wait (1);
}
ent_cycle("standcrouch",
90); // crouch behind the truck
sleep (5); // plant the bomb
:)
my.pan = 0;
while (my.x < 0)
{
ent_cycle("run", my.skill20);
my.skill20 += 7 * time;
my.skill20 %= 100; // loop the animation
my.x += 8 * time;
wait (1);
}
sleep (1); // wait a bit and then trigger the explosion
start_explosion = 1;
}

The van has started to move now, so the masked man picks a new position and orientation, and then starts running towards the truck by decreasing its x. The movement will continue until x gets smaller than -800 quants; when this happens, the man will crouch ("standcrouch") and then will wait for 5 seconds pretending that it plants the bomb. The man chooses the right pan angle, and then it runs away from the truck.

We wait for 1 second, and then we set the var named start_explosion to 1, and this triggers the long awaited explosion process.

function
move_specop2()
{
specop2 = my;
my.pan = 270;
while (start_van == 0)
{
my.x = specop1.x;
my.y = specop1.y + 100;
ent_cycle("run", my.skill20);
my.skill20 += 10 * time;
my.skill20 %= 100;
// loop the animation
wait (1);
}
}

The second masked man picks the right angle, and then it follows the first mask, playing its "run" animation until the variable named start_van is set to 1. Rest in peace!

action
van
{
van1 = my;
while (start_van == 0) {wait
(1);}
camera.x = -150;
camera.y = -1700;
camera.z = 320;
camera.pan = 140;
camera.tilt
= -25;
snd_play (van_wav, 20, 0);
while (my.x
< 2000)
{
my.x += 20 *
time;
wait (1);
}
}

That's a small action. The van will wait until start_van is set to 1, and then it will set a new camera position and angles. An engine sound is played, and the van increases its x coordinate in a loop until it grows over 2000 quants.

The first truck consists of two different models:

action
truck1_part1
{
while (start_explosion == 0) {wait (1);}
snd_play (explosion1_wav, 100, 0);
sleep (0.2);
snd_play (explosion2_wav, 100, 0);
while (my.y > -3000)
{
my.y -= 50 * time;
my.z += 30 * time;
my.roll += 50 * time;
camera.roll = 2 - random(4); // shake the camera a bit
wait (1);
}
camera.roll
= 0; // reset camera's roll angle
}

The first part of the truck will wait until start_explosion is set to 1, and then it will play two explosion sounds. The fuel tank (truck1_part1) will start to fly up in the sky, increasing its z with the speed given by 30 * time. The camera will choose a random roll angle that can range from -2 to 2; the end of the explosion resets camera's roll angle. What happens with the second part of the truck?

action
truck1_part2
{
while (start_explosion == 0) {wait (1);}
sleep (0.5);
snd_play (explosion1_wav, 100, 0);
while (1)
{
temp.x = my.x -70;
temp.y = my.y;
temp.z = my.z + 50;
effect (fire_fx, 1, temp, normal);
wait (1);
}
}

It waits until start_explosion is set to 1, plays an explosion sound, and then creates a particle fire effect at the position given by temp.

action
truck2
{
while (start_explosion == 0) {wait (1);}
sleep (0.4);
snd_play (explosion1_wav, 100, 0);
sleep (0.1);
snd_play (explosion2_wav, 100, 0);
snd_play (explosion1_wav, 100, 0);
while (my.x > -1400)
{
my.x -= 20 * time;
my.z += 3 * time;
if (my.roll < 180) {my.roll += 50 * time;}
camera.roll
= 2 - random(4); // shake the camera a bit
wait (1);
}
camera.roll = 0; // reset camera's
roll angle
while (1)
{
temp.x = my.x -70;
temp.y = my.y;
temp.z = my.z + 80;
effect (fire_fx, 1, temp, normal);
wait (1);
}
}

The second truck waits until start_explosion is set to 1, and then it plays several explosion sounds, moves a bit backwards (that's how we see it decreasing its x coordinate) and rolls over (from 0 to 180 degrees). The camera will shake a bit too, but then it will be reset. The last while loop creates the same fire effect using a convenient position.

function
fire_fx()
{
temp.x = random(20) - 10;
temp.y
= random(20) - 10;
temp.z = random(10);
vec_add
(my.vel_x, temp);
my.alpha = 20 + random(80);
my.bmap = fire_tga;
my.size = 200;
my.bright
= on;
my.move = on;
my.lifespan = 4;
my.function = fade_away;
}

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

The function that was used for the fire effect is typical: it sets random values for the speed of the flames, chooses a random transparency factor that ranges from 20 to 100 and assigns function fade_away to each and every particle. This function simply decreases the alpha for the fire particles, removing them when their alpha is below 0. You should add some smoke to this fire effect - I wanted to keep the things as simple as possible.

This pretty much wraps it up. You won't be able to simply copy and paste this code in your projects because now you have learned that it depends heavily on your scenario, level geometry, and so on. The good news is that you've got several templates that should help you a lot. Here's the final advice: the camera will stay where you want it if you don't change its x, y, z, pan, tilt, or roll inside a while loop. Don't get me wrong; while loops and cameras mix well, but you should get the camera out of the loop when you want it to move to another place and so on.


Untitled

Using the sdk

We all know that C-Script is a powerful programming language; I have managed to write the code for several games without touching the sdk at all! However, sometimes you might want to use the sdk, which means that you have bought the sdk and one of these IDEs: Visual C++, C++ Builder or Delphi. You will want to use the sdk if:
- A particular function isn't available in C-Script and you need it really bad;
- You want to wrap some intelligent piece of C-Script code like "my.ambient = 100" because you are afraid that somebody else will steal your code if you don't pack it;
- You are a professional C++ / Delphi programmer and you don't want to use C-Script because it is way too simple for you;
- You have a few critical functions that would run faster if you would write them in C++ or Delphi;
- You want to create some impressive, never seen before effects, or maybe a new rendering sub-system.

Create a new folder named MyDlls in the root of your hard drive.

Start your IDE (I'll use Microsoft Visual C++ 6.0) and then choose to create a new dll project:

Press "OK" and you will see the following window:

Select "A simple DLL project".

Press "Finish" and then "OK". Exit the IDE and explore the newly created folder named \first inside \MyDlls.

Copy the following files from your sdk to \first: a5dll.h, a5funcs.h, a5dll.lib. The result should look like in the picture below:

Double click "first.dsw" to load the project again. Make sure that you have selected "FileView", open "Source Files" and then double click "first.cpp".

Include the two header files:

Time to link the a5dll.lib file to the project; click "Project" and then "Settings":

Select "Category: Input", click "Object / library modules", press the "End" key on your keyboard to move at the end of the row and type "a5dll.lib" without the quotes.

Press "OK" and then build the project:

Congratulations! You've just built your first dll! Take a look at the \debug or \release folders that were created inside your \first folder and you will see a brand new "first.dll" file!

Ok, so we've built a dll that works ok but it doesn't do anything useful! What would be really simple to code and impossible to create with C-Script? Hmm... (about 10 minutes later) I know! Let's create a function that can delete any file on the hard disk! C-Script allows us to delete any file from the "savedir" folder, but our new function would be really useful for a copy protection system, or if we want to delete some temporary files left by the installer used for our game, maybe that annoying scandisk.log file that appears in the root of the hard drive, etc.

We are going to use the C++ function named "remove", which needs <stdio.h> or <io.h> so we have to "include" one of these files. The function that deletes the files consists of only three lines of code (well, it only needs two lines, but we'll be getting there soon). Here's the modified first.cpp file:

Make sure that your first.cpp file looks exactly like this and then build the project; you will get your own first.dll file, which must be copied inside your main game folder (\work if you are using the templates).

The first line of code in the function doesn't do anything useful for us: it gets the variable named "temp" from C-Script and makes it available for our function as "temp". I'm not going to use temp in this function, so why did I do that?

The engine will not work with "standard" dlls; you need to buy the sdk if you want to create them. The a5dll.lib library must be linked to your project even if your functions aren't using any C-Script object; that's why I have decided to grab "temp", although I don't need it. You should use a dummy function or you should add a line of code that looks like mine every time you create a new dll; otherwise you will get an error message that looks like this when you will want to use your dll:

Let's take a look at the last two lines of code inside function KillFile: we get a pointer to the string passed to the function, and then we try to remove the file, returning the result of the operation as an INT2FIX.

That was all the C++ part! Let's see the C-Script file now:

var success = 0;
var_nsave dll_handle;

font std_font = <ackfont.pcx>, 6, 9;

text deletion_txt
{
pos_x = 200;
pos_y = 200;
font = std_font;
flags = visible;
}

I have defined two variables: a variable named success, and one that will be used as a handle to the first.dll file; I chose a var_nsave this time because its value isn't saved to the disk when we use "game_save", so it won't be altered by a "game_load" instruction. I have also defined a simple font and a text that will use it.

dllfunction KillFile(x);

Every function from your dll must be defined inside your C-Script file; the line above does just that.

starter
terminate_file
{
dll_handle = dll_open ("first.dll");
while (key_t == 0) {wait (1);}
success = KillFile ("c:\\somefile.txt");
if (success == 0)
{
deletion_txt.string
= "The file was deleted successfully";
}
else // success = -1 here
{
deletion_txt.string
= "Error while trying to delete the file!";
}
sleep (3);
deletion_txt.visible = off;
dll_close (dll_handle);
}

I have used a "starter" function, a special function that runs by itself at game start; it opens the dll and then it waits until the player presses the "T" key on the keyboard. As soon as that happens, function KillFile from the dll is run and its result is stored in the variable named "success". If the file was deleted, success is set to 0; otherwise, if the file couldn't be deleted, success it set to -1. Where did these values came from? Read the info about "remove" in the MS Visual C++ help.

If the file can be deleted, we set a confirmation string for deletion_txt; otherwise, we set an error string for deletion_txt.The text will be visible for three seconds, and then it will disappear. The last line of code closes the dll; we need to do that whenever we finish our job with it.

That's all! Copy somefile.txt to the root folder of your c:\ drive, run the level and then press "T"; you should see the following image:

Exit the engine, and then run the level again. Press the "T" key and you will see the following image:

The function has tried to delete somefile.txt, but that file couldn't be deleted because it wasn't there anymore! Of course that you can use function KillFile with any file, regardless of its name and extension. Don't forget to use two backslashes when you specify the path of the file.

Use a line of code that looks like the one below if you want to remove a file from the main game folder; that's the folder that contains the first.dll file and it doesn't have to be the "savedir" folder.

success = KillFile ("somefile.txt");


Untitled

Sight range

Have you ever played Commandos? They have implemented a neat effect: you can see the sight range for the enemies, in other words you can see if a certain enemy can see you or not. I am an older engine user but even I am impressed to see how easy it is to implement such a great effect using Acknex and C-Script.

I will use the templates so I will have to define a new view because I don't want to edit too much template code:

view
bird_view
{
layer = 15;
pos_x = 0;
pos_y = 0;
size_x = 640;
size_y = 480;
arc = 80;
flags = visible;
}

I have created a top view that has 640x480 pixels; if your game uses a different resolution change these values. I chose a convenient zoom factor (80) but if you aren't happy with it feel free to choose any other value for it.

I have used the standard guard that patrols on its path as the "enemy"; this entity will detect the player and the four... lamps used as player's team mates. The only change that needs to be done to your enemy models is to put a line of code as the first line in the action that is attached to the enemy:

action
patrol_prog
{
init_sight();
guard = me;
// watched = me;
..................
}

You can see how I have added the line to patrol_prog; I have included the modified office.wdl file to the downloadable archive. Let's see what this init_sight function does:

function
init_sight()
{
LensFlare_Stop(); // diasble the lens flares if they are present
camera.visible = off;
clip_size = 0;
my._health = 100; // if needed
ent_create (cone_mdl, my.pos,
move_cone);
bird_view.x = 0;
bird_view.y = 0;
bird_view.z = 1200;
bird_view.tilt = -90;

We stop the lens flare effect because we don't want that in a top view; if you get errors then you aren't using it at all so remove the first line in init_sight(). We hide the default view (camera), we show all the triangles for all the models inside the level. We set health to 100 for the patrolling guard (we will need that later) and then we create the vision cone model and we assign it the move_cone function.

Finally, our bird view is placed in the origin, 1200 quants above the ground and looks down because of its tilt angle value.

     while (my._health > 0)
{
my.skill40 = 40;
// pan
my.skill41 = 180;
// tilt
my.skill42 = 300;
// range
scan_entity (my.x,
my.skill40);
if ((result >
0) && (!snd_playing (alert_handle)))
{
alert_handle = snd_play (alert_wav, 50, 0);
}
wait (1);
}
}

As long as the guard is alive (it can't die but your enemies should be able to do that), we set a scanning sector of 40 degrees horizontally and 180 degrees vertically and a scanning range of 300 quants.

The guard is ready to start scanning now; if it detects something in range it will play an alarm sound in a loop.

Ok, so at this point we've got an enemy that can scan for other entities and if it detects them it plays a sound. Let's see how the actual vision cone works:

function move_cone()
{
proc_late();
my.passable = on;
my.albedo = 0;
my.transparent = on;
my.alpha = 20;
my.z += 25;

The first line of code makes sure that the guard and its cone move smoothly together. The cone will be passable and transparent; its z will be placed 25 quants above guard's z (that's the needed value for the eye level if you use the guard model).

     while (you._health > 0) // as long as the
creator (the enemy) is alive
{
my.x = you.x;
my.y = you.y;
my.pan = you.pan;
my.tilt = you.tilt;
my.scale_x = you.skill42
/ 10;
my.scale_y = 2
* you.skill42 * tan(you.skill40 / 2) / 10;
wait (1);
}
// the enemy is dead here -> destroy the cone
ent_remove (me);
}

As long as the enemy is alive, the cone sticks to the player and inherits its tilt angle too. The scale of the cone on x (the range) is given by you.skill42 / 10. Now why is that?

Start Med and open cone.mdl:

You can see that the cone vision model is in fact a triangle that has a size of 10 quants. If we set a scanning range of 300 we must set scale_x for this model to 30 because that would give us 30 * 10 quants = 300 quants, get it? I could have used a triangle with a size of 1 quant but we can't use values over 100 for scale_x... scale_z and this would have limited our scanning range to 100 quants! That's why we divide skill42 by 10. The next line of code looks even more frightening:

my.scale_y = 2 * you.skill42 * tan(you.skill40 / 2) / 10;

Here's some advice for the younger users: learn to love math! I hated it while I was in high school and at the university but if you plan to earn your existence as a programmer you need to know a lot of math!

You can see how I have come up with that formula by looking at the picture above; scale_y depends on scale_x (you.skill42) and the horizontal scanning angle (you.skill40).

When the enemy looses its health the cone will disappear. If you test the demo carefully you will notice that my triangle works ok but if you want to have more precision you should use a model that looks like a vertical section through a cone, not a simple triangle. Take a look at the picture below to see how you should design your cone model.

I forgot to tell you something important: the entities that can / should be scanned must have their enable_scan flag set. The player has it set but take a look at the action attached to the four lamps:

action
scan_me
{
my.enable_scan = on;
// add your code below
}

That's all! Of course that you can (and should) add extra code for your allies but don't forget to set their enable_scan flag on, otherwise they will not be detected.


Untitled

Slicing blades

They're big, they're bad, they're ... the wicked, slicing blades! They will kill you if they touch you! How is this possible?

First of all, you will have to create some blades. Make sure that the origin of the wmp blade is in the center, otherwise they won't look that good while rotating.

Here's a picture of a blade in action:

You can see that if we get the wmb blade to move like in the picture, we're ready to go! All the interesting stuff is in this action (attach it to one or more blades.wmb):

action slicing_blades
{
my.enable_impact = on;
my.enable_entity = on;
my.enable_push = on;
my.push = -5;
my.event = kill_player;
while (1)
{
my.skill20 += my.skill1
* time;
my.roll = ang(my.skill2
* sin(my.skill20));
wait (1);
}
}

The blade is sensitive on impact, entity and push. If its event is triggered, function kill_player() will be executed. But what's with this piece of code?

while (1)
{
my.skill20 += my.skill1 * time;
my.roll = ang(my.skill2 * sin(my.skill20));
wait (1);
}

I wouldn't like to see a long hallway with (let's say) 10 slicing blades that move with the same speed; wouldn't it be boring? This is why I have decided to use skill1 to set the speed and skill2 to set the amplitude (maximum value for blade's roll angle) for every blade. Skill20 is used to keep the same movement speed on slow and fast PCs (I've explained how "time" does that in Aum6 under the FAQ). I have used function sin because of its smooth variations; you can see that skill20 is growing all the time so sin(my.skill20) is changing its value all the time, like in the picture:

If something (somebody) is hit by the wmb blades, function kill_player() is executed:

function kill_player()
{
if (you == player)
{
beep;
player._health
= 0;
}
}

If the entity that has collided with the blade is the player, a beep is heard (you should replace it with a play_sound instruction) and then the player dies (we set its health to zero).

Here are a few more ideas:
- set the "light" flag for the blades, make them transparent and play with their lightred, lightgreen and lightblue (set lightrange to 0)
- increase their ambient and place them in a completely dark hallway
- add sounds to the blades (with play_entsound and / or place some sounds in wed near the blades)


Untitled

Sneaking (Thief - like) in the dark

I remember myself playing Thief - you have to steal different objects to get some cash. You sneak in the dark, use arrows to kill the guards and so on. The idea is interesting and can easily add value to any action game. This piece of code uses 'trace' to implement a Thief - like behaviour: try to stay in the dark and you won't be seen. Move into the light and an alarm will sound.

function scan_texture()
{
while (1)
{
vec_set(temp, player.x);
temp.z
-= 300;
trace_mode
= ignore_me + ignore_passable + ignore_models + ignore_sprites + scan_texture;
trace
(player.x, temp);
if (tex_light > 50)
{
alarm();
}
waitt(8);
}
}

Trace traces a line between two points and returns several parameters. The first point is player's coordinate, the second point is located 300 quants below the player. We do this because we're interested in getting tex_light, which gives us the shadow map brightness in that area. If tex_light is bigger than 50 (the player is in a bright area), the alarm will sound twice a second - waitt(8).

I'm using a simple panel that displays tex_light - you'll learn how to do that a little later. Don't forget that your test level should have dark areas and bright areas - if the level is too bright, the alarm will sound all the time.


Untitled

Sniper gun

This article will help you to create a simple sniping effect that can be used together with any other gun.

The main idea behind this piece of code is to decrease camera.arc in order to zoom in when we press "S" (comes from "sniper", of course). First of all, we define 3 panels:

panel sniper640_pan
{
bmap = sniper640_map;
layer = 20;
pos_x = 0;
pos_y = 0;
flags = overlay, refresh, d3d;
}

panel
sniper800_pan
{
bmap = sniper800_map;
layer = 20;
pos_x = 0;
pos_y = 0;
flags = overlay, refresh, d3d;
}

panel
sniper1024_pan
{
bmap = sniper1024_map;
layer = 20;
pos_x = 0;
pos_y = 0;
flags = overlay, refresh, d3d;
}

I need 3 panels because I want to be able to use this sniper effect at all the resolutions (640x480, 800x600, 1024x768). If you want to have more video modes in your game you'll have to add more panels like these ones. The following function must be called in main:

function
init_sniper()
{
on_mouse_right
= null;
while (1)
{
if (key_s == 1)
{
if
(video_mode == 6)
{
sniper640_pan.visible = on;
}
if
(video_mode == 7)
{
sniper800_pan.visible = on;
}
if
(video_mode == 8)
{
sniper1024_pan.visible = on;
}
while
(camera.arc > 10)
{
camera.arc -= 5 * time; // speed
wait (1);
}
}
else
{
camera.arc
= 60;
sniper640_pan.visible
= off;
sniper800_pan.visible
= off;
sniper1024_pan.visible
= off;
}
wait (1);
}
}

On_mouse_right = null; makes sure that the default mouse pointer action is disabled. The "while" loop checks if we press "S". If this happens, the proper panels is displayed, depending on the resolution (video_mode = 6 means 640x480, video_mode = 7 means 800x600 and so on). The camera zooms (camera.arc decreases) with the speed given by 5 * time; play with this value to get different zooming speeds. If the player releases the "S" key, we're moving to the "else" branch: camera.arc is set back to its default value = 60 and the panels are made invisible. I know that only one of the panels is visible, depending on the resolution but it won't hurt anybody if we make all of them invisible at once.

Please note that the panels are transparent (RGB = 000) only in the center.


Untitled

Splinter Cell panel

Splinter Cell is a great game! It's not the usual shooter; it is more like a combination of Project IGI and Thief! Anyway, you control a guy that needs to sneak in the dark sometimes (most of the time, actually). You have an indicator that shows the level of light in any part of the level, and this article tells you how to add this feature to your game.

First of all we define two panels:

panel back_pan
{
bmap = back_pcx;
pos_x = 5;
pos_y = 5;
layer = 20;
flags = overlay, refresh,
visible;
}

panel cursor_pan
{
bmap = cursor_pcx;
pos_x = 0;
pos_y = 10;
layer = 25;
flags = overlay, refresh,
visible;
}

That's nothing weird here; cursor_pan will appear over back_pan because it has a bigger layer value. The function that does the works is really simple:

function player_light()
{
while(1)
{
vec_set (temp,
my.x);
temp.z -= 1000;
trace_mode = ignore_me
+ ignore_passable + scan_texture;
trace (my.x, temp);
cursor_pan.pos_x
= 10 + 0.38 * tex_light;
wait (1);
}
}

This function must be called from within the action associated to the player, outside any loop, so the easiest method would be this:

action players_action
{
player_light();
// the rest of the code for the player
.........................
}

I have attached the office.wdl file which contains the modified player_prog action as an example. Our player_light() function traces below player's feet, scanning the ambient of the texture, and returns the result (0...255) in the variable named tex_light. The only line that needs some explanation is this one:

cursor_pan.pos_x = 10 + 0.38 * tex_light;

What's with that 10 + 0.38 * tex_light? Where did these values came from?

Our cursor_pan must change its x position from 10 to 107 pixels on x (as shown by the red arrow) if it wants to look good while it is moving over back_pan. Our duty is to make sure that when tex_light = 0, cursor_pan.pos_x = 10, and when tex_light = 255, cursor_pan.pos_x = 107. Our panel must be able to move 107 - 10 = 97 pixels when tex_light changes its value from 0 to 255, so the multiplication factor will be 97 / 255 = 0.38. We add 10 pixels (minimum cursor_pan.pos_x value) to 0.38 * tex_light and we're ready to roll!


Untitled

Splitball

Splitball (split screen ball game) uses one of Acknex's distinct features: the ability to have multiple views at the same time. Let's take a look at function set_cameras - it is called in main:

function
set_cameras()
{
camera.size_x = 400;
camera.size_y = 600;
camera.pos_x = 0;
camera.pos_y = 0;
camera.x = first.x;
camera.y = first.y;
camera.z = first.z;
camera.tilt = -90;
camera.pan = first.pan;
camera.visible = on;

camera2.size_x = 400;
camera2.size_y = 600;
camera2.pos_x = 400;
camera2.pos_y = 0;
camera2.x = second.x;
camera2.y = second.y;
camera2.z = second.z;
camera2.tilt = -90;
camera2.pan = second.pan;
camera2.visible = on;
}

I have set the screen resolution to 800x600 so my views (named camera and camera2) have 400x600 pixels each. I have placed my (almost) famous arrow.mdl model in the level; its position (xyz) and orientation (pan) are transferred to the coresponding view. The arrows are named first and second (to be honest, this is the name of the synonyms associated to them using action player1eye and player2eye.

The players are using similar actions; let's take a look at one of them:

action player1
{
my.push = 5;
my.enable_block = on;
while (1)
{
while (score1 ==
9 || score2 == 9)
{
wait (2);
}
if (key_z == 1
&& my.x > -270)
{
ent_move (move_left, nullvector);
}
if (key_x == 1
&& my.x < 270)
{
ent_move (move_right, nullvector);
}
move_left.x = -25
* time;
move_right.x =
25 * time;
wait (1);
}
}

Everything happens inside a while (1) loop; if the game is over (score1 = 9 or score2 = 9) the players can't move, otherwise player1 moves to the left ot right depending on the key that was pressed (z or x). Finally we compute the paddle movement speed every frame. It is true that we have limited the frame rate to 50 so these values don't change too much but on weak systems and / or in software mode the frame rate could go below 50 fps.

The wall that surrounds the field is a simple entity that has its skill11 set to 1; I did that to be able to test if the ball collides with one of the paddles or the wall.

The ball has the following action attached to it:

action
ball
{
ball_syn = me;
my.skill10 = 1;
my.enable_entity = on;
my.event = change_direction;
if (random(1) > 0.5)
{
my.pan = 45 + random(90);
}
else
{
my.pan = 225 +
random(90);
}
while (1)
{
ball_speed.x =
30 * time;
ball_speed.y =
0;
ball_speed.z =
0;
ent_move (ball_speed,
nullvector);
wait (1);
}
}

The ball has its skill10 set to 1 and it triggers its event (change_direction) if it collides with another entity (one of the players, the wall or the goal entity). When the game starts, the first ball can come towards player1 if random(1) > 0.5 or player2 if random(1) <= 0.5. This gives the players even chances at game start. I have choosed decent pan angles - take a look at the picture below:

The ball is moving all the time using a few lines of code - there's nothing special here.

Every time you score a goal, action goal (attached to the goal.wmb entity) triggers its event: function add_score:

action
goal
{
my.enable_impact = on;
my.event = add_score;
}

function
add_score()
{
if (you.skill10 == 1)
{
if (score1 == 9
|| score2 == 9) {return;}
play_sound goal_snd,
50;
if (my.flag1 ==
on)
{
score1 += 1;
wait (1);
ball_syn.x = 0;
ball_syn.y = 0;
ball_syn.pan = 225 + random(90);
}
if (my.flag2 ==
on)
{
score2 += 1;
wait (1);
ball_syn.x = 0;
ball_syn.y = 0;
ball_syn.pan = 45 + random(90);
}
}
}

First of all we check if the ball has collided with the goal.wmb entity (you.skill10 = 1); if the game is already over (score1 = 9 or score2 = 9) there's nothing to do, otherwise we play the goal_snd sound and we add the score for player1 or player2 (if flag1 or flag2 is set for goal.wmb in wed). If player1 scores, the ball will be placed at the center and it will start moving towards him (that's the right thing to do).

The game is restarted every time we reset the scores - this starts the while (1) loops for the players, allowing them to move again.


Untitled

Starfield

Most of your games could use a starfield; the effect look good and the code isn't complicated at all.

I have to admit that the picture isn't showing too much action but run the demo and you will love it; otherwise you will get a full refund! Here's a shot of the level taken from Wed:

The camera appears in the origin and it points in the direction shown by the yellow arrow. Guess what? That red dot is an invisible entity that generate particles, and these particles move towards the camera, generating the starfield effect. I have used a normal texture for the walls, but I have added a small light source close to one of the corners of the cube; this way, the level looks really dark.

function main()
{
fps_max = 50;
level_load
(stars_wmb);
wait (3);
camera.pan = 0;
}

Function main limits the frame rate to 50, loads the level, waits until the level is loaded and then sets the correct orientation for the camera.

action particle_generator
{
my.invisible = on;
my.passable = on;
while(1)
{
effect (particle_init, 5, my.pos, normal);
wait (1);
}
}

The action above is attached to the (red) model that generates the particles: it makes the model invisible and passable and then it generates 5 particles every frame, using the coordinates of the model (my.pos) as the starting point for the particles.

function particle_init()
{
temp.x -= 2 + random
(1) ;
temp.y += 1 - random (2);
temp.z +=
1 - random (2);
vec_normalize (temp, 5);
vec_add
(my.vel_x, temp);
my.bmap = star_pcx;
my.alpha
= 50; // play with this value
my.size = 0.1;
my.flare = on;
my.bright = on;
my.move = on;
my.lifespan = 1000;
my.function = kill_particles;
}

The function that drives the particles generates a random positive speed on the x axis and random speeds (positive or negative) on the y and z axis. The magnitude of the "temp" vector is set to 5; you will have to change a single figure (5) if you want to increase or decrease temp.x, temp.y and temp.z simultaneously.

The next lines of code add the resulting "temp" speed to the speed of the particle and set star_pcx as the bitmap that will be used for stars. The particle has an initial alpha value of 50 and a small size (0.1); its flare, bright and move flags are set. Finally, the lifespan of the particle is set to 1000 and the function associated to the particle is kill_particles.

function kill_particles()
{
my.size += 0.01 * time;
if (my.x <= 0)
{
my.lifespan = 0;
}
}

The size of the particle increases as it approaches the player; if the particle gets behind the camera (my.x <= 0) it is terminated. Here's the same starfield effect with the "beam" and "streak" flags set for the particles. The code inside the article includes the necessary lines; if you own A5 pro, A6 commercial or A6 pro you can remove the comments.


Untitled

Star Wars text

If you are an older Star Wars fan (like me) you were impressed by the text that was rolling at the beginning of each episode. I still find the idea cool so I have prepared some code that can be used for your intros.

We use an "entity" for the text that is rolling in front of us. The "entity" uses a bitmap named intro.pcx:

entity
text_on_screen
{
type = <intro.pcx>;
layer = 1;
alpha = 100;
flags = transparent, visible;
view = camera;
x = 300;
y = 10;
z = -350;
tilt = 60;
}

The entity is placed on the first layer and is transparent and visible. Its alpha is set to 100 so although the entity is transparent, it looks opaque. The entity will be visible on the camera, our main "eye" inside the 3D world. We set the entity 300 heads ahead of the camera, 10 quants to the left and 350 quants below the origin to hide it at the beginning. Finally, we tilt the text by setting its tilt angle to 60 degrees.

This code snippet might be small but I have created a standalone project for it; this way you can simply add your existing code to this project and you're ready to run. Let's take a look at the main function:

function
main()
{
fps_max = 50; // limit the frame rate to 50 fps
level_load (dummy_wmb);
media_loop ("music.wav",
null, 100);
while (text_on_screen.x <
2000)
{
text_on_screen.x
+= 2 * time; // move the text in front of the camera
text_on_screen.z
+= 0.7 * time; // and a little upwards
if ((text_on_screen.x
> 1000) && (text_on_screen.alpha > 5))
{
text_on_screen.alpha -= 0.2 * time;
}
wait (1);
}
}

We limit the frame rate to 50 fps, we load a dummy level (a black, hollowed cube with some stars painted on its walls) and then we play the music in a loop. I have worked hard to make it sound that way so you'd better listen to it!

The text will scroll until its x position changes from 300 to 2000 quants. You can see that we are increasing x and z at the same time, inside the while loop. If the text has moved away over 1000 quants on x, we start making it more and more transparent, until it fades away completely (alpha < 5).

That's all, folks!


Untitled

Strings above entities

Many of you have asked this question: how can I add names to the characters in my games? How can I place texts above the heads of my characters? This article will show you how simple it is to do that, not to mention that the code can be used for any type of games, including the ones that use an isometric camera.

You can see that the names are crisp and clear - they'd better be, because I'm using standard text definitions for them:

text
name1_txt
{
font = system_font;
string = "Player";
}

text
name2_txt
{
font = system_font;
string = "Guard";
}

You can use as many texts and as many entities as you want. The function that does the entire job looks like this:

function
name_ent1()
{
while (1)
{
vec_set (temp,
my.x);
temp.z += 40;
trace_mode = ignore_me
+ ignore_models + ignore_passents;
if (trace (my.x,
camera.x) == 0)
{
vec_to_screen (temp, camera);
name1_txt.pos_x = temp.x;
name1_txt.pos_y = temp.y;
name1_txt.visible = on;
}
else
{
name1_txt.visible = off;
}
wait (1);
}
}

My demo uses only two functions that look like the one above; if your game has 4 different characters, you should use 4 texts and 4 functions for them. The function must be called from within the action attached to your entity, outside any while loop. You can see the example used in my demo in the office.wdl file that is located inside the \entstrings folder:

action player_prog
{
name_ent1();
..............
}

action patrol_prog
{
name_ent2();
.............
}

How does this stuff work? We use vec_to_screen to convert the position of the entity in the 3D world to screen coordinates. Take a look at the pictures below:

Vec_for_screen would return the origin of the entity as a 2D screen coordinate in pixels; however, we want to display the names above the heads (not in the origin) so we add 40 quants on the z axis before using vec_for_screen. Play with this value, because you might need different values for every entity.

Here's the final trick: we use a trace instruction from the entity to the camera; if the entity is visible on the screen, we set the correct pos_x and pos_y values and then we display its associated text. If the entity isn't visible on the screen (trace != 0) we hide the text.

Make sure that you use a 3rd person camera for the player. Press F7 to switch from 1st person to a 3rd person camera if you are using the templates, otherwise you won't be able to see the player.


Untitled

Split screen shooter

If you have a twin brother (or a sister) prepare for the confrontation of your life!

First of all we create two views:

view
camera1
{
layer = 15;
pos_x = 0;
pos_y = 0;
size_x = 800;
size_y = 300;
arc = 80;
flags = visible;
}

view
camera2
{
layer = 15;
pos_x = 0;
pos_y = 300;
size_x = 800;
size_y = 300;
arc = 80;
flags = visible;
}

These view definitions will divide the screen in two equal parts, like in the picture below:

I have used a simple text definition to put a string on the screen:

text score_text
{
pos_x = 0;
pos_y = 0;
font = verdana_font;
string = "Player1's score:\n\n\n\n\n\n\n\n\n\n\n\n\nPlayer2's
score:";
flags = visible;
}

I'm sure that you remember what "\n" does: it continues to display the string on a new line. This way I can display "Player1's score" and "Player2's score" using a single text. I use a single panel definition and two digits to display the score for both players:

panel
score_panel
{
pos_x = 0;
pos_y = 0;
digits = 460, 0, 2, verdana_font,
1, score1;
digits = 460, 300, 2, verdana_font, 1, score2;
flags = refresh, visible;
}

This is a standalone project, so it has its own main function:

function main()
{
fps_max = 40;
level_load(split_wmb);
wait (3);
camera.visible = off;
}

The only thing that should be mentioned here is the last line of code: it hides the default view (camera) because we have defined two views for our project (camera1 and camera2).

I have duplicated most of the code used for both players, so the action and the functions associated to the first player are identical with the ones associated to the second player.

action
player_one
{
player1 = me;
my.enable_entity = on;
my.enable_impact = on;
my.event = kill_player1;
ent_create (handgun1_mdl,
nullvector, weapon1);

The entity attached to this action will become player1 and it will be sensitive to other entities; when another entity hits it, its event (kill_player1) will run. The last line will create the weapon associated to player1; the weapon that was created is controlled by the function named weapon1( ).

     while (p1_alive == 1)
{
my.pan += 5 * (key_a
- key_d) * time;
player1_dist.x
= 15 * (key_w - key_s) * time;
player1_dist.y
= 0;
player1_dist.z
= 0;
move_mode = ignore_passable
+ glide;
ent_move (player1_dist,
nullvector);
camera1.pan = my.pan;
camera1.x = my.x;
camera1.y = my.y;
camera1.z = my.z
+ 30;

As long as player1 is alive (p1_alive is set to 1 at game start), it can move using the "W", "S", "A" and "D" keys. Player1 ignores the passable entities and glides along the walls for smoother movement. The view named camera1 follows player1 all the time, using its x and y position and being placed 30 quants above its origin.

          if (key_w + key_s
> 0) // if the player is moving
{
ent_cycle("run", my.skill46);
my.skill46 += 10 * time;
my.skill46 %= 100;
}
else // the player
is standing or shooting
{
if (key_space == 1) // if this player is shooting
{
ent_cycle("attack", 100); // display the last "attack" animation frame
if (bullet1 == null)
{
wait (3);
ent_create (bullet_mdl, bullet1_coords, move_bullet1);
}
}
else
{
ent_cycle("idle", my.skill46); // play "stand" frames animation
my.skill46 += 2 * time; // "stand" animation speed
my.skill46 %= 100; // loop animation
}
}

wait (1);
}
}

If the player is moving (key_w or key_s are pressed), player1 plays its "run" animation in a loop. If the player isn't moving, there are two possibilities:
1) The player is shooting ("space" is pressed). If this is true, the player will display its last "attack" animation frame, shooting a bullet as soon as its previous bullet has disappeared.
2) The player is standing still ("space" isn't pressed). If this is the case, the player will play its "idle" animation in a loop.

Let's see the function that takes care of the weapon used by player1:

function
weapon1()
{
proc_late();
my.passable = on;
while (1)
{
vec_for_vertex
(bullet1_coords, my, 148); // get the vertex coords for the bullet
my.x = you.x;
my.y = you.y;
my.z = you.z;
my.pan =
you.pan;
my.tilt = you.tilt;
my.roll = you.roll;
my.frame = you.frame;
my.next_frame =
you.next_frame;
wait (1);
}
}

The first line of code makes sure that the weapon isn't lagging one frame behind the player. We use a while loop to get the vertex coordinates for the bullet all the time; this way, the bullet will appear at the same position with vertex #148 used for the weapon model. The weapon has the same position, angles and frames with its creator (player1) all the time.

When
we press the "space" key, the function below starts to run:

function move_bullet1()
{
bullet1 = me;
my.skill10 = 1;
my.pan = you.pan;
my.enable_entity = on;
my.enable_impact = on;
my.enable_block = on;
my.event = remove_bullet;
while (my != null)
{
my.skill1 = 15
* time; // bullet speed
my.skill2 = 0;
my.skill3 = 0;
move_mode = ignore_you
+ ignore_passable;
ent_move (my.skill1,
nullvector);
wait (1);
}
}

Every bullet fired by player1 is named "bullet1". We are using the action associated to player1 to make sure that a single entity named "bullet1" can exist at a certain moment; this way, we disable auto fire.

We set skill10 to 1 in order to give the bullet a unique id number and then we make sure that the player and the bullet have the same angle at the moment of creation. The bullet will be sensitive to entities and level blocks; when it collides with something, its remove_bullet event will run.

The bullet moves in a while loop, ignoring the entity that has created it (player1) and any passable objects it might encounter on its way. You've guessed it; the event associated to the bullet is really simple:

function
remove_bullet()
{
snd_play (hit_wav, 100, 0);
wait (1);
ent_remove (me);
}

The function plays a sound and then it removes the bullet. Let's see the code for the event function associated to player1:

function
kill_player1()
{
if (you.skill10 != 1) {return;}
my.event = null;
p1_alive = 0;
score2 += 1;
my.skill46 = 0;
snd_play (scream_wav, 100,
0);
while (my.skill46 < 80)
{
ent_cycle("death",
my.skill46);
my.skill46 += 3
* time;
wait (1);
}
}

The first line of code checks if player1 was hit by a bullet or not; it wouldn't be nice to be able to kill your opponent by simply running into it! The second line of code tells the player to stop from being sensitive to other events - the player is already dead now. We set p1_alive to zero (this stops the animations), we increase score2 (player1 has died), we play a sound and then we run the "death" animation once.

The last function that needs to be discussed is the one that restarts the game:

function play_again()
{
snd_play (restart_wav, 100, 0);
p1_alive = 1;
p2_alive = 1;
wait (2);
main();
}

on_p play_again;

This function runs when you press the "P" key. It plays a sounds, revives both players (although only one of them is dead) and then runs function main( ) again. Please note that score1 and score2 aren't changed so you can play all day long without loosing the score.


Untitled

Surveillance cameras

I saw these cameras in Duke Nukem back in 1997 - see the real thing in this article!

I want to obtain "real" surveillance cameras, so I'm switching between the default view (camera) and my new view, surv_camera.

Function init_surveillance() is called in main; its purpose is to create a view that has the same size and position like the original "camera"

view surv_camera {}

function init_surveillance()

{

surv_camera.size_x = screen_size.x;

surv_camera.size_y = screen_size.y;

surv_camera.pos_x = 0;

surv_camera.pos_y = 0;

}

Every surveillance camera (fish.mdl in my example) has this action attached to it:

action surveillance_camera

{

if (my.skill1 + my.skill2 + my.skill3 + my.skill4 + my.skill5 + my.skill6 == 0) {beep; beep; return;}

while (1)

{

my.skill10 = abs(ang(player.pan) - ang(my.pan));

if ((vec_dist(player.x, my.x) < 100) && (my.skill10 > 150) && (my.skill10 < 210))

{

camera.visible = off;

surv_camera.x = my.skill1;

surv_camera.y = my.skill2;

surv_camera.z = my.skill3;

surv_camera.pan = my.skill4;

surv_camera.tilt = my.skill5;

surv_camera.roll = my.skill6;

surv_camera.visible = on;

my.skill11 = 0;

}

else

{

if (my.skill11 == 0)

{

surv_camera.visible = off;

camera.visible = on;

my.skill11 += 1;

}

}

wait (1);

}

}

If we forget to fill in at least one of these skill1...skill6 values, we hear two beeps and then the camera model is removed. We store the distance angle between fish.mdl (camera trigger) and the player in skill10. If the player is closer than 100 quants and the angle stored in skill10 is between 150 and 210 degrees, the player is facing the camera trigger and the corresponding surveillance camera is enabled.

If one of the surveillance cameras is enabled, the camera is hidden and surv_camera is made visible. Surv_camera's coordinates are read from skill1..6 (x, y, z, pan, tilt, roll) so you should get these values in Wed. If you want to deactivate the surveillance camera, simply rotate the player or move back.

If you want to improve this camera code you could add a panel that looks like a TV frame when you are in surveillance mode, play some sounds, etc.


Untitled

Comment tags

Now that's what I call a handy feature! You (the programmer) add a few lines of code (mostly comments) and the rest of the team (you again?) can edit values, choose different sounds, pictures, and so on, without touching a single line of script! Oh, and they'll get detailed instructions too.

Let's take a look at the code that creates a flashing light:

define red_light skill1;
define green_light skill2;
define
blue_light skill3;
define light_range skill4;
define light_on skill5;
define
light_off skill6;

Nothing new so far, right? We are setting new names for the first six skills. Let's move on:

// entry: "light is on" sound
// help: This sound is played when the light is on
sound lighton_wav = <lighton.wav>;

The lines above create a new "entry" that allows you to choose any sound file that exists in the folder, replacing the default "lighton.wav" file. But let's see how this "entry" looks like and how do we get there:

Let's see what each one of the lines above does:

// entry: "light is on" sound creates a new entry for our sound file and places "light is on" sound above the entry. You can create entries for variables, entities, pictures, sounds and strings.
// help: This sound is played when the light is on creates the help text that will be displayed when the user clicks the help button "?" and then clicks the newly created entry;
sound lighton_wav = <lighton.wav>; that's your standard sound definition; it sets the default sound (lighton.wav).

Click the button on the right side of the entry and you will see a list with the sounds that can be loaded, replacing the lighton.wav file.

Let's examine the rest of the tags.wdl script file:

//
title: Flashing light
// image: flashing.pcx
// uses: red_light, green_light,
blue_light, light_range, light_on, light_off
// desc: Set color, range, on/off
intervals for any flashing light
action flashing_light
{
my.invisible = on;
my.passable = on;
if
(my.light_on < 0.1) {my.light_on = 0.1;}
if (my.light_off
< 0.1) {my.light_off = 0.1;}
my.red = my.red_light;
my.green = my.green_light;
my.blue = my.blue_light;
while (1)
{
snd_play
(lighton_wav, 50, 0);
my.lightrange = my.light_range;
sleep (my.light_on);
my.lightrange = 0;
sleep (my.light_off);
}
}

This part of the code changes the aspect of the window that appears when you right click an entity and choose "Behavior".

The first comment sets the title of the window. The second comment displays the picture below the title. The line that "uses" red_light, green_light and so on displays these names on the behavior panel, replacing the rather boring skill1... skill6. You can edit the values and you can "Save" them. The last comment provides the description that appears below the picture, as well as the text that appears when you want to pick an action for a certain entity, and you want to choose help because you don't know what it does.

The code for the flashing light is really simple: it hides the entity, makes sure that light_on and light_off are at least 0.1 and sets the color of the light. The loop plays a sound and switches the range of the light from "Light_range" to zero and back, creating the flashing effect.


Untitled

Time demo

The new time_factor variable has made it much easier for us to control the speed in our games. Do you want to create all those time - related special effects in Max Payne? Piece of cake!

First of all I have created a panel with a slider that can adjust the speed in the game. Do you see how simple it is?

panel
speed_factor
{
bmap = slider_pcx;
layer = 20;
pos_x = 10;
pos_y = 25;
hslider = 10, -12, 200, cursor_pcx, 0, 10, time_factor;
flags = d3d, overlay,
refresh, visible;
}

Time_factor will change its value between 0 and 10 when you adjust the slider. I have placed a gun in the level; make sure that you take a look at the speed of the rockets with time_factor = 0 and time_factor = 10.

But how do we get those speed variation effects? I have provided two examples:

function init_timedemo()
{
while (1)
{
if (key_q == 1)
{
while (key_q == 1) {wait (1);} // wait for the key release
time_factor = 1; // reset it
while (time_factor > 0.01)
{
time_factor -= 0.05 * time;
wait (1);
}
}
if (key_a == 1)
{
while (key_a == 1) {wait (1);} // wait for the key release
time_factor = 1; // reset it
while (time_factor < 10)
{
time_factor += 0.2 * time;
wait (1);
}
}
wait (1);
}
}

When we press the "Q" key time_factor will decrease its value from 1 to 0.01; you'll see the actions running slower and slower. If you press "A", time_factor will grow pretty fast to 10 and all the actions in the level will run faster than you've ever seen them.


Untitled

Timed bomb

Do you know the little house in office.wmp? What if on one of its walls would hang a timed bomb, anxiously waiting to be clicked on with the mouse pointer? Anxiously waiting to destroy everything in the area? What? You have loaded my office.wmp level and you've tested the bomb already? That's cheating, man!

The action attached to timer.mdl is really simple:

action
timer
{
my.skin = 2;
my._DAMAGE = 200; // set
different damage values for different timers
my.enable_click = on;
my.event = start_timer;
}

You can see that I'm using a model with 2 skins; its damage is set to 200 (I'll use some template stuff). The timer is sensitive on clicking; if this happens, its start_timer function is executed:

function
start_timer()
{
wait (1);
second_counter = 10; // explodes
after 10 seconds
while (my != null)
{
second_counter
-= 1;
my.skin = 2;
ent_playsound (my,
timer_snd, 300);
waitt (14);
my.skin = 1;
waitt (2);
if (second_counter
< 1)
{
range = my._DAMAGE * 2;
damage = my._DAMAGE;
temp.pan = 360;
temp.tilt = 180;
temp.z = range;
indicator = _EXPLODE; // not a friendly scan
scan_entity (my.x, temp);
morph (explo_sprite, me);
my.oriented = on;
my.pan = 0;
while (my.frame < 7)
{
my.frame += 3 * time;
wait (1);
}
actor_explode(); // remove the explosion sprite
}
}
}

Second_counter holds the number of seconds before the explosion starts; I have set it to 10 seconds in my example. As long as the timer hasn't exploded (my != null) the timer will play a sound and change between its skins (1 and 2) every second because waitt (14) + waitt (2) = waitt (16) = 1 second. When the second_counter has decreased to 0, the timer will explode, morphing itself into an oriented sprite because the player needs to see a good looking explosion. The explosion sprite passes through its animation frames and then it is removed.

You can improve this timer code by allowing the player to select the number of seconds on a panel, creating timed bombs like this one as "gun" bullets and so on.


Untitled

Timer

If you want to create a racing game you will certainly need a good timer for it. The function start_counter() will run as soon as you start the level; let's take a look at the code:

string temp_str;
string counter_str;

function
start_counter()
{
str_cpy (counter_str, "0.000"); // show this string at start
while (1)
{
if (start == 1)
{
current_ticks = total_ticks - temp_ticks;
seconds = (current_ticks / 16) % 1000;
miliseconds = (current_ticks * 1000 / 16) % 1000;
str_for_num (temp_str, seconds);
str_cpy (counter_str, temp_str);
str_cat (counter_str, ".");
str_for_num (temp_str, miliseconds);
str_trunc (counter_str, 1);
}
else
{
temp_ticks = total_ticks;
}
wait (1);
}
}

function
toggle_counter()
{
start = (start == 0);
}

on_s = toggle_counter;

First of all, we copy "0.000" in the string that will be used to display the time; it is always nice to show the counter this way before the race starts. Start = 1 should be set to 1 by our racing game at game start, but I have created a small function (toggle_counter) that sets start to 1 or 0 when the "S" key is pressed.

We will use current_ticks to store the number of frames (ticks) that pass from the moment we started the counter. I'll use some numbers in my example to make the things easier for you.

Let's say that the games has started 10 seconds ago and your frame rate is 60 fps. This means that at this point, total_ticks = 10 seconds * 60 fps = 600, because total_ticks counts the number of frames that have passed since your game has started. Temp_ticks is zero at game start, so current_ticks = 600. We compute the number of seconds and miliseconds, we convert them to strings and we store them in counter_str this way: seconds.miliseconds. If we press "S" again, start = 0 and the "else" branch will run. We know that at this point total_ticks = 600 so temp_ticks (a temporary variable) will be set to 600 too.

Total_ticks keeps counting and counting even if we pause the game, load, save, etc - we can't stop it! When we press "S" again to start the timer, start = 1 and current_ticks = total_ticks - temp_ticks = 0, because the "else" branch copies total_ticks in temp_ticks when the timer is stopped. This way our counter will start from zero every time we reset it.

The time is displayed on the screen using a simple text definition:

text counter_pan
{
layer = 20;
pos_x = 0;
pos_y = 0;
font = speed_font;
string = counter_str;
flags = visible;


Untitled

Trap

This snippet was created for you, the users who liked my "intelligent music" article from Aum38. It's a gate that closes behind player's back as soon as he steps over one of the small triggers that are placed on the ground.

action trap_trigger
{
while (player == null) {wait (1);}
while (vec_dist (player.x, my.x) > 50) {wait (1);} // play with 50
player_close = 1;
}

The triggers wait until the player is created, and then they wait until the distance between them and the player goes below 50 quants. This is a critical value that depends on player's height and the z position of the triggers, so you should play with it. If the player steps over one of the triggers, a variable named "player_close" is set to 1.

action
trap_player
{
while (player_close == 0) {wait (1);}
snd_play (gate_wav, 100, 0);
while (my.z > 170)
{
my.z -= 30 * time;
wait (1);
}
}

The action above is attached to the gate entity; it waits until player_close is set to 1, plays the gate_wav sound, and then decreases its z until it goes below 170 quants (experimental value). Make sure that the triggers aren't too close to the gate; otherwise, the player might escape. Well, you could also increase the falling speed by replacing 30 * time with 50 * time or 100 * time, depending on your needs.


Untitled

Poor man's underwater effect

Ok, so we know how to create a good looking water surface. But what's happening when the player goes under it? Will he or she see the same, nice looking waves? This article will describe a simple underwater effect that works with any engine edition. The idea is simple: we monitor player's height in a while (1) loop, and if it goes below a certain value we put its head inside a blue, rotating sphere model :)

starter create_water()
{
sleep (1); // wait until the level is loaded
ent_create (sphere_mdl, nullvector, move_sphere);
}

It all begins with a starter function that waits until the level is loaded, and then it creates a sphere and attaches it the move_sphere() function:

function
move_sphere()
{
while (player == null) {wait (1);}
proc_late();
my.passable = on;
my.transparent
= on;
my.unlit = on;
my.bright = on;
my.ambient = -100;
my.alpha = 90; // play with this value
while (1)
{
my.x =
player.x;
my.y = player.y;
my.z = player.z + 40; // 40 = experimental value
my.pan = sin(total_frames * water_speed) * amplitude;
my.tilt = sin(total_frames * water_speed) * amplitude;
my.roll = cos(total_frames * water_speed) * amplitude;
if (my.z < -38) // experimental value, depends on the height of the water surface
{
my.invisible = off;
}
else
{
my.invisible = on;
}
wait(1);
}
}

This function waits until the player is loaded and then makes the sphere model passable, transparent, unlit and bright. I chose a negative ambient, but you can also darken the skin in a painting program. You will also want to play with the transparency factor - alpha. The sphere is kept 40 quants above player's head; that's an experimental value and it depends on the height of your player.

I am using the sin and cos functions to rotate the sphere around; feel free to use any combination of sin and cos for the angles, and don't forget to tweak the values for water_speed and amplitude. Like I said, the code checks if player's z is below 38 quants or not, making the sphere model visible or not. This value depends on the height of the water surface and needs to be changed according to your needs.

I have used a simple, transparent sprite for the water surface, but you can use any other method, including the terrain-based water system that was presented in AUM a while ago.


Untitled

Vertex - based effects

This special effect looks so cool! You can use it for teleports, weird looking monsters, etc. The idea behind it is simple: every vertex can become a particle generator:

The action attached to the model needs to run a function that initializes the code; I have applied this vertex based effect on the model that uses patrol_path in the office demo and here's the relevant change in the patrol_path action:

action
patrol_path
{
actor_init();
init_vertex(); // new line
// attach next path
temp.pan = 360;
temp.tilt = 180;
temp.z = 1000;
result = scan_path(my.x,temp);
if (result == 0) { my._MOVEMODE = 0; } // no path found
...................
}

You can see that the only change to the code is the red line of code that calls init_vertex(). I have included the modified actors.wdl file but make sure that you back up your actors.wdl file first before overwriting it with my file. By the time you are reading this article the new templates could be out and you might loose some of their functionality if you decide to overwrite the file.

Simply add the red line of code to any of your actions that is attached to a model and you will be set. Let's see what function init_vertex() does:

function
init_vertex()
{
var vertices; // local var
my.invisible = on;
while (1)
{
vertices = ent_vertices
(my);
while (vertices
> 0)
{
vec_for_vertex (temp, my, vertices);
effect (attach_flares, 1, temp, normal);
vertices -= 1; /
}
wait (1);
}
}

We define a local var named vertices and then we hide the model; of course that you can show the model too but I think that the effect looks better if you hide the model. We get the number of the vertices for the model and then we get the coordinates for every vertex inside a while loop without wait(1) because the loop needs to update the vertex coordinates as fast as possible. We use the same while loop to create a flare effect and then we mode to the next vertex (we are counting downward).

Function attach_flares is a standard particle function:

function
attach_flares()
{
my.bmap = flare_pcx;
my.flare = on;
my.bright = on;
my.size = 10;
my.function = remove_flares;
}

function remove_flares()
{
my.lifespan = 0; // remove the flare particle
}

The particles use the flare_pcx bitmap with its flare and bright flags set; its size is 10. The function that drives the flares (remove_flares) removes them instantly; ok, you've caught me, the particles will live for a frame anyway. You can use a bigger value for lifespan if you want to create motion trail effects, etc but when (and if) you do that, keep an eye on the particle counter that appears on the debug panel.


Untitled

Waterfall

I think that a good waterfall can be created using the following recipe: mix a transparent wmb entity with some code that shifts the texture on it and generates some particles. That's exactly what I'm going to do.

action
waterfall
{
my.transparent = on;
while (1)
{
my.v -= 30 * time;

Action waterfall is attached to the waterfall wmb entity. You can see that the entity is transparent and the texture on it is moving because we change its v parameter; let's take a look at the entity the way we see it in Wed:

I can hear you asking: what's so special about this entity?

Well, we need to know the width of the waterfall (130 quants in my example) and me must place its origin the way you see it in the Back view. This way the particles will be created in the region that separates the waterfall and the water pool without having to write complicated code / adding more entities as particle generators.

          vec_set (temp,
my.pos);
temp.y += 60 -
random(120);
effect (water_splash,
1, temp, normal);
wait (1);
}
}

We use temp to store the position of the waterfall entity and then we add a random value between -60 and 60 quants to the waterfall entity on y. This will create several water_splash particle generators with random positions on y. The particles are produced using the common "effect" instruction, with 1 particle being created every frame. I haven't limited the number of splashes: if your PC has 100 fps you will get 100 particles every second. This way weaker PCs will get the chance to display as many splashes as they can. If you want, you can emit several particles at once and use sleep(x) instead of wait (1) inside the loop.

Let's see the function that generates the particles:

function
water_splash()
{
temp.x = random(4) - 2;
temp.y = random(2) - 1;
temp.z = random(1) + 1;
vec_add (my.vel_x, temp);
my.alpha = 10 + random(10);
my.bmap = water_pcx;
my.size = 20 + random(10);
my.flare = on;
my.move = on;
my.bright = on;
my.lifespan = 200;
my.function = splash_fade;
}

The particles have a small random speed on x (-2..2) and y (-1..1) and a positive speed on z (1..2) because they will rise a little bit. You can see that the particles start their life with a random alpha and size and with a lifespan of 200 ticks. The function that drives every particle is presented below:

function splash_fade()
{
my.size += 5 * time;
my.alpha -= 1 * time;
if (my.alpha < 0)
{
my.lifespan = 0;
}
}

The particle increases its size and decreases its alpha during its lifetime. When the particles has become invisible (alpha < 0) it is removed because we don't want to waste resources for nothing.

You should be able to create great looking waterfalls using this piece of code as an example. You can choose from a number of different splash bitmaps depending on a random value, play with alpha, size, flags and so on. Good luck!


Untitled

Water drops

This effect could add a lot of value to your game. I would use it inside my cave levels, for broken pipes and (why not?) for rain!

My example shows drops falling from a broken pipe and producing ripples when they impact with the water pool below. We need to place a drop generator inside our level and this is the action that must be attached to it:

action drop_generator
{
my.invisible = on;
my.passable = on;
while (1)
{
ent_create(drop_mdl,
my.pos, falling_drop);
sleep (4 + random(2));
}
}

The drop generator will be made invisible and passable so we can use any model for it; I have used the drop model itself. This generator creates a drop at its position every 4...6 seconds. The drop that is created will run the function named falling_drop:

function falling_drop()
{
my.flare = on;
vec_set(temp, my.pos);
vec_set(drop_height, my.pos);
temp.z -= 5000; // trace
5000 quants below the drop generator
trace_mode = ignore_me + ignore_models + ignore_sprites;
drop_height.z = my.z - trace(my.x,
temp) + 2; // place the ripple sprite 2 quants above the water surface
while (my.z > drop_height.z)
{
my.z -= 3 * time;
wait (1);
}
ent_create(ripple_pcx, my.pos, ripple_effect);
ent_remove(me); // remove
the drop
}

The drop model will have its "flare" flag set. We trace between the current position of the drop and a position that is placed 5000 quants below, storing the result (the height of the water surface) in drop_height.z. We add two quants to the result because we want to place the sprite used for the ripple above the water surface.

We make the drop fall by changing its z inside a while loop and when it has the same height with the water surface we create the ripple effect (using the sprite named ripple_pcx) and then we remove the drop.

function
ripple_effect()
{
ent_playsound (my, drip_wav, 500);
my.oriented = on;
my.flare = on;
my.alpha = 60;
my.tilt = 90;
while (my.scale_x < 5)
{
my.scale_x += 0.03
* time;
my.scale_y = my.scale_x;
if (my.scale_x
> 3)
{
my.alpha -= 1 * time;
}
wait (1);
}
ent_remove(me); // remove the sprite
}

We play the drip_wav sound, we set the "oriented" and "flare" flags for the sprite and we choose the proper "tilt" angle and "alpha" value for it. We start to increase the size of the sprite in a while loop, decreasing its alpha factor when its scale is over three. When the scale of the sprites has grown over five, we remove it.


Untitled

Realistic water

This time we will learn to create a water effect that looks pretty good. Take a look at the picture below to see what I mean.

First of all we create a terrain in Med. I have used the default values (33 x 33 vertices, 5 quants / triangle) and I have pulled down the first vertex of the terrain, just like in the picture below:

Remember to do that for every terrain that will be used for water; otherwise, the vertices of the terrain will not be able to move at all. Choose any skin that looks good for your terrain and you're ready to go!

define amplitude skill1;
define water_speed skill2;

define number_of_vertices 1089;

var
vertex_array [number_of_vertices];
var counter;
var index;

My terrain has 33 * 33 = 1089 vertices; change this value if your terrain has a different size. I have defined an array that will store a different value for every vertex and two variables that will be used as counters.

action nice_waves
{
my.transparent = on;
my.passable = on;
my.alpha = 75;
my.ambient
= -100;
if (my.amplitude == 0)
{
my.amplitude = 2; // default wave amplitude value
}
if (my.water_speed == 0)
{
my.water_speed = 10; // default wave speed value
}

The terrain is made transparent and passable; its transparency factor is set to 75 but you can use any other value here. The skin used in my demo wasn't too dark so I have set ambient to a negative value. If you forget to set skill1 in Wed, amplitude will be set to 2; if you forget about water_speed, it will default to 10.

    while (counter < number_of_vertices)
{
vertex_array[counter] = random(360); // set
random values in vertex_array
counter += 1;
}

TThe first while loop fills vertex_array with random values; here's a possible result for my.amplitude = 5:

We are going to use the sine function for our waves because it looks good and it is really simple to use. I'll give you a quick explanation about it.

First of all you will notice that this function looks like a wave - that's a good sign! You can see that no matter how big is x, the values of the sine function will range from -1 to 1. Our waves will have an amplitude given by this sine value (from -1 to 1) multiplied with my.amplitude (skill1) set in Wed. If I set amplitude = 5 in Wed, the vertices will change their z values from -5 to 5, get it?

We have generated random values as starting points for our waves; take a look at the picture below to see how they look on the sine wave.

I couldn't draw all the points because there are 1089 values on this sine wave! Every vertex will start from its own value stored in vertex_array and its z coordinate (its height) will change continuously as indicated in the picture above. Let's get back to our action now:

  while (1)
{
index = 0;
while (index < number_of_vertices)
{
vec_for_mesh(temp, my, index); // store the vertex coordinates in temp
temp.z = sin(counter + vertex_array[index])
* my.amplitude; // change the z component
vec_to_mesh(temp, my, index); // deform the terrain entity
index += 1;
}
counter += my.water_speed * time;
wait(1);
}
}

We start with the first vertex; we store its coordinates in temp using vec_for_mesh, we set temp.z to the new value given by our sine function multiplied with my.amplitude and then we change the height of the vertex using vec_to_mesh. We repeat the process for all the vertices, without using a wait (1) instruction in the while loop because we would need 1089 frames (5...20 seconds) to pass through all the vertices. This is one of those situations when we can use a while loop without "wait".

We get out of the inner while loop as soon as all the vertices are set to their new heights, and then we increase the counter by adding the time corrected water_speed to it. This moves all the vertices to the right along the sine function graphic; like I said, we can increase counter over and over because the sine function won't go below -1 or above 1.

That's the end of the article! Don't forget to set the proper scale for your terrain in Wed.


Untitled

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 :)


Untitled

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.