2D Games

Top  Previous  Next

This month we are going to begin our journey through the world of 2D gaming. And the best way to do that is to start with... fake 2D games, of course! GameStudio integrates a 2D engine and a 3D engine who work great together, even if they are two different pieces of software.

 

Many developers use the power of the 3D engine to create great looking 2D games; just take a look at this month's "Kino One" featured game and you will understand what I mean. Most of these games will use regular 3D levels, just like in the picture below:

 

aum81_workshop1

 

This fake 2D game named ExQuest was developed by me for Aum78's "Plug and play" section. As you can see, I have used a regular 3D level with lots of black textures and a red border. The fake 2D effect is achieved easily, using a top view camera:

 

aum81_workshop2

 

As you can see, our 3D world looks 2D now; nevertheless, we continue to have access to all the 3D features like polygon-based collision detection, 3D explosions, particle effects and so on.

 

Copy the \2dgames folder inside your GameStudio folder, and then open and run the space1.c script:

 

aum81_workshop3

 

Move the mouse around and you will see that the ship changes its position; this "game" wouldn't keep anyone interested for too much time but the code is very simple, so what would you expect?

 

void main()

{

       vec_set(sky_color, vector(100, 1, 1));

       fps_max = 70;

       video_mode = 7;

       video_depth = 32;

       video_screen = 1;

       level_load ("space1.wmb");

       wait (3);

       camera.tilt = -90;

       camera.z = 1000;

       while (!player) {wait (1);}

       while (1)

       {

               camera.x = player.x;

               camera.y = player.y;

               camera.pan = player.pan;

               wait (1);

       }

}

 

Function main sets a dark blue background color, limits the frame rate to 70 frames per second, and then tells the game to run at a resolution of 800 x 600 pixels, using 32 bits of colors and in full screen mode. We load the level, and then we tell the camera to look downwards (tilt = -90); that's how we trick the player into thinking that he is playing a 2D game. The camera is placed at a height of 1000 quants, moves together with the player and has the same pan angle with the player (the "while" loop takes care of that).

 

action player1()

{

       player = my;

       set (my, POLYGON);

       my.ambient = 100;

       VECTOR player_speed;

       while (1)

       {

               player_speed.x = 20 * mouse_force.y * time_step;

               player_speed.y = -15 * mouse_force.x * time_step;

               player_speed.z = 0; // no need to move on the z axis

               c_move(my, player_speed, nullvector, IGNORE_PASSABLE | GLIDE);

               wait (1);

       }

}

 

The action attached to the ship is trivial: it tells the player to use polygon-based collision detection (something that would be very hard to achieve in a "real" 2D environment) and then it sets the ambient of the model to 100 in order to make it look brighter. The player moves with a speed given by mouse_force.y and mouse_force.x along the x and y axis and doesn't move at all on the z axis.

 

aum81_workshop4

 

I have used a blue background because I wanted us to see the limits of the level; right now, the player can move outside the playable (black) area. The following demo improves player's code, taking the level boundaries into account and adding a few more features.

 

Open the space2.wmp level and run it using space2.c; you will see something like this:

 

aum81_workshop5

 

This time the player can move using acceleration and friction, doesn't exceed the level boundaries and we've also got a nice looking starfield effect. All these new features were achieved using less than 20 new lines of code - talk about productivity!

 

void main()

{

       vec_set(sky_color, vector(1, 1, 1)); // set a dark, black background color

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

       video_mode = 7; // run in 800 x 600 pixels

       video_depth = 32; // 32 bit mode

       video_screen = 1; // start in full screen mode

       level_load ("space2.wmb"); // load the level

       wait (3); // wait until the level is loaded

       camera.tilt = -90; // make the camera look downwards

       camera.z = 1000; // and then set the proper height of the camera

       while (!player) {wait (1);} // wait until the player model is loaded

       while (1) // keep the camera above the player

       {

               camera.x = player.x;

               camera.y = player.y;

               camera.pan = player.pan; // the camera has the same pan angle with the player

               wait (1);

       }

}

 

I have only edited the first line of code inside function main, setting a black background color for our level; this way, our level and the background blend nicely together, creating a seamless experience.

 

action player1() // attach this action to the ship model

{

       player = my; // I'm the player

       set (my, POLYGON); // use polygon-based collision detection

       my.ambient = 100; // make the ship look brighter

       VECTOR player_speed, star_pos;

       var temp_x, temp_y;

       while (1)

       {

               temp_x = 15 * mouse_force.y;

               my.skill1 = temp_x * time_step + maxv(1 - time_step * 0.1, 0) * my.skill1;

            player_speed.x = (5 + my.skill1) * time_step;

               temp_y = -10 * mouse_force.x;

               my.skill2 = temp_y * time_step + maxv(1 - time_step * 0.1, 0) * my.skill2;

            player_speed.y = my.skill2 * time_step;

               player_speed.z = 0;

               my.y = clamp(my.y, -450, 450); // limit y to -450...+450 quants

               c_move(my, player_speed, nullvector, IGNORE_PASSABLE | GLIDE);

 

               star_pos.x = my.x - 500 + random(1000);

               star_pos.y = my.y - 500 + random(1000);

               star_pos.z = 0;

               effect(starfield, 1, star_pos.x, nullvector);

               wait (1);

       }

}

 

The action attached to the player model has changed in a significant way; the ship is moved on both axis using acceleration (play with 15 and -10) and friction (play with 0.1). The ship will move at all times (5 gives the default movement speed) and it will limit its y position to the -450... +450 quants interval.

 

Finally, the last few lines inside the loop generate a random position for a particle star each frame, placing it close to the player and a few quants below it.

 

function starfield(PARTICLE *p)

{

       p.alpha = 5 + random(50);

       p.bmap = star_tga;

       p.size = 2 + random(1); // generate stars with random sizes

       p.flags |= (BRIGHT | TRANSLUCENT);

       p.event = fade_stars;

}

 

function fade_stars(PARTICLE *p)

{

       p.alpha -= 0.5 * time_step; // fade out the stars

       if (p.alpha < 0)

       {

               p.lifespan = 0;

       }

}

 

There's nothing special about the particle functions; they generate tiny particles (stars) with random sizes that live for a few frames. Play with 0.5 if you want to alter their the fading speed.

 

Time to add more features to our project; open space3.wmp and then run it using space3.c. It starts just like our previous demo, but wait a bit and you will see that the player is surrounded by asteroids. Try to avoid them all - I didn't ;)

 

aum81_workshop6

 

One of the greatest advantages when it comes to using a 3D level for a fake 2D game is the precise, polygon-based collision detection. Take a look at the picture below; the two ships have barely touched themselves and the explosion was triggered already. Something like that would be very hard to achieve (very cpu-intensive) in a real 2D game.

 

aum81_workshop7

 

That was just the beginning of the explosion, of course; I chose to allow several impacts for a single collision in order to create a more psychedelic effect.

 

aum81_workshop8

 

Aren't you anxious to look at the code for space3.c? There you go!

 

action player1() // attach this action to the ship model

{

       my.emask |= (ENABLE_IMPACT | ENABLE_ENTITY);

       my.event = destroy_player;

       var init_z;

       player = my; // I'm the player

       init_z = my.z;

       set (my, POLYGON); // use polygon-based collision detection

       my.ambient = 100; // make the ship look brighter

       VECTOR player_speed, star_pos;

       var temp_x, temp_y;

       while (1)

       {

               my.z = init_z;

               temp_x = 15 * mouse_force.y; // 15 gives the forward movement speed

               my.skill1 = temp_x * time_step + maxv(1 - time_step * 0.1, 0) * my.skill1; // 0.1 gives the friction

            player_speed.x = (5 + my.skill1) * time_step; // 5 = default movement speed

               temp_y = -10 * mouse_force.x; // 10 gives the sideway movement speed

               my.skill2 = temp_y * time_step + maxv(1 - time_step * 0.1, 0) * my.skill2; // 0.1 gives the friction

            player_speed.y = my.skill2 * time_step;

               player_speed.z = 0; // no need to move on the z axis

               my.y = clamp(my.y, -450, 450); // limit y to -450...+450 quants

               c_move(my, player_speed, nullvector, IGNORE_PASSABLE | GLIDE);

 

               star_pos.x = my.x - 500 + random(1000); // x = -500...+500

               star_pos.y = my.y - 500 + random(1000); // y = -500...+500

               star_pos.z = 0;

               effect(starfield, 1, star_pos.x, nullvector);

               wait (1);

       }

}

 

The action attached to the player has changed a little; the player is now sensitive to impact with other entities and runs its event function (destroy_player) when it collides with another entity. We store the initial height of the player; we will use it as a reference for all the other entities that can collide with the player, in order to make sure that the collisions can take place.

 

function destroy_player()

{

       wait (1);

       ent_create (explosion_pcx, my.x, explosion_sprite); // display the explosion sprite

       wait (-0.3);

       set(my, INVISIBLE);

       wait (-2);

       ent_remove(my);

}

 

Function destroy_player( ) will run as soon as the player collides with an entity; it creates an explosion sprite, makes player's ship invisible and then removes it completely.

 

function explosion_sprite()

{

       my.scale_x = 2;

       my.scale_y = my.scale_x;

       set (my, PASSABLE);

       set (my, TRANSLUCENT);

       set (my, NOFOG);

       set (my, BRIGHT);

       my.ambient = 100;

       my.red = 255;

       my.lightrange = 100;

       my.tilt = 90;

       while (my.frame < 20) // play all the animation frames

       {

               my.frame += 2 * time_step;

               my.scale_x += 0.1 * time_step; // increase the scale of the explosion every frame

               my.scale_y = my.scale_x;

               my.lightrange = 300 - random(100); // make it generate light on a range of 200...300 quants

               wait(1);

       }

       ent_remove(my); // and then remove it

}

 

Function explosion_sprite( ) creates a bright sprite that goes through all its animation frames, increasing its scale and generating dynamic light around it.

 

action asteroid()

{

       while (!player) {wait (1);}

       my.z = player.z; // make sure that the player can collide with the asteroids

       my.ambient = 100;

       set(my, POLYGON);

       my.pan = random(360);

       my.tilt = random(360);

       my.roll = random(360);

       while (1)

       {

               my.pan += 2 * time_step;

               my.tilt += 2 * time_step;

               my.roll += 2 * time_step;

               wait (1);

       }

}

 

The first enemy is an asteroid that has the same height with the player and rotates around its pan, tilt and roll axis. Each asteroid starts with random pan, tilt and roll angles, so you won't see too many asteroids looking exactly the same.

 

action enemy()

{

       my.ambient = 100;

       while (!player) {wait (1);}

       my.z = player.z; // make sure that the player can collide with the enemies

       while (vec_dist(player.x, my.x) > 1000) {wait (1);} // wait until the player comes close to this enemy

       while (1)

       {

               c_move(my, vector(15 * time_step, 0, 0), nullvector, IGNORE_PASSABLE | GLIDE); // 15 = enemy movement speed

               wait (1);

       }

}

 

The enemies have the same height with the player and stand still until the player comes closer than 1000 quants to them. If the player comes close, the enemies start to move towards the player with a speed given by 15 * time_step. Our level is quite small, so it only contains a few enemies; feel free to make it as big as you want.

 

I didn't add weapons and bullets for the player and for the enemies because I wanted to keep the things as easy as possible. Nevertheless, you can use your regular shooter weapon / bullet code with this demo - it will work for sure!