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!
|