Code snippets

Top  Previous  Next

Mani(a)c Miner - the little man returns in full 3D

I remember myself playing those good old games on my Sinclair Spectrum computer; 32Kb of Ram were enough to write a great looking game. One of my favorites was Manic Miner - a game where you play the little man that has to collect some stuff in order to open the door to the next level. It's a classic, but I simply loved it.

By the end of this article, you'll learn how to create any type of platform / arcade 3D game, including Manic Miner. But first, let's give them a warm welcome:

guard.mdl aka 'Maniac Miner'
oldone.mdl aka 'Rock'
curtain.mdl aka 'Curtain'
cola.mdl aka 'Cola'

and finally

evilplnt.mdl also known as "The Evil Plant"! Is this scary or what?

Function main is so simple - there's nothing to explain here. Every object has its own action and we know that these actions start when we run the level without doing anything special. Before I forget: I'm using simplified actions / functions from time to time to make all this stuff easier for you - look at the wdl files for extra spices.

Action player_start is the corresponding action for guard.mdl:

action player_start
{
    start_coords.x = my.x;
    start_coords.y = my.y;
    start_coords.z = my.z;
    my.enable_entity = on;
    my.enable_push = on;
    my.enable_impact = on;
    my.event = maniac_event;
    points = 0;
    maniac_moves();
}

First of all, we store maniac's (player's) coordinates because it will be returned to this position if it dies. If the maniac collides with any entity, its maniac_event will be triggered. Finally, we set score to 0 and we call the function that takes care of maniac's movement:

function maniac_moves()
{
     while (1)
     {
          maniac_gravity;
          if (key_force.x != 0)
          {
               if (key_force.x > 0)
               {
                    player.pan = 0;
               }
              else
               {
                     player.pan = 180;
               }
               walk_speed.x = maniac_speed;
              ent_cycle("walk", my.skill9);
              my.skill9 += animation_speed * time;
              if (my.skill9 > 100) {my.skill9 = 0;}  
         }
         else
         {
              ent_cycle("stand", my.skill10);
              my.skill10 += stand_speed * time;
              if (my.skill10 > 100) {my.skill10 = 0;}  
              walk_speed.x = 0;
        }
        if (key_space == 1)
        {
             maniac_jump();
        }
        move_mode = ignore_you + ignore_passable;
        ent_move (walk_speed, nullvector);
        wait (1);
    }
}

There is a call to maniac_gravity and then some strange key_force.x stuff. When you press the left or right arrow keys on your keyboard, key_force.x changes from 0 to -1 or 1, depending on what key you have pressed. The instruction ent_move (walk_speed, nullvector); will move the maniac in the direction that it is facing so if we make the maniac change its pan angle, it will be enough to make it go in that direction. Because its move_mode = ignore_you + ignore_passable, the maniac can pass through passable entities.

ent_cycle("walk", my.skill9);
my.skill9 += animation_speed * time;
if (my.skill9 > 100) {my.skill9 = 0;}  

I have explained how ent_cycle works in Aum2 - here's a short version: at game start, my.skill9 = 0, so ent_cycle("walk", my.skill9); sets guard's frame to the first walk frame (if guard.mdl has some animation frames named 'walk'); my.skill9 increases because of the 2nd line of code - therefore the walking frames are changing. If we reach the last walking frame (this means my.skill9 = 100), we are looping the walking animation using the last line of code.

Now let's get back to maniac_moves(). If we don't press one of the arrow keys, maniac will "stand" and its speed on x will be reset. If we press space, the maniac will jump:

function maniac_jump()
{
     while (jump_time > -1)
     {
           if (key_space == 0)  {return;}
           walk_speed.z = jump_height * time * jump_time;
           jump_time -= 0.04 * time;
           wait (1);
    }
    while (key_space == 1) {wait (1);}
    jump_time = 1;
}


When the maniac is on the ground, jump_time = 1. The idea is to give the maniac a positive walk_speed.z for a limited time, then make it negative - otherwise the maniac would jump up in the sky and we don't want that! When we press space, jump_time decreases until it is smaller than -1 (the red line in the picture above). When jump_time = -1 the jump is finished; we wait for key_space to be released then we allow the maniac to jump again.

If we press space and then we release it while we're still in the while(jump_time > -1) loop, the following instruction will be executed: if (key_space == 0) {return;}
This allows us to have longer or shorter jumps, depending on how much time "space" has been pressed because as long as jump_time > 0, the jump is getting higher. If we release space quickly and jump_time has only decreased to 0.8 the jump is shorter than the one that is performed for jump_time = 0.1

Function maniac_gravity() takes care that the maniac stands on the ground (if it isn't jumping):

function maniac_gravity()
{
     vec_set (temp, my.x);
     temp.z -= 2000;
     trace_mode = ignore_me + ignore_sprites + ignore_models + use_box;
     my.skill11 = trace(my.x, temp);
     if (my.skill11 > 2)
     {
          walk_speed.z -= 2 * time;
     }
     else
     {
          walk_speed.z = 0;
          jump_time = 1;
     }
     if (my.skill11 < 0)
     {
          walk_speed.z += 5 * time;
     }
}

We're using trace to get maniac's height above the ground and we're storing it in skill11. While we're in the air (skill11 > 2) we use a negative speed on z to pull the maniac down. If skill11 <= 2, we assume that the maniac is on the ground, so we reset its speed on z and we allow him to jump again by setting jump_time  to 1. It is possible to see the maniac getting stuck with its feet in the ground, so we pull it out a little by giving him a positive z speed boost if my.skill11 < 0.

If the maniac collides with something, its event is triggered - you already know that. Here's what happens:

function maniac_event()
{
    if (event_type == event_entity || event_type == event_push || event_type == event_impact)
    {
          wait (1);
          if (you.skill9 == 1)
          {
               player.x = start_coords.x;
               player.y = start_coords.y;
               player.z = start_coords.z;
               points = 0; // reset score
               beep;
         }
    }
}

This function checks if the entity that collided with the maniac has its skill9 set to 1 (this is correct for all the "bad" entities). If this is correct, the maniac has to die, so it will be warped to its starting position (remember we've stored it at game start?), the score will be reset and so on.

The maniac has to be afraid by two entities: the rock and the evil plant. The rock has a simple action:

action rock
{
   my.y = player.y;
   my.skill9 = 1;
}

The first line makes sure that even if we place the rock (in wed) in a wrong position, it will match player's y coordinate at game start - this way we make sure that the collision can happen. The rock is "bad", so it has its skill9 set to 1. The evil plant has a bigger action:

action evil_plant 
{
   plant_speed.y = 0;
   plant_speed.z = 0;
   my.y = player.y;
   my.skill9 = 1;
   while (1)
   {
        move_mode = ignore_you + ignore_passable;
        ent_move (plant_speed, nullvector);
        my.skill20 += plant_speed.x;
        if (my.skill20 == my.skill1 || my.skill20 == my.skill1 * -1)
        {
              plant_speed.x *= -1; 
        }
       wait (1);
   }
}

The plant moves only on x (from left to right and back), so we're setting its speed on y and z to 0; The plant moves all the time using ent_move in a while loop; we're storing the distance covered by the plant in skill20. On the other hand, we have set the maximum distance in wed (skill1). If the distance is smaller than -skill1 or bigger than skill1, the plant will change its direction. This action has a few more lines of code; one of them is

my.skill1 *= plant_speed;

Can you guess what's happening here?
Finally, the good stuff:

action eat_dots
{
   my.y = player.y;
   my.enable_impact = on;
   my.event = dot_action;
}

You can eat cola dots - they react on impact, triggering the following function:

function dot_action()
{
    if (event_type == event_impact)
    {
        my.invisible = on;
        my.passable = on;
        play_sound eatdot_snd,70;
        points += 10;
    }
}

I don't want to "remove" the dots because there are other actions that might need the my synonym. I prefer to make the dots invisible and passable - they won't exist for the player anymore.

If the maniac has managed the get to the top of the level, the curtain waits for the impact. If it happens, a nice zoom out effect is applied, allowing us to see the whole level.

There's something more to explain: the score panel. We start by defining a font and a string (to write "Score:" at the bottom of the screen):

font comic_font, <comic20.pcx>, 34, 38;
string score_string, "Score:";

We define a panel that is always visible:

panel score_panel
{
   pos_x = 0;
   pos_y = 0;
   digits 650,550,4,comic_font,1,points;
   flags = refresh, visible;
}

digits 650,550,4,comic_font,1,points; = "display the variable points using comic_font, with four digits, multiplied by 1, at (650,550) pixels from the upper left corner".

We define a text that is always visible to show "Score:"

text score_text
{
   pos_x = 470;
   pos_y = 550;
   font = comic_font;
   string = score_string;
   flags = visible;
}

Before I forget, Manic Miner (PC version) is freely available for download at 

Manic Miner's Homepage:
http://www.andyn.demon.co.uk

If you think that Manic Miner is too easy to play, wait until you meet the Kong Beast!

The rope and the ladder code - pay one and get the other one for free

By the time I'm writing this article, I haven't tested if the rope code works fine with ladders too, so let me create a small test level and I'll get back with you asap... I have created office.wmp - it has a rope and a ladder in it.

I have told you that the code is really simple. Let's take a look at it:

action rope
{
   gravity_temp = gravity;
   my.enable_impact = on;
   my.event = rope_action;
}

If you take a look at movement.wdl in template, you'll see that the gravity is controlled by 'gravity' - a variable that changes its value if the player is under the water, etc. That's what I have used here: if the player is close enough to the rope / ladder, gravity is set to 0.01 (close to 0) and the player is able to move up and down without falling. Please note that we store the initial 'gravity' in our 'gravity_temp' variable; you can use another entity skill for that if you want to save some memory.  Let's see the rest of the code:

function rope_action()
{
   if (event_type == event_impact && you == player)
   {
        while (my.skill10 < my.skill1)
        {
            my.skill10 = abs(my.x - player.x) + abs(my.y - player.y);
            gravity = 0.01;
            if (key_home == 1 && my.z + (my.max_z - my.min_z) / 2 > player.z - (player.max_z - player.min_z) / 2)
            {
                move (player, rope_speed_up, nullvector);
            }
            if (key_end == 1) // go down
            {
                move (player, rope_speed_down, nullvector);
            }
            wait (1);
        }
        gravity = gravity_temp;
    }
    my.skill10 = 0; 
}

If the player collides with the rope, we check if he continues to be close to the rope in the while loop. At game start, skill10 = 0 so the condition in while is true. We compute the distance between the player and the rope on x and y and if it is small enough (smaller than skill1, which is set in wed), we're still inside the while loop, so we can move the player up and down using the keys home (up) and end (down). You'll have to choose the best value for skill1 when you're testing your ropes and ladders - this value depends on the player width, rope / ladder size, etc. If the player gets away from the rope, gravity = gravity_temp restores the original gravity and the player can walk, run or swim as before.

Don't let this line:

if (key_home == 1 && my.z + (my.max_z - my.min_z) / 2 > player.z - (player.max_z - player.min_z) / 2)

stand in your way; the explanation is quite simple. The distance between the rope / ladder and the player is computed only on x and y (I'll let you guess why), so the player could climb way above the end of the rope / ladder without loosing its x and y coordinates. This scary line makes sure that player's feet stop climbing at the end of the rope / ladder. You know that my refers to the rope / ladder. If you create an entity with its center in the origin (and I would always do that if I were you), its x, y and z are located in the center of the entity.

We can't use something like that:

if (key_home == 1 && my.z > player.z) because this won't allow the player to climb to the end of the rope / ladder

If you take a look at the first picture, you see that we must find out the 'height' (length) of the rope; fortunately we can use max_z - min_z to find it out (2nd picture). Our goal is to allow the player to climb on top of the rope / ladder (3rd picture). I hope that you know that the rope is red and the player is blue; their origins are the yellow dots.

If you look at the 1st picture again, you see that if we add half of the rope to its center (which is my.z) we get the upper z rope coordinate; that's :

my.z + (my.max_z - my.min_z) / 2

If the player has its origin in the center too (we're assuming that this is true), its lower z coordinate (player's feet) will be given by:

player.z - (player.max_z - player.min_z) / 2

We are substracting half of its body from its center, so the player can climb as long as his feet aren't above the top of the rope. Btw, the player climbs using a simple move instruction and two vectors: one of them has a positive speed on z, the other one has a negative speed on z. Change these values to get different climbing speeds.

This rope / ladder code is way too simple to be perfect but it is something that will get you started.