lilith3d

 

Engine

Documentation

Notes

Tutorial 3: Meteor Strike!

  • Particle systems
  • Changing world heights
  • Publisher / Listener
  • More on mouse input
  • Other intersection functions

This is a big tutorial - hang in there and take it slow!

Goal of stage 3: When the user clicks the terrain, a meteor falls from the sky. When it hits the ground, it explodes, and leaves a small pit in the ground. (To misquote StrongBad, "Whoa. That was cool. What key do I press to get that?" )

The meteor and the explosion are modeled with a particle system. We'll start there.

Tutorial 3: Create a Meteor

More correctly, we create the class Meteor. What this does is manage our particle system. It needs a constructor to get going, and then we'll call DoTick() on every frame. It will tell us when it is all done and ready to be deleted with the Done() method.

Meteor::Meteor( int x, int y )

The height of the terrain (257x257 vertices) can only be set at an integer location. Our meteor, therefore, has an integer x,y. It is also much faster to query terrain height at integer values rather than floating point values.

z = L3_CAMERA_CEILING;

Why do we start the meteor at the L3_CAMERA_CEILING? What is the L3_CAMERA_CEILING? Remember worlddefine.h? It has tons of stuff like this. The L3_CAMERA_CEILING is the highest z value anything is at, so we start there.

lastParticleTime = TimeClock::Instance()->Msec();

You will encounter this issue over and over in 3D games: the framerate changes. How do you make the game play the same regardless of the frame rate? In this case, we create a particle every 10ms. If 100ms elapse between frames, we create 10. If very very little time occurs between frames, we create 0. In order to do this, the variable 'lastParticleTime' always contains the time that the last particle was created - in the constructor it gets initialized to the current time.

The TimeClock is not a singleton per se, but like many Lilith3D objects there can be only one TimeClock. The single instance can be queried through its Instance() method.

void Meteor::DoTick()

In general, there is no secret to a good particle effect except playing around and tweaking it. This is not even a good particle effect - it's not bad, but not great. The first part of the function are tweaking constants.

Then, just to cut down on typing, grab some pointers, and initialize a random number generator:

TerrainMesh* tmesh = Lilith3D::Instance()->GetTerrainMesh();
GravParticles* gravParticles = Lilith3D::Instance()->GetGravParticles();
U32 currentTime = timeClock->Msec();
Random rand( currentTime );

Random has some useful methods for generating random numbers, both float and integer. It is fast, although not as fast as rand(), but it has more randomness than most rand() implementations.

Positive z is up; negative z is down. Adjust the z position of the meteor with the all important CalcVelocity. This makes sure the meteor z change is independent of the frame rate:

z -= timeClock->CalcVelocity( METEOR_VELOCITY );

So simple, so powerful. We then check if the meteor has hit the terrain:

 if ( z < tmesh->Height(  x, y ) )

If it HAS hit the terrain, create a big particle explosion! (If it hasn't hit the terrain, it leaves a blue trail. I'll only cover the 'hit' case, the 'doesn't hit' is very straightforward.) The basic particle call is:

 gravParticles->Create( location, velocity, color, 0.0f, L3GravParticles::SMALL );

The 'location' is where to place the particle, 'velocity' it's inital direction and speed. Both are Vector3Fs - an x,y,z vector with floating point components. You will see Vector3F everywhere in the code. The basic particle is a circular particle. You specify its color with a Color3F, which is an RGB color with floating point components. Beyond that is the gravity - in this case 0 but often GravParticles::GRAVITY - and finally the size of the particle.

Particles are easy to use. "Fire and forget". Once you create a particle it will move, run its path, and be cleaned up by the engine.

If you don't want a circular particle, GravParticles defines other kinds. (Only FLAME at the time of this writing, but more will be added.)

gravParticles->Create( location, velocity, color, 0.0f, 1.0f, GravParticles::FLAME1 );

Tutorial 3: Make a crater.

Still inside the DoTick() function, this is the core call the differentiates a geo-morphing engine from a regular one.

const float DELTA = 0.8f;
tmesh->StartHeightChange();
tmesh->SetHeightDelta( x, y, -DELTA );
tmesh->SetHeightDelta( x+1, y, -DELTA * 0.5f );
tmesh->SetHeightDelta( x-1, y, -DELTA * 0.5f );
tmesh->SetHeightDelta( x, y+1, -DELTA * 0.5f );
tmesh->SetHeightDelta( x, y-1, -DELTA * 0.5f );
 tmesh->EndHeightChange();

Before the Terrain can change, you MUST call StartHeightChange(). When you are done changing the terrain, call EndHeightChange(). EndHeightChange() "commits" the changes and calculates the new landscape.

Either method:

tmesh->SetHeightDelta( x, y, -DELTA );
tmesh->SetHeight( x, y, 1.0 ); 

Will set the new terrain height. This code wants to adjust it from the current height, so the SetHeightDelta() version is used.

That's it! The terrain will re-shape. Just don't forget your start/end pair!

Tutorial 3: Listening to the mouse

The Game object has a 'click' method that is called (later in the tutorial) when the user clicks the mouse. When the terrain is clicked on, a new Meteor is created at that location.

Tutorial 3: What did the user click?

When the user clicks the mouse, how do you determine what was clicked? The Lilith3D::IntersectRayFromScreen method will return just this.

What it returns is a little tricky - it returns a std::vector of LilithObjects. A LilithObject is a wrapper for stuff in the game world, and where the click occured. It has methods to query the specific object clicked, the distance to the object, and the point of intersection.

The IntersectRayFromScreen allows you to filter on particular objects. In this case, we filter on the terrain, and take the first intersection as the correct one.

else if ( event.button.button == SDL_BUTTON_LEFT )
{
// Extra check to make sure only the left button is pressed.
if ( SDL_GetMouseState(0, 0) == SDL_BUTTON( SDL_BUTTON_LEFT ) )
{
LilithObjectList oList;
lilith->IntersectRayFromScreen( event.button.x,
event.button.y,
TEST_TERRAIN,
&oList );
     if ( !oList.empty() ) {
        game->Click( *oList.begin(), true );
     }
  }
}
               

Tutorial 3: Walk the meteor list.

Every frame we need to call our meteors DoTick() so they can update. Also, we should delete Meteors no longer being used. The list DoTick/delete is processed, every frame, before calling BeginDraw.

Other intersection functions

Lilith provides a bunch:

  • IntersectPlaneAABB
  • IntersectRayTri
  • TerrainMesh::IntersectRay

...just to name a few. These are powerful and efficient functions that are usually an important part of any game. The TerrainMesh provides intersection methods, and global intersection methods are in "geometry.h".

Abbreviations to be aware of:

  • AABB. Axis-aligned bounding box. A rectangular solid ligned up with the x,y,z axis.
  • Tri. 3 sided polygon.
  • Ray. An infinte line that starts at a point and has a direction.

Conclusion

That was a lot of ground, but now you have something to play with! Enjoy clicking on the terrain, summoning meteors from the sky, and carving holes through the terrain!