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.

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.