2D Games

Top  Previous  Next

This month we are going to play with three different projects. Let's start by opening and running 2dspace1.c:

 

aum82_workshop1

 

It's not something to brag about at a party but we've got to start somewhere, right? Move the ship around using the arrow keys; as you can see, diagonal movement is supported.

 

#include <acknex.h>

#include <default.c>

 

BMAP* ship_tga = "ship.tga";

 

PANEL* ship_pan =

{

       bmap = ship_tga;

       layer = 15;

       pos_x = 300;

       pos_y = 250;

       flags = VISIBLE;

}        

 

function main()

{

       fps_max = 60; // limit the frame rate to 60 fps

       video_mode = 8; // run at 1024 x 768 pixels

       video_screen = 1;

       vec_set(screen_color, vector(0, 0, 0)); // make the background color black

}

 

We start by defining the bitmap that will be used for our ship_pan panel. Function main limits the frame rate to 60 frames per second, tells the game to run at a 1024 x 768 pixels resolution in full screen mode and sets a black color for the background.

 

function ship_startup()

{

       VECTOR ship_speed;

       var keys_pressed;

       ship_pan.center_x = ship_pan.size_x * 0.5; // set the rotation point at the center of the panel

       ship_pan.center_y = ship_pan.size_y * 0.5;

       while (1)

       {

               ship_speed.x = 15 * (key_cur - key_cul) * time_step;

               ship_speed.y = 15 * (key_cud - key_cuu) * time_step;

               ship_pan.pos_x += ship_speed.x;

               ship_pan.pos_y += ship_speed.y;

               keys_pressed = 0;

               if (key_cur) keys_pressed += 1;

               if (key_cul) keys_pressed += 2;

               if (key_cud) keys_pressed += 4;

               if (key_cuu) keys_pressed += 8;

 

The first few lines of code define a vector and a var, and then set the rotation center for our ship panel to the center of the bitmap. We are using a single bitmap for all the 8 ship orientations, setting a proper angle for our panel through coding; most of the other 2D engines need a separate bitmap for each angle, just like in the pictures below:

 

aum82_workshop13 aum82_workshop4

 

It's good to know that we don't have to go through all this pain, right? Let's get back to our function: it controls the ship by adding a number of pixels to its pos_x and / or pos_y coordinates, depending on how long the cursor keys are pressed (15 controls the movement speed). I have defined a variable named "keys_pressed"; it is set to 0 each frame, and then changes its value depending on what keys are pressed. I have used numbers that are powers of two because this way I can create unique combinations of keys; a value of (let's say) 9 can only be achieved by pressing the "1" (key_cur) and "8" (key_cuu) buttons, so I won't have problems identifying which keys were pressed.

 

               if (keys_pressed == 1) // only the cursor right key is pressed?

               {

                       ship_pan.angle = 180;

               }

               if (keys_pressed == 2) // only the cursor left key is pressed?

               {

                       ship_pan.angle = 0;

               }

               if (keys_pressed == 4) // only the cursor down key is pressed?

               {

                       ship_pan.angle = 90;

               }

               if (keys_pressed == 8) // only the cursor up key is pressed?

               {

                       ship_pan.angle = 270;

               }

               if (keys_pressed == 5) // the right and down keys are pressed?

               {

                       ship_pan.angle = 135;

               }

               if (keys_pressed == 6) // the left and down keys are pressed?

               {

                       ship_pan.angle = 45;

               }

               if (keys_pressed == 9) // the right and up keys are pressed?

               {

                       ship_pan.angle = 225;

               }

               if (keys_pressed == 10) // the left and up keys are pressed?

               {

                       ship_pan.angle = 315;

               }

               wait (1);

       }

}

 

The rest of the code does just that; it checks which keys are pressed and sets a proper angle for our ship panel. Now let's try to improve this example and create something better; open 2dspace2.c and run it:

 

aum82_workshop2

 

This looks more like a game: we've got a nice background, our ship moves smoother using acceleration and friction and we've also got a grey, bouncing alien container that changes its color to red and decreases the score when it collides with the player.

 

aum82_workshop3

 

The enemy container will move faster and faster as the time passes, so it will be harder and harder to avoid it. Let's see the code that makes it tick:

 

#include <acknex.h>

#include <default.c>

 

var players_score = 0;

 

BMAP* ship_tga = "ship.tga";

BMAP* enemy1_tga = "enemy1.tga";

BMAP* enemy2_tga = "enemy2.tga";

 

FONT* arial_font = "Arial#20";

 

First of all, we've got a few bitmap definitions for our ship and for our grey / red alien container. We are also defining a true type font that will be used for the score.

 

PANEL* score_pan =

{

       layer = 20;

       digits (10, 10, "Score: %.f", arial_font, 1, players_score);

       flags = visible;

}

 

PANEL* background_pan =

{

       bmap = "background.tga"; // this bitmap has 1024 x 768 pixels

       layer = 10;

       pos_x = 0;

       pos_y = 0;

       flags = VISIBLE;

}        

 

PANEL* ship_pan =

{

       bmap = ship_tga;

       layer = 15; // the ship panel appears over the background panel

       pos_x = 300;

       pos_y = 250;

       flags = VISIBLE;

}        

 

PANEL* enemy1_pan =

{

       bmap = enemy1_tga;

       layer = 15;

       pos_x = 100;

       pos_y = 150;

       flags = VISIBLE;

}        

 

function main()

{

       fps_max = 60; // limit the frame rate to 60 fps

       video_mode = 8; // run at 1024 x 768 pixels

       video_screen = 1;

       vec_set(screen_color, vector(0, 0, 0)); // make the background color black

}

 

Then, we've got panel definitions for the score (we are using a simple "digits" instruction for that), for the background, the player and enemy container. Function main doesn't do anything special.

 

function ship_startup()

{

       VECTOR ship_speed;

       var hspeed = 0; // stores the horizontal speed

       var vspeed = 0; // stores the vertical speed

       var keys_pressed;

       ship_pan.center_x = ship_pan.size_x * 0.5; // set the rotation point at the center of the panel

       ship_pan.center_y = ship_pan.size_y * 0.5;

       while (1)

       {

               vec_set(ship_speed.x, accelerate (hspeed, 9 * (key_cur - key_cul), 0.3)); // 9 gives the acceleration

               vec_set(ship_speed.y, accelerate (vspeed, 9 * (key_cud - key_cuu), 0.3)); // 0.3 gives the friction

               ship_pan.pos_x += ship_speed.x;

               ship_pan.pos_y += ship_speed.y;

 

This time we are using "accelerate" to move our ship using acceleration and friction; feel free to play with 9 and 0.3 if you want to control the acceleration and the friction. The rest of the code uses the same keys_pressed variable to set a proper angle for the ship panel, just like we did it in our previous example.

 

               keys_pressed = 0;

               if (key_cur) keys_pressed += 1;

               if (key_cul) keys_pressed += 2;

               if (key_cud) keys_pressed += 4;

               if (key_cuu) keys_pressed += 8;

               if (keys_pressed == 1) // only the cursor right key is pressed?

               {

                       ship_pan.angle = 180;

               }

               if (keys_pressed == 2) // only the cursor left key is pressed?

               {

                       ship_pan.angle = 0;

               }

               if (keys_pressed == 4) // only the cursor down key is pressed?

               {

                       ship_pan.angle = 90;

               }

               if (keys_pressed == 8) // only the cursor up key is pressed?

               {

                       ship_pan.angle = 270;

               }

               if (keys_pressed == 5) // the right and down keys are pressed?

               {

                       ship_pan.angle = 135;

               }

               if (keys_pressed == 6) // the left and down keys are pressed?

               {

                       ship_pan.angle = 45;

               }

               if (keys_pressed == 9) // the right and up keys are pressed?

               {

                       ship_pan.angle = 225;

               }

               if (keys_pressed == 10) // the left and up keys are pressed?

               {

                       ship_pan.angle = 315;

               }

 

Please note the "clamp" instructions below; they don't allow the player to move outside the screen boundaries. The big "if" branch tests if the ship and the enemy have collided. If a collision has occurred, we change the enemy panel bitmap to the red one and we subtract 10 points from player's score each frame; otherwise, we set the proper (grey) bitmap for our enemy.

 

               ship_pan.pos_x = clamp(ship_pan.pos_x, 0, 940);

               ship_pan.pos_y = clamp(ship_pan.pos_y, 0, 680);

 

               if ((((ship_pan.pos_x + 67 >= enemy1_pan.pos_x) && (ship_pan.pos_x + 67 <= enemy1_pan.pos_x + 37)) ||

               ((enemy1_pan.pos_x + 37 >= ship_pan.pos_x) && (enemy1_pan.pos_x + 37 <= ship_pan.pos_x + 67)))

               && (((ship_pan.pos_y + 67 >= enemy1_pan.pos_y) && (ship_pan.pos_y + 67 <= enemy1_pan.pos_y + 37)) ||

               ((enemy1_pan.pos_y + 37 >= ship_pan.pos_y) && (enemy1_pan.pos_y + 37 <= ship_pan.pos_y + 67))))

               {

                       enemy1_pan.bmap = enemy2_tga;

                       players_score -= 10; // the player and the enemy have collided here

               }

               else        

               {

                       enemy1_pan.bmap = enemy1_tga;

               }

               wait (1);

       }

}

 

Collision in 2D is a key concept when it comes to 2D game programming, so let's take some time and discuss about it. First of all, there isn't any magical function that checks if two 2D objects have collided; we have to write it. Let's take a potential situation from our demo as an example:

 

aum82_workshop5

 

If the two objects have collided, their bitmaps overlap. If we would test their pixels one by one for collisions, we would waste a lot of precious CPU resources for this simple task; therefore, we are going to use a simple, much faster method that is preferred by 99% of game developers: bounding box collision detection.

 

Let's imagine that our sprites are in fact rectangles (they are rectangular sprites indeed, btw). This means that if our player and enemies don't have a rectangular shape, our code could register collisions that didn't actually happen, just like in the picture below.

 

aum82_workshop6

 

Well, this is a small price to pay and there's a way to minimize its impact, as you will see in the following game demo. But before going there, let's see how we can check if our objects have collided in 2D; let's examine what's happening on the x axis for now:

 

aum82_workshop7

 

As you can see, only the enemy in the center has collided with the player. This has happened because its panel's pos_x has a value that's closer to player's pos_x. We need to take into account the size of the two bitmaps, so we could use something like this to check if the player has collided with the enemy:

 

((ship.pos_x + ship_width >= enemy.pos_x) && (ship.pos_x + ship_width < enemy.pos_x + enemy_width)

 

The line above has checked if the right side of the player panel has collided with the enemy or not; we have to take into account a potential collision with the left side of the player panel as well:

 

((enemy.pos_x + enemy_width >= ship.pos_x) && (enemy.pos_x + enemy_width <= ship.pos_x + ship_width))

 

Any of these situations could trigger the collision, so we have to use the OR operator and connect them:

 

((ship.pos_x + ship_width >= enemy.pos_x) && (ship.pos_x + ship_width < enemy.pos_x + enemy_width) ||

((enemy.pos_x + enemy_width >= ship.pos_x) && (enemy.pos_x + enemy_width <= ship.pos_x + ship_width)))

 

And now let's take a look at the code that deals with the collisions on the x axis, as it appears in our game demo:

 

if ((((ship_pan.pos_x + 67 >= enemy1_pan.pos_x) && (ship_pan.pos_x + 67 <= enemy1_pan.pos_x + 37)) ||

((enemy1_pan.pos_x + 37 >= ship_pan.pos_x) && (enemy1_pan.pos_x + 37 <= ship_pan.pos_x + 67)))

 

That's how it works! A similar mechanism is used to test for collisions on the y axis:

 

aum82_workshop8

 

(((ship.pos_y + ship_width >= enemy.pos_y) && (ship.pos_y + ship_width <= enemy.pos_y + enemy_width)) ||

((enemy.pos_y + enemy_width >= ship.pos_y) && (enemy.pos_y + enemy_width <= ship.pos_y + ship_width)))

 

We can tell for sure that the enemy and the player have collided if the statements are valid on both axis at the same time and this is exactly what the code from our game demo does:

 

if ((((ship_pan.pos_x + 67 >= enemy1_pan.pos_x) && (ship_pan.pos_x + 67 <= enemy1_pan.pos_x + 37)) ||

((enemy1_pan.pos_x + 37 >= ship_pan.pos_x) && (enemy1_pan.pos_x + 37 <= ship_pan.pos_x + 67)))

&& (((ship_pan.pos_y + 67 >= enemy1_pan.pos_y) && (ship_pan.pos_y + 67 <= enemy1_pan.pos_y + 37)) ||

((enemy1_pan.pos_y + 37 >= ship_pan.pos_y) && (enemy1_pan.pos_y + 37 <= ship_pan.pos_y + 67))))

{

       // the ship and the enemy have collided here        

}

 

The last function from the second demo is the one that controls the enemy:

 

function enemy1_startup()

{

       var speed_x = 10;

       var speed_y = 5;

       var sense_x = 1;

       var sense_y = 1;

       while (1)

       {

               enemy1_pan.pos_x += speed_x * sense_x * time_step;

               if ((enemy1_pan.pos_x < 0) || (enemy1_pan.pos_x > 987))

               {

                       players_score += 10 + integer(random(50));

                       sense_x *= -1;

                       speed_x += 1 + random(1);

                       speed_x = minv(speed_x, 50);

               }

               enemy1_pan.pos_y += speed_y * sense_y * time_step;

               if ((enemy1_pan.pos_y < 0) || (enemy1_pan.pos_y > 730))

               {

                       sense_y *= -1;

                       speed_y += speed_x / 2 - random(speed_x);

                       speed_y = minv(speed_y, 50);

               }

               enemy1_pan.pos_x = clamp(enemy1_pan.pos_x, 0, 988);

               enemy1_pan.pos_y = clamp(enemy1_pan.pos_y, 0, 731);

               wait (1);

       }

}

 

We are using different variables to control the speed on the x and y axis, as well as variables that control the direction (sense_x and sense_y). If the enemy panel has reached the left or right screen borders, we add a random number of points to player's score and we change the direction of the movement for the enemy, multiplying its speed by -1. We are also adding +1...+2 to its speed at each collision. We have to limit speed_x, of course; I chose a value of 50.

 

The same thing happens on the y axis; the enemy moves until it hits the upper or lower border of the screen, changing its direction and increasing its speed on the y axis with a value that depends on speed_x. The last 2 lines of code make sure that the enemy doesn't exceed the boundaries of the screen (extra safety measures are always good when you are dealing with objects that move at huge speeds).

 

I couldn't end this month's 2D workshop without presenting you an Invaders clone; load 2dspace3.c and run it:

 

aum82_workshop9

 

It's an improved version of the original, with 50 bitmapped enemies that fire bullets trying to destroy player's ship, a scrolling background, and so on.

 

aum82_workshop10

 

The player will also die if at least one of the enemies reaches the bottom of the screen, just like in the original Invaders game:

 

aum82_workshop11

 

Nevertheless, the game is quite easy to beat; kill all the enemies and you will get 1000 points.

 

aum82_workshop12

 

Here's the content of the 2dspace3.c file:

 

#include <acknex.h>

#include <default.c>

 

var players_score = 0;

var players_health = 100;

var i = 0; // used as an index for the enemies

 

BMAP* ship_tga = "ship.tga";

BMAP* shiphit_tga = "shiphit.tga";

BMAP* pbullet_tga = "pbullet.tga";

BMAP* enemy_tga = "enemy.tga";

BMAP* explo1_tga = "explo1.tga";

BMAP* explo2_tga = "explo2.tga";

BMAP* explo3_tga = "explo3.tga";

BMAP* explo4_tga = "explo4.tga";

 

FONT* arial_font = "Arial#16b";

 

#define player_width 50

#define player_height 60

#define enemy_width 51

#define enemy_height 64

#define bullet_width 5

#define bullet_height 11

#define ebullet_width 7

#define ebullet_height 11

#define bullets_freq 0.993

 

This time, we are using two bitmaps for player's ship: the normal bitmap and the "ship was hit" bitmap. The enemies use a single bitmap and we're also using 4 different bitmaps for the explosion frames. I have defined the width and height of the bitmaps that are used by the player, the enemies and their bullets. The values that are used for the player are a bit smaller than the real size of the bitmap; this way, we get more accurate bounding box collisions. The last "define" line gives the firing rate for the enemies; a value of 1 will stop the firing, while smaller values will make the enemies fire more often. Feel free to play with 0.993 until you get the desired result.

 

SOUND* engage_wav = "engage.wav";

SOUND* plfire_wav = "plfire.wav";

SOUND* enemyexplo_wav = "enemyexplo.wav";

SOUND* enemyfire_wav = "enemyfire.wav";

SOUND* playerhit_wav = "playerhit.wav";

 

PANEL* enemies[50];

PANEL* enemy_bullets[50];

PANEL* temp_bullet;

 

The code above defines the sound effects and several panel pointers. The first panel pointer definition generates an arrays of 50 panel pointers that will store the name of the enemies: enemies[0] will be the panel pointer that's used for the first enemy, enemies[1] will be the panel pointer that's used for the second enemy, and so on.

 

The second panel pointer array will be used for the bullets that are fired by the enemies; enemy_bullets[0] will be used for the bullet that's been fired by the first enemy, and so on (each enemy can fire a single bullet at a time). Finally, the last panel pointer definition will be used for player's bullets.

 

PANEL* score_pan = // displays the score

{

       layer = 30;

       digits (10, 0, "Score: %.f", arial_font, 1, players_score);

       flags = VISIBLE;

}

 

PANEL* background1_pan = // displays the black starry background

{

       bmap = "background.tga"; // this bitmap has 1024 x 768 pixels

       layer = 10;

       pos_x = 0;

       pos_y = 0;

       flags = VISIBLE;

}        

 

PANEL* background2_pan = // displays the black starry background

{

       bmap = "background.tga"; // the bitmap is identical with the one above

       layer = 10;

       pos_x = 0;

       pos_y = -769;

       flags = VISIBLE;

}        

 

PANEL* ship_pan = // displays player's ship

{

       bmap = ship_tga;

       layer = 30; // the ship panel appears over the background panel

       pos_x = 500;

       pos_y = 700;

       flags = VISIBLE;

}        

 

PANEL* health_pan = // displays the red horizontal health bar

{

       bmap = "health.tga";

       pos_x = -12;

       pos_y = 0;

       layer = 25;

       flags = VISIBLE;

}

 

We are defining a panel for the score, two panels for the background, a panel for player's ship and another one for the red health bar at the top of the screen.

 

function main()

{

       randomize(); // generate random series of numbers at each game run

       fps_max = 60; // limit the frame rate to 60 fps

       video_mode = 8; // run at 1024 x 768 pixels

       video_screen = 1; // start in full screen mode

       vec_set(screen_color, vector(0, 0, 0)); // make the background color black

       media_loop("soundtrack.wav", NULL, 30); // start the soundtrack

       background(); // initialize the scrolling background stars

       players_ship(); // start player's function

       create_aliens(); // create the enemies

}

 

Function main does the usual things, and then starts a soundtrack that plays in a loop and calls 3 functions that deal with the background, the player and the enemies.

 

function background()

{

       while (1)

       {

               background1_pan.pos_y += 3 * time_step;

               background2_pan.pos_y = background1_pan.pos_y - 769;

               if (background1_pan.pos_y > 768)

               {

                       background1_pan.pos_y -= 768;

               }

               health_pan.scale_x = maxv(0.01, players_health / 100);

               wait (1);

       }

}

 

Function background( ) takes care of the scrolling background. I have used two panels that move downwards by increasing their pos_y values; in fact, the second panel is dragged 769 pixels behind the first panel. As soon as the first panel has disappeared at the bottom of the screen, it is moved to the top of the screen again, creating a seamlessly scrolling background. We've got a while (1) loop running there, so I have used it to scale player's health panel (the red bar at the top of the screen) as well, depending on the players_health value.

 

function players_ship()

{

       VECTOR ship_speed;

       var hspeed = 0; // stores the horizontal speed

       while (players_health > 0)

       {

               vec_set(ship_speed.x, accelerate (hspeed, 9 * (key_cur - key_cul), 0.3));

               ship_pan.pos_x += ship_speed.x;

               ship_pan.pos_x = clamp(ship_pan.pos_x, 0, 964);

               if ((key_space) && (!temp_bullet))

               {

                       snd_play(plfire_wav, 100, 0);

                       temp_bullet = pan_create("bmap = pbullet.tga; NULL;", 35);

                       // player_width is a bit smaller than normal, so we add a few more pixels to pos_x

                       temp_bullet.pos_x = ship_pan.pos_x + player_width / 2 + bullet_width / 2;

                       temp_bullet.pos_y = 695;

                       temp_bullet.flags |= VISIBLE;

               }

               if (temp_bullet)

               {

                       if (temp_bullet.pos_y > 5)

                       {

                               temp_bullet.pos_y -= 10;

                       }

                       else

                       {

                               reset(temp_bullet, VISIBLE);

                               temp_bullet = NULL;

                       }

               }

               wait (1);

       }

 

The function that controls player's ship is quite simple; we are using a loop that runs for as long as the player is alive, moving it with acceleration and friction. This time, player's ship only moves horizontally, so a single "clamp" instruction keeps it in the useful area. If the player presses the "space" key and the "temp_bullet" panel pointer is null (the player didn't fire a bullet already), we create a panel named temp_bullet, we place it at its proper position and then we make it visible.

 

If the bullet exists and it didn't hit the upper border of the screen (its pos_y is greater than 5), we move the bullet upwards by 10 pixels each frame. Finally, if the bullet has reached the top of the screen, we hide it and we free the pointer, allowing the player to fire a new bullet if he / she wants to do that.

 

       snd_play(enemyexplo_wav, 100, 0); // let's play a few explosion sounds

       ship_pan.bmap = explo1_tga; // and change the ship bitmap to several explosion bitmaps

       wait (6);

       ship_pan.bmap = explo2_tga;

       wait (6);

       ship_pan.bmap = explo3_tga;

       wait (6);

       snd_play(enemyexplo_wav, 100, 0);

       ship_pan.bmap = explo4_tga;

       wait (6);

       snd_play(enemyexplo_wav, 100, 0);

       reset(ship_pan, VISIBLE); // now let's hide the ship

       ship_pan.pos_y = -1000; // and move it outside the screen (so that no collisions are triggered for it anymore)

}

 

The player is dead here, so we play a few explosion sounds and we replace the ship panel bitmap with our explosion bitmaps. We hide the ship and we move it 1000 pixels above the screen; this way, the enemies won't be able to hit player's ship anymore.

 

function create_aliens()

{

       var temp1 = -52;

       var temp2 = 50;

       while (i < 50)

       {

               wait (-1.5);

               enemies[i] = pan_create("bmap = enemy.tga; flags = VISIBLE;", 20);

               enemies[i].pos_x = temp1;

               enemies[i].pos_y = temp2;

               start_enemy(i);

               enemy_collisions(i);

               i += 1;

       }

}

 

Function create_aliens( ) generates the enemies; all of the panels are created at x = -52, y = 50, every 1.5 seconds. Play with 1.5 if you'd like to have two consecutive aliens appear closer or farther to each other. Our function calls the start_enemy(i) and enemy_collisions(i) functions as well; this way, the first enemy calls start_enemy(0) and enemy_collisions(0), the second enemy calls start_enemy(1) and enemy_collisions(1) and so on.

 

function start_enemy(i)

{

       var enemy_y;

       var bullet_fired = 0;

       while(is (enemies[i], VISIBLE))

       {

               while((enemies[i].pos_x < 900) && (is (enemies[i], VISIBLE)))

               {

                       enemies[i].pos_x += 1;

                       if ((enemy_bullets[i] == NULL) && (random(1) > bullets_freq))

                       {

                               create_enemy_bullets(i);

                       }

                       wait (1);

               }

               snd_play(engage_wav, 100, 0);

               enemy_y = enemies[i].pos_y;

 

Function start_enemy( ) contains four inner loops and runs for as long as the enemy is visible. The first inner loop will run until the enemy's panel reaches 900 pixels on the x axis, adding 1 pixel to enemy's pos_x each frame. If the current enemy doesn't have a flying bullet already and random(1) has delivered a big value we create a bullet by calling create_enemy_bullets(i). As you can imagine, each enemy runs its own bullet function.

 

The last two lines of code are run as soon as the first inner loop ends; we play the engage_wav sound, telling the player that the enemy will start to move downwards, and then we store enemy's pos_y inside the variable named enemy_y.

 

               while((enemies[i].pos_y < enemy_y + 80) && (is (enemies[i], VISIBLE)))

               {

                       if (enemies[i].pos_y > 680)

                               players_health = 0;

                       enemies[i].pos_y += 3;

                       wait (1);

               }

 

The second inner loop has ended here; it moves the enemy downwards for about 80 quants if it isn't dead (it is still visible). If one of the enemies has reached the bottom of the screen (its pos_y > 680 pixels), we set player's health to zero, making it explode. If the enemy didn't reach the bottom of the screen, we add 3 pixels to its pos_y value each frame until it moves at least 80 pixels vertically.

 

               while((enemies[i].pos_x > 50) && (is (enemies[i], VISIBLE)))

               {

                       enemies[i].pos_x -= 1;

                       if ((enemy_bullets[i] == NULL) && (random(1) > bullets_freq))

                       {

                               create_enemy_bullets(i); // then create a bullet!

                       }

                       wait (1);

               }

 

It's time for the third inner loop; it moves the enemies towards the left side of the screen, subtracting one pixel from its pos_x each frame for as long as enemy's pos_x is greater than 50 pixels. If the enemy doesn't have a flying bullet already and random(1) has delivered a value that's greater than bullets_freq we create a new bullet.

 

               snd_play(engage_wav, 100, 0);

               enemy_y = enemies[i].pos_y;

               while((enemies[i].pos_y < enemy_y + 80) && (is (enemies[i], VISIBLE)))

               {

                       if (enemies[i].pos_y > 680)

                               players_health = 0;

                       enemies[i].pos_y += 3;

                       wait (1);

               }

       }

       enemies[i].pos_y = -10000; // the enemy is dead here, so move it outside the screen

}

 

The last inner loop moves the enemy 80 quants downwards one more time, adding 3 pixels to its pos_y each frame. If the enemy has reached the bottom of the screen (pos_y > 680) we trigger player's explosion by setting its health to zero. The last line of code moves the dead enemies 10000 pixels above the screen, where player's bullets can't find them anymore.

 

function create_enemy_bullets(i)

{

       var ebullet_speed = 5 + random(5);

       enemy_bullets[i] = pan_create("bmap = ebullet.tga; NULL;", 25);

       enemy_bullets[i].pos_x = enemies[i].pos_x + enemy_width / 2 - ebullet_width / 2;

       enemy_bullets[i].pos_y = enemies[i].pos_y + 15;

       enemy_bullets[i].flags |= VISIBLE;

       snd_play(enemyfire_wav, 90, 0); // play the enemyfire_wav sound effect

       while (enemy_bullets[i] != NULL)

       {

               if (enemy_bullets[i].pos_y < 780)

               {

                       enemy_bullets[i].pos_y += ebullet_speed;

               }

               else // reached the bottom of the screen?

               {

                       reset(enemy_bullets[i], VISIBLE);

                       break; // get out of the while loop

               }

 

Each enemy runs an instance of create_enemy_bullets(i) when it wants to fire a bullet. The bullets have random speeds; their panels are generated at runtime, 15 quants below the top of the enemy bitmaps. We play the enemyfire_wav sound effect, and then we run a loop for as long as the bullet exists.

 

If the bullet didn't reach the bottom of the screen (its pos_y is smaller than 780 pixels) we increase its pos_y with the random value that's stored in ebullet_speed; otherwise, if the bullet has reached the bottom of the screen, we hide the enemy bullet panel and we get out of the loop.

 

               if ((((enemy_bullets[i].pos_x + ebullet_width >= ship_pan.pos_x) && (enemy_bullets[i].pos_x + ebullet_width <= ship_pan.pos_x + player_width)) ||

                       ((ship_pan.pos_x + player_width >= enemy_bullets[i].pos_x) && (ship_pan.pos_x + player_width <= enemy_bullets[i].pos_x + ebullet_width)))

                       && (((enemy_bullets[i].pos_y + ebullet_height >= ship_pan.pos_y) && (enemy_bullets[i].pos_y + ebullet_height <= ship_pan.pos_y + player_height)) ||

                       ((ship_pan.pos_y + player_height >= enemy_bullets[i].pos_y) && (ship_pan.pos_y + player_height <= enemy_bullets[i].pos_y + ebullet_height))))

               {

                       players_health -= 10;

                       snd_play(playerhit_wav, 90, 0);

                       ship_pan.bmap = shiphit_tga;

                       reset(enemy_bullets[i], VISIBLE);

                       wait (3);

                       ship_pan.bmap = ship_tga;

                       break; // get out of the while loop

               }

               wait (1);

       }

       enemy_bullets[i] = NULL;

}

 

The "if" branch above checks if the player has collided with the enemy bullet or not; if the answer is affirmative, we subtract 10 points from player's health, we play a "playerhit_wav" sound effect, and then we change the bitmap that's used for player's panel with a "shiphit_tga" bitmap for 3 frames. We hide the enemy bullet panel (it has done its job), we restore player's regular ship bitmap, and then we get out of the loop. The last line of code sets the enemy bullet pointer to null, so the same enemy can create a new bullet.

 

function enemy_collisions(i)

{

       while (1)

       {

               if (temp_bullet)

               {

                       if (is (enemies[i], INVISIBLE)) {break;}

                       if ((((temp_bullet.pos_x + bullet_width >= enemies[i].pos_x) && (temp_bullet.pos_x + bullet_width <= enemies[i].pos_x + enemy_width)) ||

                       ((enemies[i].pos_x + enemy_width >= temp_bullet.pos_x) && (enemies[i].pos_x + enemy_width <= temp_bullet.pos_x + bullet_width)))

                       && (((temp_bullet.pos_y + bullet_height >= enemies[i].pos_y) && (temp_bullet.pos_y + bullet_height <= enemies[i].pos_y + enemy_height)) ||

                       ((enemies[i].pos_y + enemy_height >= temp_bullet.pos_y) && (enemies[i].pos_y + enemy_height <= temp_bullet.pos_y + bullet_height))))

                       {

                               players_score += 20; // add 20 points to player's score

                               reset(temp_bullet, VISIBLE); // let's hide player's bullet

                               temp_bullet = NULL; // and allow the player to fire a new bullet

                               snd_play(enemyexplo_wav, 100, 0); // play the enemyexplo_wav sound effect

                               enemies[i].bmap = explo1_tga; // change the enemy bitmap to several explosion frames

                               wait (3);

                               enemies[i].bmap = explo2_tga;

                               wait (3);

                               enemies[i].bmap = explo3_tga;

                               wait (3);

                               enemies[i].bmap = explo4_tga;

                               wait (3);

                               reset(enemies[i], VISIBLE); // and then hide the enemy panel

                       }

               }

               wait (1);

       }

}

 

The last function detects if player's bullet and the enemy have collided. If player's bullet exists and the enemy is visible (it isn't dead) we check for collisions just like we did it before. If player's bullet and the enemy have collided, we increase player's score by 20, we hide player's bullet panel, we set it to null, allowing the player to fire a new bullet, we play the enemyexplo_wav sound effect, and then we display the four explosion bitmaps, replacing the original enemy bitmap. The last line of code hides the enemy panel.

 

This workshop grew bigger than I expected, but I think that you are now prepared to write your own 2D space shooter games. Next month we will explore new areas of 2D gaming, so stay tuned!