Morrowing

Beware of the new RPG series! This month we will start to create a new type of game using modules that can (hopefully) be reused in your games, regardless of their type. The project includes a few wdl files already; be sure to check each one of them out.

function main()
{
fps_max = 60;
on_f2 = null;
on_f4 = null;
on_f5 = null;
on_f6 = null;
on_f7 = null;
on_f8 = null;
on_f9 = null;
on_f10 = null;
on_f11 = null;
on_f12 = null;
level_load (level1_wmb);
wait (3);
mouse_map = pointer_pcx;
while (1)
{
if (key_space == on)
{
mouse_mode = 2;
}
else
{
mouse_mode = 0;
}
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
wait (1);
}
}

Main() is the only function inside the morrowing.wdl file; it limits the frame rate to 60 fps, and then it disables many of the function keys (the ones that don't appear here are used for something else). We load the level, we wait until it is loaded, and then we set the bitmap that will be used for the mouse pointer.

If we press and hold the "space" key, the mouse pointer is made visible; otherwise, we hide it. The last few lines of code allow us to move the mouse pointer on the screen if it is visible. We will make the pointer show up whenever we need it (if we need to talk to a character) but don't forget that you can show the pointer at any time by simply pressing and holding down the "space" key.

action player1
{
player = my;
my.x = -2700;
my.y = -2500;
my.z = -2000;
my.pan = 160;
my.transparent = on;
my.alpha = 100;
my.skill40 = camera_distance;
my.skill41 = camera_height;
camera_mode = 3;

This is the action that is attached to the player; you will find it inside the player.wdl file. We set the proper position and orientation for the player; setting it in C-Script is useful if you have a huge level and the player is too small to be noticeable on the map in Wed. The player is transparent, but its alpha factor of 100 makes it opaque. We store the distance from the player to the camera in skill40 and the height of the camera in skill41, and then we set the camera_mode to 3 because the game starts with the camera in 3rd person mode.

while (1)
{
player.pan -= 10 * mouse_force.x * time - 1.5 * (key_a - key_d);
camera.x = player.x - camera_distance * cos(player.pan);
camera.y = player.y - camera_distance * sin(player.pan);
camera.z = player.z + camera_height + 0.8 * sin(my.skill46 * 3.6);
camera.pan = player.pan;
camera.tilt += 7 * mouse_force.y * time;
camera_distance = min (max (camera_distance, 5), 500);
if (key_w + key_s > 0)
{
ent_cycle("walk", my.skill46);
my.skill46 += 10 * (1 + key_shift * 0.5) * time;
my.skill46 %= 100;
}
else
{
ent_cycle("stand", my.skill48);
my.skill48 += 2 * time;
my.skill48 %= 100;
}

The player changes its pan angle if we move the mouse around or if we press the "A" or "D" keys. The following lines keep the camera behind the player, at the distance given by camera_distance and a little bit above the player. I have used a small trick that makes the player wave its head a bit while it is moving; this way the player won't move like a car. How does this trick work? I am adding 0.8 * sin(my.skill46 * 3.6) to camera.z; skill46 is the skill that controls the "walk" animation, and its value changes from 0 to 100. This means that my.skill46 * 3.6 ranges from 0 to 360 and sin(0...360) ranges from -1 to 1, which means that the player will see the camera changing its height this way 0.8 * (-1...1) = from -0.8 to 0.8. You can increase 0.8 if you want to obtain a more prominent head waving effect but don't use a huge values because your customers might throw up while they are playing the game and you will need to bundle a new keyboard with each and every copy of your game :)

The camera has the same pan angle with the player and can tilt freely; the following line limits camera_distance to (5...500) quants. If we press the "W" or "S" key, the player starts its "walk" animation, increasing its speed by 50% if we press and hold the "shift" key. If the player is standing we start playing the "stand" animation in a loop.

if (camera_mode == 3)
{
avoid_obstacles();
}
vec_set (temp, my.x);
temp.z -= 10000;
trace_mode = ignore_me + ignore_passable + use_box;
temp.z = -trace (my.x, temp);
temp.x = 10 * (key_w - key_s) * (1 + 1 * key_shift) * time;
temp.y = 0;
my.skill47 += ent_move (temp, nullvector);
if (my.skill47 > 50)
{
snd_play(step_wav, 30, 0);
my.skill47 = 0;
}
wait (1);
}
}

If we are playing the game in 3rd person mode, function avoid_obstacles() runs each and every frame; we'll discuss it right away. We trace 10,000 quants below the player and then we adjust its height accordingly, placing its feet on the ground. The player can move forward / backward by pressing the "W" and "S" keys; the movement speed doubles if we press and hold the "shift" key.

We add to skill47 the distance that was covered by the player during the last frame; if this distance is greater than 50 quants, we play a step_wav sound and then we reset skill47. Play with 50; this value controls the number of steps that can be heard during the "walk" animation cycle.

function avoid_obstacles()
{
trace_mode = ignore_me + ignore_passable;
vec_set (temporary_distance.x, camera.x);
temporary_distance.z -= 50; // 50 = experimental value
distance_traced = trace (player.x, temporary_distance.x); // trace between the player and temporary_distance
if (distance_traced == 0) // no obstacles on the way?
{
my.alpha = min (100, my.alpha + 3 * time); // then increase player's alpha up to 100
if (camera_distance < my.skill40) // if the camera got closer to the player
{
camera_distance += 1; // restore the initial camera_distance slowly
}
}
else // obstacles encountered?
{
distance_traced -= 2; // then bring the camera 2 quants closer to the player!
my.alpha = (distance_traced / (my.skill40 + 1)) * 100; // decrease player's alpha; don't allow a division by zero
camera.x = player.x - distance_traced * cos(camera.pan); // place the camera behind the player
camera.y = player.y - distance_traced * sin(camera.pan); // at the new distance given by distance_traced
}
}

This function traces from the player to a point that is placed 50 quants below the camera; if distance_traced = 0 (there aren't any obstacles in the way), we increase player's transparency factor up to 100 (if it was smaller than 100). If camera_distance is smaller than the value that was stored in skill40, we increase camera_distance by 1 quant every frame, until it gets back to the initial value.

If distance_traced isn't zero, we've got an obstacle that separates the player and the camera; we decrease distance_traced, bringing the camera 2 quants closer to the camera each frame and we also decrease player's transparency factor, allowing the player to see through the model that would otherwise block the view. The camera is kept behind the player, at the new distance given by distance_traced, until there aren't more obstacles in the way.

If you are still scratching your head (not that I'm against that!) the following picture should clear the fog. I chose to trace to a point that is placed 50 quants below the camera because under normal circumstances (normal relief) you won't see the camera penetrating the relief. Never! Yeah, right, and windows doesn't have any bugs!

function first_person_camera() // press "F1" to run this function
{
camera_distance = 0; // place the camera at player's position
camera_height = 20; // play with this value
player.invisible = on; // make the player model invisible
camera_mode = 1; // set the camera_mode variable to 1st person
}

function third_person_camera() // press "F3" to run this function
{
camera_distance = player.skill40; // restore the distance from the player to the camera
camera_height = player.skill41; // as well as its height
player.invisible = off; // and show the player
camera_mode = 3; // set the camera_mode variable to 3rd person
}

on_f1 = first_person_camera;
on_f3 = third_person_camera;

The game can be played using a 1st person camera (press F1) or a 3rd person camera (press F3). The corresponding functions place the camera at player's position and make the model invisible, or restore camera_distance and camera_height at their initial values and show the player model.

A simple sky cube definition that can be found inside the environment.wdl file concludes the article:

sky level1_cube
{
type = <free2+6.tga>;
z = 100;
layer = 20;
flags = cube, visible;
}

Feel free to play with z = 100. I'll see you all next month!

Circular lens

Last month I was testing a 6 digit priced engine and one of its test levels included a circular lens. I have looked at it quite a bit because I liked the idea, and then I sat down and I wrote the code that does the same thing for A6. You will need A6 professional for this effect, but A6 pro is about 200 - 300 times cheaper anyway!

entity lens_ent
{
type = <sphere.mdl>;
x = 700;
y = 0;
z = 0;
layer = 40;
flags = visible;
}

First of all we define an entity named lens_ent. We put it 700 quants ahead of the view (play with this value to set the size of the lens) and we place it in the center of the screen (however, you can set different y and z values if you want to place it elsewhere).

view lens_view
{
layer = -10;
flags = visible;
arc = -20;
}

Step two: we define a view named lens_view and we give it a negative layer, because we don't want to see it. We make it visible and we give it a decent, negative zoom factor; the left and right side of the image would appear reversed on the sphere model (the lens) if we wouldn't use a negative value here.

starter init_lens()
{
lens_view.pos_x = 0;
lens_view.pos_y = 0;
lens_view.size_x = screen_size.x;
lens_view.size_y = screen_size.y;
lens_view.bmap = bmap_for_entity (lens_ent, 0);
while (1)
{
lens_view.x = camera.x; // keep the lens view in sync with the camera
lens_view.y = camera.y;
lens_view.z = camera.z;
lens_view.pan = camera.pan;
lens_view.tilt = camera.tilt;
lens_view.roll = camera.roll;
wait (1);
}
}

Step three: we set the same position and size for lens_view and the default view (camera). We render the view on the lens_ent's bitmap (sphere.mdl's skin). The loop will keep lens_view in sync with the camera, updating its position and angles all the time. The result is obvious: what we see on the screen appears on the hidden view, which renders the image on the skin of our sphere.mdl model.

Now you can see why I like A6; it is easy to use and it gets better and better every month! Hold on, I've got another idea!

Did you see what I did? The lens on my sniper gun shows a real view of the level, changing as I rotate around. I have defined another "entity" for the gun model and I have adjusted x, y and z in lens_ent's definition until the lens has blended nicely on the sniper gun. Is this cool or what?