Code snippets

Top  Previous  Next

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.


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.