Code snippets

Top  Previous  Next

Sword combat

This time I have created a small level where the player and a skeleton fight using swords. Although the code can (and should) be improved it offers precious information on how to create close range combat weapons (sword, knife, fist, etc). You know that I don't like to use complicate main functions:

function main()
{
    level_load (sword_combat_wmb);
    wait (2);
    clip_size = 0;
    on_d = null;
    fps_max = 40;
}

We're going to move the player using WSAD so I had to disable the debug panel that appears when you press the "D" key. The action attached to the player is:

action player_fight
{
    player = me;
    player.healthpoints = 100;
    while (player.healthpoints > 0)
    {
         camera.x = player.x - 200 * cos(player.pan);
         camera.y = player.y - 200 * sin(player.pan);
         camera.z = player.z + 200;
         camera.pan = player.pan;
         camera.tilt = -30;

These lines say: I'm the player and I have 100 health points (healthpoints is just another name for skill18 in my example). As long as my healthpoints are bigger than 0, the while loop will continue to run. The following lines are placing the camera 200 quants behind the player all the time.

         my.pan += 4 * (key_a - key_d) * time - 20 * mouse_force.x * time;
         player_distance.x = 10 * (key_w - key_s) * time;
         player_distance.y = 0;
         player_distance.z = 0;

The player can rotate by pressing A and D or by moving the mouse. You can move forward / backward using the keys W and S. Player_distance is a var that will be used in the ent_move instruction that moves the player.

         if ((key_w == 1) || (key_s == 1))
         {
              ent_cycle("walk", my.skill20);
              my.skill20 += 4 * time;
              if (my.skill20 > 100) {my.skill20 = 0;}
         }
         else
         {
              ent_cycle("stand", my.skill21);
              my.skill21 += 2 * time;
              if (my.skill21 > 100) {my.skill21 = 0;}
         }   
         ent_move(player_distance, nullvector);

If the player is walking (W or S are pressed) we play its "walk" animation frames. If W or S aren't pressed the player is standing so we play its "stand" animation frames. Finally, player_distance is used to move the player.

         if (mouse_left == 1)
         {
              while (my.skill22 < 100)
              {
                   ent_vertex(my.sword_tip, 315); // get the value in Med
                   ent_vertex(my.sword_base, 293); // get the value in Med
                   trace_mode = ignore_me + ignore_passable;
                   trace (my.sword_base, my.sword_tip);
                   if (result != 0)
                   {
                        effect (particle_sparks, 10, target, normal);
                        if (you != null) {you.healthpoints -= 6 * time;} 
                        ent_playsound (my, sword_snd, 50);
                   }
                   ent_cycle("attack", my.skill22);
                   my.skill22 += 8 * time; 
                   wait (1);
              }
              while (mouse_left == 1) {wait (1);}
         }
         wait (1);
    }

If we press the left mouse button, we enter the attack loop. This loop will run until the player has reached his last "attack" animation frame, stored in skill22. We store the sword base and sword tip (just another names for skill12 and skill15) and we trace between these two positions. If result != 0 (the player has hit something) we generate some particles at hit point using the predefined "target" vector. If the player has hit an entity (not a wall, etc) health is decreased and a sword sound is played. The player will run through his "attack" animation frames and after we release the left mouse button everything goes back to normal.

    while (my.skill23 < 90)
    {
         ent_cycle("death", my.skill23);
         my.skill23 += 3 * time;
         wait (1);
    }
    my.passable = on;
}

At this point the player is dead, so he plays the "death" animation frames and then he becomes passable, therefore he can't be hit by the enemy sword from now on because trace is set to ignore passable entities.

The action attached to the enemy is similar; I have used a pointer named "enemy" but if you plan to use several enemy units replace "enemy" with "my":

action enemy_fight
{
    enemy = me;
    enemy.healthpoints = 100;
    while (my.healthpoints > 0)
    {
         if (vec_dist (my.x, player.x) < 200 && player.healthpoints > 0)
         {
              vec_set(temp, player.x);
              vec_sub(temp, my.x);
              vec_to_angle(my.pan, temp);
              my.tilt = 0;
              enemy_distance.x = 5 * time;
              enemy_distance.y = 0;
              enemy_distance.z = 0;
              ent_move(enemy_distance, nullvector);
              ent_cycle("walk", my.skill19);
              my.skill19 += 5 * time;
              if (my.skill19 > 100) {my.skill19 = 0;}

The enemy has 100 healthpoints too; if the player is alive and comes closer than 200 quants, the enemy will rotate towards him and will start chasing him, playing its "walk" animation frame in a loop.

              if (vec_dist (my.x, player.x) < 50)
              {
                   while (my.skill20 < 100)
                   {
                        ent_vertex(my.sword_tip, 291); // get the value in Med
                        ent_vertex(my.sword_base, 306); // get the value in Med
                        trace_mode = ignore_me + ignore_passable;
                        trace (my.sword_base, my.sword_tip);
                        if (result != 0)
                        {
                             effect (particle_blood, 2, target, normal);
                             if (you != null) {you.healthpoints -= 4 * time;}
                             ent_playsound (my, sword_snd, 50);
                        }
                        ent_cycle("attack", my.skill20);
                        my.skill20 += 5 * time; 
                        wait (1);
                   }
                   waitt (6); // slows down the enemy and reduces the number of traces per second
              }
         }

If the player is closer than 50 quants, the enemy will attack using the similar code and a different particle function. The enemy will cause less damage - the player will have a hard time anyway :).

         else // the player is farther than 200 quants away
         {
              ent_cycle("stand", my.skill21); // play stand frames animation
              my.skill21 += 2 * time; // "stand" animation speed
              if (my.skill21 > 100) {my.skill21 = 0;} // loop animation
         }
         wait (1);
    }
    while (my.skill22 < 80) // the enemy is dead
    {
         ent_cycle("death", my.skill22); // play death frames animation
         my.skill22 += 1 * time; // "death" animation speed
         wait (1);
    }
    my.passable = on; // the corpse can't be hit by the sword from now on
}

If the player is farther than 200 quants, the enemy will go back to its "stand" animation frames. The enemy dies sharing the same code with the player.

The functions that generate particles (player's blood and skeleton's blood) are similar:

function particle_sparks()
{
    temp.x = random(2) - 1;
    temp.y = random(2) - 1;
    temp.z = random(1) - 1.5;
    vec_add (my.vel_x, temp);
    my.alpha = 30 + random(50);
    my.bmap = spark_map;
    my.size = 10;
    my.flare = on;
    my.bright = on;
    my.move = on;
    my.lifespan = 20;
    my.function = fade_particle;
}

function particle_blood()
{
    temp.x = random(2) - 1;
    temp.y = random(2) - 1;
    temp.z = random(1) - 1.5;
    vec_add (my.vel_x, temp);
    my.alpha = 70 + random(30);
    my.bmap = blood_map;
    my.size = 6;
    my.flare = on;
    my.bright = on;
    my.move = on;
    my.lifespan = 20;
    my.function = fade_particle;
}

function fade_particle()
{
    my.alpha -= 5 * time;
    if (my.alpha < 0) {my.lifespan = 0;}
}

The particles are generated in an area located near the impact point and they're falling down (temp.z < 0). Let's take a look at the panel and text:

string health_str = "Player Health:      Enemy Health:";

 

panel health_panel
{
    pos_x = 0;
    pos_y = 0;
    digits = 120, 575, 4, swc_font, 1, player.healthpoints;
    digits = 550, 575, 4, swc_font, 1, enemy.healthpoints;
    flags = refresh, visible;
}

 

text health_text // displays the text
{
    pos_x = 0;
    pos_y = 550;
    font = swc_font;
    string = health_str;
    flags = visible;
}

You can see that I'm using a single panel with 2 digits to display the values and a single text with a longer string to display player's and enemy's strings. You're right, that's why I have created a pointer for the enemy: I wanted to display its health too.

So what happens if the skeleton hits player's sword? The sword and the player are created as a single model in my example so player's health will be decreased anyway. You can avoid that by having a player with a separate sword model attached to him or by detecting the impact point and comparing its coordinates with sword's coordinates read by ent_vertex.

Forest

This snippet will generate a forest in a square sized area in your level. This should save you a lot of work and more than that, you will have a different forest every time you run the level.

define num_trees = 100;

var max_x = 1000;
var min_x = -1000;
var max_y = 1000;
var min_y = -1000;

The forest will have 100 trees; in my example the trees will be placed inside a rectangular area (-1000 quants ... 1000 quants on x and -1000 ... 1000 quants on y but you can use any other values).

The forest is generated by running this function in main:

function generate_forest()
{
    randomize(); // always generate a different forest
    while (tree_index < num_trees)
    {
         tree_pos.x = sign(min_x) * random(abs(min_x)) + sign(max_x) * random(abs(max_x));
         tree_pos.y = sign(min_y) * random(abs(min_y)) + sign(max_y) * random(abs(max_y));
         tree_pos.z = 1000; // 3000 quants above the floor level
         tree_index += 1;
         if (random(1) > 0.5)
         {
              ent_create (tree1_mdl, tree_pos, create_tree);
         }
         else
        {
              ent_create (tree2_mdl, tree_pos, create_tree);
        }
        ifdef fun;
              waitt (8);
        endif;
    }
}

First of all we call the predefined function randomize(); this makes sure that we get a different forest every time we run the level. Random numbers aren't that "random"; they're usually computed using complicated functions but every time you start getting a random number, that huge "random" function delivers the same result. Randomize makes sure that random won't deliver the first function value, but a "random" function value - got it?

As long as we haven't generated all the trees (tree_index < num_trees), we generate a random tree position inside the rectangular area, 1000 quants above the ground (if the ground is set to 0 on the z axis). We add 1 to tree_index, meaning that we have generated a new tree and depending on a random value we create one of the two trees that are used for this forest.

If we run the level like this: "office.wdl -d fun" we place two trees a second, so we will be able to see how they're created and placed in the level. If we run the level without -d fun all the trees will be placed in the level way too fast - but that's what we want, isn't it?

The function that runs for every tree is:

function create_tree()
{
    wait (1);
    my.pan = random(360);
    vec_set (temp, my.pos);
    temp.z -= 3000;
    trace_mode = ignore_me + ignore_sprites + ignore_models + use_box;
    my.z -= trace (my.pos, temp) + 20;
}

You can see that the tree will have a random pan angle. We do a trace 3000 quants below the initial position and we decrease the z coordinate until the tree is placed on the ground. We add 20 quants more to make sure that the "roots" will stick in the ground even if the tree is on a slope.

Please make sure that you use low poly models or (even better) sprites for your trees. If your tree has 500 polygons and you want to have 500 trees in the forest, you won't find a 3d card that is able to render 250,000 polygons at decent frame rates.