Beginner's corner

Top  Previous  Next

Deformable terrains

 

The following snippet is a remake of one of my older Aum articles; however, this code is more complex, more flexible and runs faster. You control a 1st person player character who has got a rocket launcher and moves around, deforming a terrain level as he or she pleases.

aum50_shot_1

 

panel cross_pan
{
       bmap = cross_pcx;
       pos_x = 393;
       pos_y = 293;
       layer = 15;
       flags = overlay, refresh, visible;
}

 

function main()
{
       level_load (terrain1_wmb);
}

 

action terrain
{
       terrain1 = my; // used by vec_for_mesh and vec_to_mesh
       my.enable_impact = on; // triggers events if it is hit by rockets
       my.event = create_crate;
}

 

I am using a crosshair panel named cross_pan; its pos_x and pos_y values make sure that the cross is placed in the center of the screen at the 800 x 600 pixels screen resolution. Function main loads the level, and the action named terrain makes the terrain sensitive to impact with other entities (our rockets) and sets its associated event function (create_crate).

 

function create_crate()
{
       if (you.skill40 != 12345) // not a rocket?
       {
               return; // then get out of here
       }
       vec_set (hit_coords, rocket1.x); // store rocket's coords before they get lost
       wait (1);
       my.skill1 = 1; // set the index to 1
       my.skill2 = 50000; // set a huge distance at first; skill2 will get smaller and smaller for the vertices that are closer and closer to the explosion
       while (my.skill1 < 1090) // go through all the vertices (1...1089) on the 33x33 grid (not using vertex_dist[0])
       {
               vec_for_mesh (temp, terrain1, my.skill1); // sets temp to vertex1, 2, 3...
               vertex_dist [my.skill1] = vec_dist (hit_coords.x, temp.x); // measures the distance between temp (vertex1, 2, 3...) and hit_coords
               if (vertex_dist[my.skill1] < my.skill2) // this vertex is closer than the others?
               {
                       my.skill2 = vertex_dist[my.skill1]; // then it might be the one, so store the distance
                       my.skill3 = my.skill1; // and its index as well
               }
               my.skill1 += 1; // move on to the following vertex
       }
       deform (my.skill3);
}

 

The function above will run if something (like our rocket) impacts with the terrain. I took care to set rocket's skill40 to "12345" so if some other entity (the player or something else) is colliding with the terrain, nothing will happen. If a rocket has caused the collision to happen, we store rocket's coordinates inside the hit_coords variable, we set an index variable to 1 and then we store a huge distace (50,000 quants) inside skill2.

 

Our terrain has 33 x 33 = 1089 vertices; the "while" loop goes through all the vertices, checking which one of them is closer to hit_coords. When the loop stops running we've got the smallest distance stored in skill2 and the correct vertex index stored in skill3. We are going to deform the terrain using the coordinates given by skill3.

 

function deform(vertex_number)
{
       vec_for_mesh(temp, terrain1, vertex_number);
       vec_scale (temp, 0.7);
       vec_to_mesh (temp, terrain1, vertex_number);
       ent_fixnormals (my, my.frame); // recalculate the normal vectors, not really needed
}

 

This function deforms the terrain, lowering the height of the vertex by 70%; feel free to choose another value that suits your needs. The last line recalculates the normal vectors and isn't really needed; its role is to display smooth lighting across the terrain, even after its deformation.

The player uses a simple action:

 

action player_1st
{
       my.invisible = on; // no need to see the player model in 1st person view
       while (1)
       {
               my.skill1 = 3 * (key_w - key_s) * time; // move with w / s
               my.skill2 = 2 * (key_a - key_d) * time; // strafe with a / d
               vec_set (temp, my.x);
               temp.z -= 1000;
               trace_mode = ignore_me + ignore_passable + use_box;
               my.skill3 = -trace (my.x, temp);
               my.pan -= 3 * mouse_force.x;
               my.tilt += 3 * mouse_force.y;
               if (mouse_left == 1)
               {
                       players_bullet();
               }
               move_mode = ignore_passable + ignore_passents + glide;
               ent_move(my.skill1, nullvector);
               wait (1);
               camera.x = my.x;
               camera.y = my.y;
               camera.z = my.z + 30;
               camera.pan = my.pan;
               camera.tilt = my.tilt;
       }
}
 
We can move using the "W", "S", "A" and "D" keys; the code includes gravity and allows us to rotate the camera around using the mouse. If the player presses the left mouse button (LMB) function players_bullet() will run. The player moves ignoring the passable entities and glides along the surfaces that would try to block its path; the camera uses the same coordinates and angles with the player, being placed 30 quants above its origin.

 

function players_bullet()
{
       proc_kill(4); // only one instance of this function should run
       if (rocket1 != null) {return;} // don't allow more than 1 rocket to be fired at a certain moment
       while (mouse_left == 1) {wait (1);} // disable auto fire
       snd_play (rocket_wav, 50, 0);
       rocket1 = ent_create (rocket_mdl, camera.x, move_players_bullet);
}

 

function move_players_bullet()
{
       var distance_covered = 0;
       my.pan = you.pan; // the bullet and the player have the same pan angle
       my.tilt = you.tilt; // and tilt angle
       my.enable_entity = on; // the bullet is sensitive to other entities
       my.enable_impact = on;
       my.enable_block = on; // and to level blocks
       my.event = remove_bullet;
       my.skill2 = 0;
       my.skill3 = 0;
       my.skill40 = 12345; // unique identifier for the rocket
       while (my.invisible == off)
       {
               my.skill1 = 30 * time; // bullet speed
               move_mode = ignore_you + ignore_passable;
               distance_covered += ent_move (my.skill1, nullvector);
               if (distance_covered > 100)
               {
                       my.skill3 -= 1 * time;
               }
               wait (1);
       }
}

 

Function players_bullet() will run each time the player presses the LMB; we make sure that only one instance of this function is running, and then we wait until the previously fired rocked disappears. The following lines disable the auto firing, play a rocket_wav sound and create a rocket model that is named rocket1 and is driven by function move_players_bullet()

 

That function makes sure that the rocket has the same orientation with the player and makes it sensitive to impact with other entities and level blocks; the event function is named remove_bullet. The rocked is moving due to the values that are stored in skill1... skill3 which act like a single variable. We are setting skill40 to a weird value (12345), thus uniquely identifying the rocket, who will continue to move until it is made invisible. We are setting its speed by setting its skill1 inside the while loop, and we are adding the distance that was covered by the rocked to the local distance_covered variable. As soon as that distance grows bigger than 100 (feel free to play with this value) we decrease skill3, which moves the rocket on its vertical axis downwards. The rocked will fall to the ground and will create a crater.

 

function remove_bullet()
{
       my.invisible = on;
       my.passable = on;
       ent_playsound (my, shockwave_wav, 200);
       ent_create(explosion_pcx, my.x, explosion_sprite);
       sleep (1);
       ent_remove (my);
}

 

function explosion_sprite()
{
       wait (1);
       my.scale_x = 1.5;
       my.scale_y = my.scale_x;
       my.passable = on;
       my.flare = on;
       my.bright = on;
       my.ambient = 100;
       while (my.frame < 7)
       {
               my.frame += 1 * time;
               wait (1);
       }
       ent_remove (my);
}

 

What happens when the rocket hits the ground? It is made invisible, passable and we are playing an explosion sound at the impact position. We also create an explosion sprite and we make sure that it runs the explosion_sprite() function.

The last function sets the proper scale for the explosion, makes the sprite passable, bright, plays its animation frames using a one-shot animation, and then it removes the explosion sprite. It's a simple method but it works pretty well.

 

 

Serious grass

 

aum50_shot_2

 

Serious Sam was the first game that came up with this idea (as far as I know): the player moves around in a level and the grass sprites, models, whatever are created before his / her eyes, becoming more and mode opaque as he or she approaches them. The models or sprites that are far away are removed automatically; this way you can create some decent looking vegetation that won't kill the cpu.

 

starter generate_grass()
{
       var player_position1;
       var player_position2;
       while (player == null) {wait (1);}
       while (1)
       {
               vec_set (temp.x, player.x);
               temp.x += 500 - random(1000);
               vec_set (temp.y, player.y);
               temp.y += 500 - random(1000);
               temp.z = player.z + 200;
               // not too close to the player and the player has moved recently?
               if ((abs(temp.x - player.x) > 300) && (abs(temp.x - player.x) > 300))
               {
                       // increase the nexus if you want to have more than 280 bushes at once
                       if (((player_position1.x != player_position2.x) && (number_of_bushes < 280)) || (total_frames < 300))
                       {
                               if (random(1) > 0.1)
                               {
                                       ent_create (grass1_mdl, temp.x, place_grass);
                               }
                               else
                               {
                                       ent_create (grass2_mdl, temp.x, place_grass);
                               }
                       }
               }
               vec_set (player_position1.x, player.x);
               wait (1);
               vec_set (player_position2.x, player.x);
       }
}

 

I'm using a starter function that waits until the player is created, and then it generates grass in a square who's center is the player model:

 

aum50_grass1

 

If you examine the first "if" branch you will notice that the vegetation is generated only if the distance between the grass model and the player is bigger than 300 quants; we wouldn't want the player to see a bush appearing in front of his very eyes, right? The following "if" branch makes sure that the grass is created only when the player is moving; it wouldn't make too much sense to create the vegetation while the player is standing still. I have used a simple trick in order to detect if the player is moving or not; I have two variables named player_position1 and player_position2 and they store player's position before and after the "wait (1)" instruction. If the player has moved in the last frame, the two variables will be different and the grass will be created if number_of_bushes is smaller than 280. This is a variable that counts how many grass models are created and limits the amount of grass polygons that are visible in the level. You can set a bigger value for number_of_bushes, but you'll have to increase the size of the nexus (make it 100 if you want to have close to 1,000 bushes on the screen, and so on).

 

I have almost forgotten to tell you about the final touch: it won't matter is the player has moved or not, or if the number of bushes is bigger than 280 for the first 300 frames (total_frames < 300). This little trick helps us make sure that the grass models are created in a greater number at game start, when the player didn't move too much and still needs to see some convincing-looking vegetation in the level.

 

I am using two different grass models / bushes named grass1_mdl and grass2_mdl, and each one of them runs the place_grass function:

 

function place_grass()
{
       number_of_bushes += 1;
       my.passable = on;
       my.transparent = on;
       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); // place the grass model on the ground
       while (vec_dist(player.x, my.x) < 1000)
       {
               my.alpha = min (100, (10000 / (vec_dist(player.x, my.x))));
               wait (1);
       }
       ent_remove (my);
       number_of_bushes -= 1;
}

 

My fans might have noticed that each grass model is created 200 quants above player's origin, this function increases number_of_bushes, makes the model passable, transparent, gives it a random orientation and then places it on the ground at the correct height using a trace instruction. Each model will run a while loop that controls its transparency, making it fade away as the player gets away from the model; if the player has moved more than 1,000 quants away, the model is removed and number_of_bushes is decreased, allowing another brand new grass model to be created.