Posted by: Persuter
Date posted: Feb 14 2003 User Rating: 5 out of 5.0 | Number of views: 15843 Number of comments: 1 | Description: Part 3 in 5 part series |
Part 2 Part 4
Welcome to the third part of the particle engine tutorial. In this part, we'll talk about what actually goes into creating a particle system for Half-Life. We'll just basically create a small snow particle system. If you study this and understand how everything else works in the particle system, you can easily see how to build this into much larger structures.
OK, so, to make a snow particle system, we must figure out how we can fit it into our generic five functions. DrawParticle's function is rather obvious, and really won't change much from particle system to particle system. We will make TestSystem always return true, so it, too, will not be an interesting function. (In a real system, you would probably want to do some sort of check to see whether it should still be snowing, and also a check to see whether the player is indoors (where it rarely snows).)
First off, our snow particles must be affected by gravity, and must disappear when they hit the ground. Thus, TestParticle must test to see if a particle has hit the ground and, if it has, return false. UpdateParticle must simulate the effects of gravity (you can reeeeeally get complex here, making snow swirl around moving players, creating wind effects, etc., but we will stick with nice fluttering snow). UpdateSystem must continually create snow particles in the correct place.
So then, what do we have?
| | class SnowParticleSystem : public ParticleSystem { public: SnowParticleSystem( cl_entity_t* ); virtual bool TestSystem( void ); virtual void UpdateSystem( void ); virtual bool TestParticle( particle* ); virtual void UpdateParticle( particle* ); virtual void DrawParticle( particle* );
private:
void MakeNewSnowParticle( particle* ); cl_entity_t* m_pCenterEntity;
float difftime, lasttime; }; |
Now you might be asking yourself: Wait a minute. What's all this cl_entity_t* m_pCenterEntity stuff? This is the entity which we will use as our reference point when creating and destroying snow particles. That is, the snow is created around this entity. This will, in our system, generally be the player. Remember not to think in server dll terms. The particles you show on the client side won't be known at all by the server side, so we only want to show snow particles to the local player.
The function MakeNewSnowParticle should be fairly obvious: we will use it to change a particle structure to make a random new snow particle. I'll explain difftime and lasttime in a bit.
OK, so let's write the constructor:
| | #define STARTING_SNOW_PARTICLES 80
SnowParticleSystem::SnowParticleSystem( cl_entity_t* entity ) { m_pCenterEntity = entity;
particle snowparticle;
for( int i = 0; i < STARTING_SNOW_PARTICLES; i++ ) { MakeNewSnowParticle( &snowparticle ); AddParticle( snowparticle ); }
lasttime = gEngfuncs.GetClientTime(); } |
OK, nothing too scary there. We simply set m_pCenterEntity to the passed-in entity, and then create snowparticle and change it over and over, adding the new particle "template" each time. Let's go ahead and take a look at how MakeNewSnowParticle works.
| | void SnowParticleSystem::MakeNewSnowParticle( particle* part ) { part->red = part->green = part->blue = 255; part->transparency = 220;
part->origin[0] = m_pCenterEntity->origin[0]+gEngfuncs.pfnRandomFloat( -400.0,400.0 ); part->origin[1] = m_pCenterEntity->origin[1]+gEngfuncs.pfnRandomFloat( -400.0,400.0 ); part->origin[2] = m_pCenterEntity->origin[2]+gEngfuncs.pfnRandomFloat( 100.0, 125.0 );
part->velocity[0] = 0.0; part->velocity[1] = 0.0; part->velocity[2] = gEngfuncs.pfnRandomFloat( -2.2, -1.5 ); } |
Again, pretty simple. We set the colour to pure white, and the transparency to almost opaque (I find that allowing a little bit of transparency makes things look a little nicer). We then take the center entity's origin and offset it by a random vector. We can create the particle anywhere in a 60-foot by 60-foot square about 10 feet over the player's head. Finally, we set the particle's velocity to straight downward, with a slight randomness to make sure our snow doesn't all fall at the same rate.
When we're done with this, I encourage you to come back and play with these values. A change in any one of them will result in a very different system, and you will gain more insight into how these sorts of functions work (the particle creation function is the second-most-important function in virtually any particle system).
Now we'll do a simpler one.
| | bool SnowParticleSystem::TestSystem( void ) { return true; } |
Ok, not so hard, I hope. <!--emo&:)--> <!--endemo--> Next we'll do UpdateSystem, which is an important function.
| | #define PARTICLES_ADDED_PER_SECOND 30
void SnowParticleSystem::UpdateSystem( void ) { float time = gEngfuncs.GetClientTime(); difftime = �time - lasttime; lasttime = time;
if( difftime > 0.0 ) { particle snowparticle;
for( int i = 0; i < ((int)(PARTICLES_ADDED_PER_SECOND*difftime))+1; i++ ) { MakeNewSnowParticle( &snowparticle ); AddParticle( snowparticle ); } } } |
Now, note what we're doing here. We set lasttime to the client time in the constructor, and now we calculate the difference between that time and the current time, and then set lasttime to the current time again. The important number that we will use is difftime, which will tell us how long it has been since the last frame. This is extremely important, because the particle systems are updated every frame, which means that as your frame rate changes, we must have a way to keep the animation consistent. That is, your snow should not fall faster because you're in a low-poly area. (The +1 is there because if difftime is too low, since integers always round down, you'll get 0 particles added per frame, which unfortunately does not average to what you want.) We check that difftime is greater than 0.0 because in single player, when you pause the game, you need the particles to stop generating, and time stops incrementing during pause. Thanks to Powersoul and Ghoul for noting the problem and suggesting the fix. Actually, if you ARE implementing this in a single player mod where this sort of thing is important, you might simply want to do a difftime check in ParticleSystemManager::UpdateSystems and refuse to do anything to a system unless difftime is greater than 0.0. Otherwise you should also put this check in all your UpdateSystem and UpdateParticle calls.
We immediately use this calculation to add the proper number of snow particles. If we're running at ten frames per second, we still add the same number of particles as if we're running at sixty or more.The particle creation is exactly the same otherwise.
OK, now on to the particle functions.
| | bool SnowParticleSystem::TestParticle( particle* part ) { vec3_t origin; VectorCopy( part->origin, origin );
vec3_t diff; VectorSubtract( origin, m_pCenterEntity->origin, diff ); if( Length( diff ) > 600 ) return false;
vec3_t test[4]; for( int i = 0; i < 4; i++ ) VectorCopy( origin, test[i] );
test[0][2]-=2; test[1][2]-=1 test[2][2]-=1.5; test[3][2]-=0.5;
if(gEngfuncs.PM_PointContents( origin, NULL )==CONTENTS_SKY ||( gEngfuncs.PM_PointContents( test[0], NULL )!=CONTENTS_SOLID && gEngfuncs.PM_PointContents( test[1], NULL )!=CONTENTS_SOLID && gEngfuncs.PM_PointContents( test[2], NULL )!=CONTENTS_SOLID&& gEngfuncs.PM_PointContents( test[3], NULL )!=CONTENTS_SOLID)) return true; else return false; } |
OK, that's a pretty long function, but realistically, it'll be the same in nearly every particle system you do involving solids (as opposed to gases, like smoke). There's nothing too surprising. If you're scared by that long test, try just using one test vector, it'll work just as well for snow. (Things that fall faster, like rain, need higher resolution.) (Attribution: That test is taken whole from Basiror's particle system, except for a bit of a change involving errors in vector and pointer math in the test vector initializations.)
As long as we're on gEngfuncs, let's go ahead and do the DrawParticle function:
| | void SnowParticleSystem::DrawParticle( particle* part ) { vec3_t normal,forward,right,up,point,origin; VectorCopy( part->origin, origin );
gEngfuncs.GetViewAngles((float*)normal); AngleVectors(normal,forward,right,up); HSPRITE snowsprite = SPR_Load( "sprites/snowparticle.spr" ); gEngfuncs.pTriAPI->SpriteTexture((struct model_s*)gEngfuncs.GetSpritePointer(snowsprite),0);
gEngfuncs.pTriAPI->Color4f(part->red/255.0, part->green/255.0, part->blue/255.0, part->transparency/255.0); gEngfuncs.pTriAPI->Brightness(part->transparency/255.0); float size = 3.0;
gEngfuncs.pTriAPI->Begin( TRI_TRIANGLE_FAN );
gEngfuncs.pTriAPI->TexCoord2f (0, 0); VectorMA ( origin,size ,up ,point); VectorMA (point ,-size ,right ,point); gEngfuncs.pTriAPI->Vertex3fv(point);
gEngfuncs.pTriAPI->TexCoord2f (0, 1); VectorMA (origin,size,up,point); VectorMA (point,size,right,point); gEngfuncs.pTriAPI->Vertex3fv (point);
gEngfuncs.pTriAPI->TexCoord2f (1, 1); VectorMA (origin,-size,up,point); VectorMA (point,size,right,point); gEngfuncs.pTriAPI->Vertex3fv (point);
gEngfuncs.pTriAPI->TexCoord2f (1, 0); VectorMA (origin,-size,up,point); VectorMA (point,-size,right,point); gEngfuncs.pTriAPI->Vertex3fv (point); � �
gEngfuncs.pTriAPI->End();
} |
If you're confused by all that TriAPI stuff, I suggest you go out and read some stuff on OpenGL triangle drawing, which is extremely similar. Otherwise, just use it, it's more or less the same whatever you're doing. Basically, what it does is make four corners of a square that is perpendicular (flat) to your vision, and set those four corners to the corners of your sprite that you are using to display the snow. Now, that texture loading is unbelievably inefficient. To be as efficient as we can be, we would put SpriteTexture in UpdateSystem, and the SPR_Load in our constructor. However, we're lazy (I am, anyway), and it doesn't make as much difference as some people would have you believe. We'll change it for the better in part 5 of the tutorial. (Attribution: This drawing code is lifted nearly whole from a tutorial on flfmod.com on particle systems, which I believe was written by Deadpool (please email me if I am incorrect on this). The billboarding code is attributed on that site to Ilian.)
OK, four functions down, one to go. Aren't you excited?!
| | void SnowParticleSystem::UpdateParticle( particle* part ) { float time = gEngfuncs.GetClientTime(); difftime = �time - lasttime; lasttime = time;
if( difftime > 0.0 ) { part->velocity[0] += gEngfuncs.pfnRandomFloat( -0.2, 0.2 ); part->velocity[1] += gEngfuncs.pfnRandomFloat( -0.2, 0.2 ); VectorMA( part->origin, 60.0*difftime, part->velocity, part->origin ); } } |
Well, that was simple. After checking that we're not in pause, we simply set the velocity parallel to the ground (that is, left and right, back and forward) to some small vector, and then multiply the velocity by difftime*60.0, and then add it to the origin. (If you're confused about VectorMA, it simply multiplies the third argument by the second argument, then adds it to the first argument, and sticks the result in the fourth argument. It's very helpful to have in vector operations, as you can see.) Since part->velocity[2] is already negative, meaning the snow particle is falling downwards, this simply makes the snow particle flutter back and forth in a surprisingly realistic manner as it falls. If we wanted the particle to speed up as it fell, we would also have an acceleration vector pointing downwards which we would then add to the velocity vector each frame, but in this case it really isn't necessary.
Finally, now you may ask yourself, where can I make it so that this snow system appears? Well, this is not a tutorial on how to implement commands, but hint: look in input.cpp. Once you've found an appropriate place to add a snow system, you can simply add it with:
| | GetManager()->AddSystem( new SnowParticleSystem( gEngfuncs.GetLocalPlayer() ) ); |
Now it should be snowing all around you. If it isn't, something's wrong. (Note: again, there will be header files which you will have to include to be able to do this.
OK, that's pretty much it. This is a very very VEEEERY simple particle system which you can implement right now in your game, and it will run fine. Now, if you're actually going to release the mod with this code in it, I strongly suggest you read on to the fifth part, which will tell you how to optimize all this a bit better, not to mention the fourth part, where you will learn how to control all this from the server side. Also, pleeeease don't copy/paste this code and then email me asking where snowparticle.spr is.
As usual, if you have any questions or noted bugs, email me at persuter@planethalflife.com. I'll be happy to answer any questions you have (as long as they are not questions that can be easily answered by reading the tutorials).
Part 2 Part 4 |
|
User Comments
Showing comments 1-1
I am currently working my way through this tutorial, but I have run into two problems.
Firstly, the "gEngfuncs.GetLocalPlayer()" in the final line of code above does not seem to be working as expected. As far as I can tell, the pointer being returned is NULL, and when MakeNewSnowParticle() attempts to use the value of m_pCenterEntity->origin I get a Runtime crash. I have placed the GetManager()->AddSystem() call inside the InitInput() function in input.cpp; this seemed the obvious place for it, but is that my problem? Do I need to put it elsewhere?
(This is not a big issue! I haven't moved on to Part 4 of the tut yet -- I wanted to be sure everything was working with Part 3 before I proceeded, but it seems that once I do move on to Part 4 I won't be using GetLocalPlayer() anyway?!)
Secondly, once I removed references to m_pCenterEntity so the code would run, and set my origin to (0,0,0), I started seeing snowflakes appearing. However, they were NOT falling as expected, they just sat there! After a fair bit of poking around, I discovered the problem!
In SnowParticleSystem::UpdateSystem() -- which runs once per frame, if my understanding is correct? -- we calculate the value of difftime and reset the value of lasttime. However, in SnowParticleSystem::UpdateParticle() -- which runs much more frequently, we ALSO calculate difftime and reset lasttime. Essentially, given the granularity of GetClientTime(), this means that difftime is almost always 0.0 -- more snow particles were being generated at around 1 or 2 per second, and any particle movement that occurred was so small as to be invisible! (I suspect, based on your commentary, that this was a last minute change?) Once I removed the following three lines from UpdateParticle():
float time = gEngfuncs.GetClientTime(); difftime = time - lasttime; lasttime = time;
and simply checked the value of difftime calculated by UpdateSystem(), my snow started falling.
These problems aside, the tutorial has been great so far. I'm learning heaps! Thanks for writing it up! :-)Edited by darkPhoenix on Feb 11 2010, 05:00:35
|
|
You must register to post a comment. If you have already registered, you must login.
|
297 Approved Articless
8 Pending Articles
3940 Registered Members
0 People Online (5 guests)
|
|