2D Games

Top  Previous  Next

This month we will deal with advanced 2D collision mechanisms. We have discussed about primitive collision, which works fine for rectangular objects, but what should we do when we are dealing with a player that has to move around without stepping into water, in a level that looks like this?

 

aum84_workshop1

 

Let's fire up the first example (2dcollision1.c) - it's the most basic demo that uses the advanced collision detection mechanism.

 

aum84_workshop2

 

Move the player around using the cursor left / right keys; you will see that the player follows the red outline precisely. Now let's take a good look at the code:

 

#include <acknex.h>

#include <default.c>

 

BMAP* level_tga; // pointer to the level bitmap

BMAP* player_png = "player.png";

 

PANEL* my_player;

 

PANEL* level_pan =

{

       layer = 10;

       pos_x = 0;

       pos_y = 0;

       bmap = "landscape.tga";

       flags = SHOW;

}

 

function main()

{

       fps_max = 70;

       video_mode = 7; // 800 x 600 pixels

       video_depth = 32; // 32 bit mode

       video_screen = 1; // start in full screen mode

}

 

Nothing new so far; let's see the function that drives the player.

 

function player_startup()

{

       my_player = pan_create("bmap = player_png;", 100);

       my_player.pos_x = 325;

       my_player.pos_y = 425;

       my_player.flags |= SHOW;

       while (1)

       {

               my_player.pos_x += 2 * (key_cur - key_cul);

               my_player.pos_x = clamp(my_player.pos_x, -15, 750);

               wait (1);

       }

}

 

Function player_startup() creates a panel and assigns it the name "my_player". We set player's initial pos_x and pos_y values, and then we make it visible. The "while" loop moves the player around by changing its pos_x value and makes sure that pos_x stays in the -15...750 pixels range.

 

function collisions_startup()

{

       wait (3); // wait until the video functions are available

       var coords_x;

       var coords_y;

       var format;

       var pixel;

       COLOR pixel_color;

       level_tga = bmap_create("landscape.tga");

       while (1)

       {

               coords_x = my_player.pos_x + bmap_width(player_png) / 2;

               coords_y = my_player.pos_y + bmap_height(player_png) - 15; // play with 15

               format = bmap_lock (level_tga, 0);

               pixel = pixel_for_bmap(level_tga, coords_x, coords_y);

               pixel_to_vec (pixel_color, NULL, format, pixel); // store the color of the pixel in pixel_color

               bmap_unlock (level_tga);

               // detected a red pixel on the color map bitmap?

               if (pixel_color.red == 255)

               {

                       my_player.pos_y += 1;

               }

               else // this isn't a red pixel on the color map?

               {

                       my_player.pos_y -= 1;

               }

               wait (1);

       }

}

 

The function above does all the collision magic; it waits until the video functions are available, and then creates a bitmap and names it level_tga. This is exactly the bitmap that serves as a level for our demo. The "while" loop does several interesting things:

 

- First of all, the coords_x and coords_y variables are set to a position that's 15 quants below player's feet; we are using player's bitmap width and height in order to compute the proper values.

- Then, we lock the level_tga bitmap and we get the color of the pixel specified by the coords_x and coords_y variables, storing the color of the pixel inside the pixel_color COLOR vector (it's similar to a regular vector, but used .blue, .green and .red instead of .x, .y, .z).

- If the red component of pixel_color is plain red (pixel_color.red == 255), player's feet are touching the red outline, so we move it upwards by 1 pixel; otherwise, if we don't detect a red pixel below player's feet, we push it downwards by 1 pixel.

 

That's all there is to advanced collision mechanisms; you simply check the color of the pixels near the player and you are done! You might have noticed that our player bounces a bit; this is happening because the code pushes it upwards or downwards 70 times per second (that's the value of fps_max set inside function main). The solution to this problem is simple and was implemented inside the 2dcollision2.c demo, where the player doesn't bounce anymore.

 

The code is almost identical with the one in the previous demo, so let's see what has changed:

 

function collisions_startup()

{

       wait (3); // wait until the video functions are available

       var coords1_x, coords1_y, coords2_x, coords2_y, format, pixel1, pixel2;

       var player_offset = 15;

       COLOR pixel1_color, pixel2_color;

       while (1)

       {

               coords1_x = my_player.pos_x + bmap_width(player_tga) / 2;

               coords1_y = my_player.pos_y + bmap_height(player_tga) - player_offset;

               coords2_x = my_player.pos_x + bmap_width(player_tga) / 2;

               coords2_y = my_player.pos_y + bmap_height(player_tga) - player_offset - 2;

               format = bmap_lock (level_tga, 0);

               pixel1 = pixel_for_bmap(level_tga, coords1_x, coords1_y);

               pixel_to_vec (pixel1_color, NULL, format, pixel1); // store the color of the pixel1 in pixel_color

               // detected a red pixel on the color map bitmap?

               if (pixel1_color.red == 255)

               {

                       my_player.pos_y += 1;

               }

               else // this isn't a red pixel on the color map?

               {

                       pixel2 = pixel_for_bmap(level_tga, coords2_x, coords2_y);

                       pixel_to_vec (pixel2_color, NULL, format, pixel2); // store the color of the pixel in pixel_color

                       // we didn't detect a red pixel 2 pixels below the first one? then let's move downwards, towards it!

                       if (pixel2_color.red != 255)

                       {

                               my_player.pos_y -= 1;

                       }

               }

               bmap_unlock (level_tga);

               wait (1);

       }

}

 

This time we are using two "sensors" to track the color of the pixels; one of them is placed 15 pixels (-player_offset) below player's feet and the other one is placed 17 pixels  (-player_offset - 2) below player's feet. If the first sensor has detected a red pixel, it pushes the player upwards, just like before; otherwise, if it hasn't detected a red pixel, the control is taken by the second sensor, which pushes the player downwards only if it (the second sensor) didn't detect a red pixel as well. This way, we can be sure that the player doesn't jump up and down and that it will follow the red track as long as at least one of its sensors detects a red pixel.

 

How do we translate this code into something that works in a level that contains water, houses, and so on? Open 2dcollision3.c and then run it:

 

aum84_workshop3

 

This time the player can move freely in the level and collides with the red area, which would be the water from the first picture of this workshop. We are using four sensors this time - see for yourself:

 

function collisions_startup()

{

       wait (3); // wait until the video functions are available

       var coords1_x, coords1_y, coords2_x, coords2_y, coords3_x, coords3_y, coords4_x, coords4_y;

       var format, pixel1, pixel2, pixel3, pixel4;

       COLOR pixel1_color, pixel2_color, pixel3_color, pixel4_color;

       while (1)

       {

               // check player's feet

               coords1_x = my_player.pos_x + bmap_width(player_tga) / 2;

               coords1_y = my_player.pos_y + bmap_height(player_tga) - 2; // play with 2

               format = bmap_lock (level_tga, 0);

               pixel1 = pixel_for_bmap(level_tga, coords1_x, coords1_y);

               pixel_to_vec (pixel1_color, NULL, format, pixel1); // store the color of the pixel1 in pixel1_color

               if (pixel1_color.red == 255) // detected a red pixel below player's feet?

               {

                       my_player.pos_y -= 2;

               }

               // check player's head

               coords2_x = my_player.pos_x + bmap_width(player_tga) / 2;

               coords2_y = my_player.pos_y + 20; // play with 20

               pixel2 = pixel_for_bmap(level_tga, coords2_x, coords2_y);

               pixel_to_vec (pixel2_color, NULL, format, pixel2); // store the color of the pixel2 in pixel2_color

               if (pixel2_color.red == 255) // detected a red pixel above player's head?

               {

                       my_player.pos_y += 2;

               }

               // check player's left side

               coords3_x = my_player.pos_x + 2 ; // play with 2

               coords3_y = my_player.pos_y + bmap_height(player_tga) / 2;

               pixel3 = pixel_for_bmap(level_tga, coords3_x, coords3_y);

               pixel_to_vec (pixel3_color, NULL, format, pixel3); // store the color of the pixel3 in pixel3_color

               if (pixel3_color.red == 255) // detected a red pixel on the left side of the player?

               {

                       my_player.pos_x += 2;

               }

               // check player's right side

               coords4_x = my_player.pos_x + bmap_width(player_tga) - 2; // play with 2

               coords4_y = my_player.pos_y + bmap_height(player_tga) / 2;

               pixel4 = pixel_for_bmap(level_tga, coords4_x, coords4_y);

               pixel_to_vec (pixel4_color, NULL, format, pixel4); // store the color of the pixel4 in pixel4_color

               if (pixel4_color.red == 255) // detected a red pixel on the right side of the player?

               {

                       my_player.pos_x -= 2;

               }

               bmap_unlock (level_tga);

               wait (1);

       }

}

 

The four sensors track the positions placed 2 pixels below player's feet, 20 pixels above player's head and 2 pixels sideways, on the left and right side. This time we are using different values for the sensors (feel free to play with them) because we want to allow player's head to touch the (red) water, like in this picture:

 

aum84_workshop4

 

By the way, you can use any color for the collision detection mask and you can check all the RGB values like this:

 

if ((pixel_color.red == 155) && (pixel_color.green == 125) && (pixel_color.blue == 15)) // check for RGB = 155, 125, 15

 

I chose a pure red because I wanted to have striking collision lines / areas. Oh, and this time player's function allows it to move vertically as well.

 

function player_startup()

{

       my_player = pan_create("bmap = player_tga;", 100);

       my_player.pos_x = 325;

       my_player.pos_y = 325;

       my_player.flags |= SHOW;

       while (1)

       {

               my_player.pos_x += 2 * (key_cur - key_cul);

               my_player.pos_y += 2 * (key_cud - key_cuu);

               wait (1);

       }

}

 

All is well, but right now I feel some negative vibe coming from some of my readers: do we have to make our levels ugly, painting them in these dull colors in order to get the collision to work? Fear not, because the 2dcollision4.c demo takes us one step further, introducing alpha-based 2D collision detection:

 

aum84_workshop5

 

Yes! It's our good old 2D level in all its splendor and with working collision detection! Our pixel_to_vec instruction is able to return the color of the pixel AND its transparency (alpha) value. I have created a tga file with an alpha channel for our level and I have named it levelmask.tga:

 

aum84_workshop6

 

As you see, I have selected the water areas and I have saved them as the alpha channel; they are the white zones in the "Load From Alpha" window in my painting application (Paint Shop Pro). The black areas will be the zones that can be explored by the player (grass, sand, stone, etc). Since this bitmap can't be displayed properly on the screen (the transparent parts wouldn't be visible) I am using another bitmap named levelok.tga for the panel that displays the level - it doesn't contain any transparent areas.

 

Here's how the fresh code looks like:

 

function collisions_startup()

{

       wait (3); // wait until the video functions are available

       var coords1_x, coords1_y, coords2_x, coords2_y, coords3_x, coords3_y, coords4_x, coords4_y;

       var format, pixel1, pixel2, pixel3, pixel4, alpha1, alpha2, alpha3, alpha4;

       COLOR pixel1_color, pixel2_color, pixel3_color, pixel4_color;

       while (1)

       {

               // check player's feet

               coords1_x = my_player.pos_x + bmap_width(player_tga) / 2;

               coords1_y = my_player.pos_y + bmap_height(player_tga) - 2; // play with 2

               format = bmap_lock (level_tga, 0);

               pixel1 = pixel_for_bmap(level_tga, coords1_x, coords1_y);

               pixel_to_vec (pixel1_color, alpha1, format, pixel1); // store the color of the pixel1 in pixel1_color

               if (alpha1 != 0) // detected a white area of the alpha channel below player's feet?

               {

                       my_player.pos_y -= 2;

               }

               // check player's head

               coords2_x = my_player.pos_x + bmap_width(player_tga) / 2;

               coords2_y = my_player.pos_y + 20; // play with 20

               pixel2 = pixel_for_bmap(level_tga, coords2_x, coords2_y);

               pixel_to_vec (pixel2_color, alpha2, format, pixel2); // store the color of the pixel2 in pixel2_color

               if (alpha2 != 0) // detected a white area of the alpha channel above player's head?

               {

                       my_player.pos_y += 2;

               }

               // check player's left side

               coords3_x = my_player.pos_x + 2 ; // play with 2

               coords3_y = my_player.pos_y + bmap_height(player_tga) / 2;

               pixel3 = pixel_for_bmap(level_tga, coords3_x, coords3_y);

               pixel_to_vec (pixel3_color, alpha3, format, pixel3); // store the color of the pixel2 in pixel2_color

               if (alpha3 != 0) // detected a white area of the alpha channel on the left side of the player?

               {

                       my_player.pos_x += 2;

               }

               // check player's right side

               coords4_x = my_player.pos_x + bmap_width(player_tga) - 2; // play with 2

               coords4_y = my_player.pos_y + bmap_height(player_tga) / 2;

               pixel4 = pixel_for_bmap(level_tga, coords4_x, coords4_y);

               pixel_to_vec (pixel4_color, alpha4, format, pixel4); // store the color of the pixel2 in pixel2_color

               if (alpha4 != 0) // detected a white area of the alpha channel on the right side of the player?

               {

                       my_player.pos_x -= 2;

               }

               bmap_unlock (level_tga);

               wait (1);

       }

}

 

As you can see, this time we are checking the alpha1... alpha4 values returned by pixel_to_vec; if we detect the white areas of the alpha channel, we move the player away from them by changing its pos_x and pos_y values. This means that our level bitmaps can have any shape, color, and so on - the alpha-based bitmap mask will do the job.

 

And there you have it: fully working 2D collision, ready to be plugged into your projects. Next month we will get busy with creating huge, tile-based 2D worlds, so stay tuned!