Welcome, Guest! Login | Register

Particle Engine tutorial part 3 [Print this Article]
Posted by: Persuter
Date posted: Feb 14 2003
User Rating: 5 out of 5.0
Number of views: 11902
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?

 CODE (C++) 
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:

 CODE (C++) 
#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.

 CODE (C++) 
void SnowParticleSystem::MakeNewSnowParticle( particle* part )
{
    // Snow particles are white, and not too transparent
    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.

 CODE (C++) 
bool SnowParticleSystem::TestSystem( void )
{
    return true;
}


Ok, not so hard, I hope. <!--emo&:)-->user posted image<!--endemo--> Next we'll do UpdateSystem, which is an important function.

 CODE (C++) 
#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.

 CODE (C++) 
bool SnowParticleSystem::TestParticle( particle* part )
{
    // We copy the part->origin to an unassociated vector so that we don't accidentally mess with it
    vec3_t origin;
    VectorCopy( part->origin, origin );

    // We check that the snow particle hasn't gotten too far away from us. If it has, we don't want to bother keeping the particle around
    vec3_t diff;
    VectorSubtract( origin, m_pCenterEntity->origin, diff );
    if( Length( diff ) > 600 )
        return false;

    // Now we create four test vectors and copy origin into each one
    vec3_t test[4];
    for( int i = 0; i < 4; i++ )
        VectorCopy( origin, test[i] );

    // We then set the test vectors to be a little bit below the snow particle
    test[0][2]-=2;
    test[1][2]-=1
    test[2][2]-=1.5;
    test[3][2]-=0.5;

    // This long test basically checks whether any of the test vectors are in solid ground (which means the snow particle is about to hit the ground, and so we should recycle it).
    // It also checks if the particle is in the sky, in which case we do NOT want to do these solid tests
    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:


 CODE (C++) 
void SnowParticleSystem::DrawParticle( particle* part )
{
    vec3_t normal,forward,right,up,point,origin;
   
    // We again copy part->origin into another vector to prevent us accidentally messing with it
    VectorCopy( part->origin, origin );

    // We then get the view angles for the player so that we can "billboard" the sprites    
    gEngfuncs.GetViewAngles((float*)normal);
    AngleVectors(normal,forward,right,up);
   
    // We then set the texture to be used on the triangle  
    HSPRITE snowsprite = SPR_Load( "sprites/snowparticle.spr" );
    gEngfuncs.pTriAPI->SpriteTexture((struct model_s*)gEngfuncs.GetSpritePointer(snowsprite),0);

    // We then set the colour, brightness, and size of each particle
    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;

    // Finally, we draw the snow particle
    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?!

 CODE (C++) 
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:
 CODE (C++) 
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

Rate This Article
This article is currently rated: 5 out of 5.0 (1 Votes)

You have to register to rate this article.
User Comments Showing comments 1-1

Posted By: darkPhoenix on Feb 11 2010 at 04:57:08
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.

Latest Articles
3rd person View in Multiplayer
Half-Life 2 | Coding | Client Side Tutorials
How to enable it in HL2DM

By: cct | Nov 13 2006

Making a Camera
Half-Life 2 | Level Design
This camera is good for when you join a map, it gives you a view of the map before you join a team

By: slackiller | Mar 05 2006

Making a camera , Part 2
Half-Life 2 | Level Design
these cameras are working monitors that turn on when a button is pushed.

By: slackiller | Mar 04 2006

Storing weapons on ladder
Half-Life 2 | Coding | Snippets
like Raven Sheild or BF2

By: British_Bomber | Dec 24 2005

Implementation of a string lookup table
Half-Life 2 | Coding | Snippets
A string lookup table is a set of functions that is used to convert strings to pre-defined values

By: deathz0rz | Nov 13 2005


Latest Comments
knock knock
General | News
By: omega | Dec 22 2016
 
knock knock
General | News
By: MIFUNE | Oct 10 2015
 
New HL HUD Message System
Half-Life | Coding | Shared Tutorials
By: chbrules | Dec 31 2011
 
knock knock
General | News
By: Whistler | Nov 05 2011
 
Particle Engine tutorial part 4
Half-Life | Coding | Client Side Tutorials
By: darkPhoenix | Feb 18 2010
 
Particle Engine tutorial part 2
Half-Life | Coding | Client Side Tutorials
By: darkPhoenix | Feb 11 2010
 
Particle Engine tutorial part 3
Half-Life | Coding | Client Side Tutorials
By: darkPhoenix | Feb 11 2010
 
Game Movement Series #2: Analog Jumping and Floating
Half-Life 2 | Coding | Shared Tutorials
By: mars3554 | Oct 26 2009
 
Particle Engine tutorial part 5
Half-Life | Coding | Client Side Tutorials
By: Deadpool | Aug 02 2009
 
Particle Engine tutorial part 5
Half-Life | Coding | Client Side Tutorials
By: Persuter | Aug 02 2009
 

Site Info
297 Approved Articless
8 Pending Articles
3940 Registered Members
0 People Online (7 guests)
About - Credits - Contact Us

Wavelength version: 3.0.0.9
Valid XHTML 1.0! Valid CSS!