Special Effects - Introduction
This document will explain the steps necessary to create special effects in the Source engine. Special effects encompass many different visual aspects of the game, from bullet impacts, explosions, dust and debris. We’ll start with the simplest implementation of a special effect, which is a server-dispatched effect. We will build on the code and examples in this tutorial later to make more complex entities.
1) Create a new file called env_sparkler.cpp in your MOD’s server-side folder
Create and include this file in your server project. This will act as the basis for our effect tutorial. Our goal is to create a simple sparkle effect that pulses once when called from the server. To accomplish this, we’ll create a simple utility function that dispatches our effect to the client for it to act upon.
2) Add our dispatch function
Add the following code to env_sparkle.cpp to create the basic dispatch function.
#include "cbase.h" #include "te_effect_dispatch.h" void MakeSparkle( const Vector &origin, float flScale ) { CEffectData data; data.m_vOrigin = origin; data.m_flScale = flScale; DispatchEffect( "Sparkle", data ); }
See definitions of CEffectData and DispatchEffect().
. . . void MakeSparkle( const Vector &origin, float flScale ) { CEffectData data; data.m_vOrigin = origin; data.m_flScale = flScale;
The code above declares a CEffectData class instance and fills in an origin and scale for the effect. This data will be used on the client-side to determine where to place the effect and how large it should be. The CEffectData instances simply hold values and the interpretation of those values on the client-side is completely up to the user.
. . . data.m_vOrigin = origin; data.m_flScale = flScale; DispatchEffect( "Sparkle", data );
Here we call the DispatchEffect() function with an identifier of "Sparkle". We’ll use this name on the client-side to receive and process this dispatch. We pass in the CEffectData() instance that we previously filled out.
Now that we have the server-side sending off a dispatch, we’ll need to create the client-side code to receive it.
3) Create a new file called c_env_sparkler.cpp in your Mod’s client-side folder
Create and include this file in your client project. Like the server-side counter-part, this file will hold our client-side implementation of our sparkle effect. Here we’ll receive the effect dispatched from the server and interpret that into our visual result.
4) Add our dispatch receiving function
Adding the block of code below will give us the necessary framework to start our visual effect.
void SparkleCallback( const CEffectData &data ) { } DECLARE_CLIENT_EFFECT( "Sparkle", SparkleCallback );
See the DECLARE_CLIENT_EFFECT macro definition for more information.
This completes the bridge between server and client. Any server dispatches for the "Sparkle" effect will now be routed into the SparkleCallback
function.
We can now add the actual creation of particles to make our effect appear in the world.
5) Add the particle creation framework to our client-side code
Now that we have put in place the scaffolding for creating our client-side effects, we can begin to actually spawn the particles that will comprise the visuals. Add the following code to the SparkleCallback()
function in c_env_sparkler.cpp
.
#include "particles_simple.h" void SparkleCallback( const CEffectData &data ) { CSmartPtr<CSimpleEmitter> pSparkleEmitter; pSparkleEmitter = CSimpleEmitter::Create( "Sparkle" ); if ( pSparkleEmitter == NULL ) return; Vector origin = data.m_vOrigin; float scale = data.m_flScale; pSparkleEmitter->SetSortOrigin( origin ); }
Particles are created using the CParticleEffect
class. For convenience, the CSimpleEmitter
class wraps some useful functionality for rendering and is what we will generally use to create and deal with particles.
To make our particles, we must create an instance of the CSimpleEmitter
class. To do this we execute the following lines of code:
CSmartPtr<CSimpleEmitter> pSparkleEmitter; pSparkleEmitter = CSimpleEmitter::Create( "Sparkle" );
CSmartPtr
is a helper template class that provides extra safety for our particle emitter pointer. We create the particle system pSparkleEmitter
by calling CSimpleEmitter::Create()
. The string parameter simply identifies this emitter by name for debugging purposes and can be whatever the user finds most useful.
Vector origin = data.m_vOrigin; float scale = data.m_flScale;
In this next code block we create local variables to hold the data we need from our CEffectData instance for this call. Next, we call the emitter’s member function SetSortOrigin()
to tell the particle system where its base origin is for sorting.
pSparkleEmitter->SetSortOrigin( origin );
Without this information, the particle emitter cannot accurately cull its child particles. Here we set it to be the origin point passed in to the function.
6) Add particles to our emitter
At this point we have an empty emitter waiting for child particles to be added to it. The emitter is responsible for allocation and cleanup of these children, as well as culling, moving and drawing them. To add particles, we will add the following block of code to our SparkleCallback
function.
PMaterialHandle hMaterial = pSparkleEmitter->GetPMaterial("effects/yellowflare");
The particles will need a material so that they can be drawn (For more information on materials, see the Creating Materials documentation.) The particles hold onto this information via a reference handle. This handle must be obtained from the particle emitter, via the GetPMaterial()
member function in CSimpleEmitter
. The function will return a handle of type PMaterialHandle
. We will use this in the next step to declare the material for our new particles.
GetPMaterial()
will return a NULL
pointer.Now we can start to actually build the particles which will cause them to appear in our world.
. . . PMaterialHandle hMaterial = pSparkleEmitter->GetPMaterial("effects/yellowflare"); SimpleParticle *pParticle; for ( int i = 0; i < 64; i++ ) { pParticle = pSparkleEmitter->AddSimpleParticle( hMaterial, origin ); if ( pParticle == NULL ) return; }
CSimpleEmitter::AddSimpleParticle()
The CSimpleEmitter
must deal with how its children are created. To do this, we use the AddParticle()
function call to create a new particle. This function’s declaration is as follows:
SimpleParticle* AddSimpleParticle( PMaterialHandle hMaterial, const Vector &vOrigin, float flDieTime=3, unsigned char uchSize=10 );
The hMaterial handle is the material handle we obtained in the previous code block. The vOrigin parameter is the starting position for this particle. The flDieTime data member refers to the lifetime of our particle and uchSize refers to its size. We’ll override the final two parameters later, so we leave them as defaults in this example. The function will return a pointer to the newly created particle if it was successful. If not, we will receive a NULL pointer.
We’ve now allocated a particle that will display at a position and for a certain amount of time. However, that won’t amount to much of a visual effect. To obtain an effect that more accurately lives up to the name Sparkler, we’ll need to customize the parameters of how the particle is to move and draw. To do this, we’ll look at the SimpleParticle itself.
SimpleParticle
Emitters contain a list of particles which they move and draw. How they do this depends on the information contained within the particles themselves. We use the SimpleParticle
class to contain this information for the CSimpleEmitter
class. The SimpleParticle
class is defined as:
class SimpleParticle : public Particle { public: Vector m_vecVelocity; // Velocity of the particle in units per sec float m_flRoll; // Initial roll (in radians) float m_flDieTime; // How long it lives for float m_flLifetime; // How long it has been alive for so far unsigned char m_uchColor[3]; // Color unsigned char m_uchStartAlpha; // Beginning alpha unsigned char m_uchEndAlpha; // End alpha unsigned char m_uchStartSize; // Starting size unsigned char m_uchEndSize; // Ending size float m_flRollDelta; // Radians of roll per second };
The data members all describe characteristics of the particle and are fairly self-explanatory. By specifying varying numbers in these data members, we can get a wide range of behaviors from our particles. By specifying an m_uchEndSize
larger than the m_uchStartSize
, the particle will grow. If the m_uchEndAlpha
is larger than m_uchStartAlpha
, the particle will slowly become more opaque.
All values are expressed as changes over the lifetime of the particle. For instance, the particle will be m_uchEndSize
pixels wide when its m_flLifetime
member is equal to its m_flDieTime
member (at which point it is removed from the emitter). Likewise, the particle will be m_uchStartAlpha
(0-255) at the beginning of its life (m_flLifetime
is equal to zero).
Now that we know the parameters available to us that allow us to change our particles’ behaviors, we can finish our effect. First, we set our starting and ending sizes.
. . . pParticle = pSparkleEmitter->AddSimpleParticle( hMaterial, origin ); if ( pParticle == NULL ) return; pParticle->m_uchStartSize = (unsigned char) scale; pParticle->m_uchEndSize = 0;
The above code block says that the particle will begin at scale size in pixels, and then linearly shrink to 0 pixels in size over the course of its lifetime. Next, we’ll describe how this particle turns.
. . . pParticle->m_uchStartSize = (unsigned char) scale; pParticle->m_uchEndSize = 0; pParticle->m_flRoll = random->RandomFloat( 0, 2*M_PI ); pParticle->m_flRollDelta = random->RandomFloat( -DEG2RAD( 180 ), DEG2RAD( 180 ) );
Because SimpleParticles
are always oriented to face the camera (like a sprite), the turning will only happen in one axis. This will cause the particle to roll around its center while still facing the camera. The m_flRoll
and m_flRollDelta
data members describe the starting position and rate of turn of the particle per second, respectively. It’s important to note that these numbers are describing radians and not degrees. Above you’ll see that m_flRoll
is set to a random value between zero and 2*M_PI (or 360 degrees). This means that the particle will start out randomly oriented across its full range of potential angles. We also use a random value for our m_flRollDelta
field, but this time we use the helper macro DEG2RAD that will convert values expressed in degrees to radians. This is useful because degrees are generally more intuitive than radians for most users. Using this macro, we find a random value somewhere between -180 degrees and 180 degrees of turn per second. Decreasing our values will make the particle spin more slowly, increasing them will make it spin more quickly. Now we’ll describe the color of our particle.
. . . pParticle->m_flRoll = random->RandomFloat( 0, 2*M_PI ); pParticle->m_flRollDelta = random->RandomFloat( -DEG2RAD( 180 ), DEG2RAD( 180 ) ); pParticle->m_uchColor[0] = 255; // Red pParticle->m_uchColor[1] = 255; // Green pParticle->m_uchColor[2] = 255; // Blue
The m_uchColor
data member array holds the information for the red, green, and blue components of our particle’s color, respectively. This is more accurately the tint of the particle, because it describes the color values applied to the vertices of the polygons. If you have a monochromatic (black and white) base material, the particle will be tinted to exactly the color you specify in the m_uchColor[]
character array. If the base material is quite colorful, the results of tinting may be undesired. For our example, we’ll leave the color white (all values are at their maximum 255 value). This means that the base material’s colors will be unaffected by this tint. Decreasing all the values in the array by the same amount will produce darker grays, which cause the colors of the particle’s base material to darken. Try changing the colors and see how they affect the particles being created.
$vertexcolor
keyword set for the engine to factor in the color values provided. See the Creating Materials document for more information.Next we’ll describe how this particle will change its opacity over its lifetime.
. . . pParticle->m_uchColor[0] = 255; pParticle->m_uchColor[1] = 255; pParticle->m_uchColor[2] = 255; pParticle->m_uchStartAlpha = 0; pParticle->m_uchEndAlpha = 255;
Most particles will fade in or out, depending on their utility. We control the fading by using the m_uchStartAlpha
and m_uchEndAlpha
data members. The particle will begin with an opacity (or alpha value) of m_uchStartAlpha
and will linearly fade towards m_uchEndAlpha
over its lifetime. The value is expressed as a normalized float value, where 1.0 is fully opaque and 0.0 is fully translucent. In this example, the particle will begin as completely translucent (invisible) and will become completely opaque over the course of its lifetime. Try swapping these values to make the particle fade out instead.
Now we’ll make the particle actually move through space.
. . . pParticle->m_uchStartAlpha = 0; pParticle->m_uchEndAlpha = 255; Vector velocity = RandomVector( -1.0f, 1.0f ); VectorNormalize( velocity ); float speed = random->RandomFloat( 4.0f, 8.0f ) * scale; pParticle->m_vecVelocity = velocity * speed;
To do this, we use the m_vecVelocity
data member. Much like velocity for entities, the velocity of particles describes the number of units to move in a direction per second (i.e. a vector with a magnitude of 128 will move a particle 128 units in the direction of the vector, each second). In our example, we’ll construct our velocity in two steps. First, we create a unit-length vector randomly pointing into space. Next, we randomly determine a speed for the particle to travel at. This speed is multiplied by the scale value so that it will become faster as the particle becomes larger. It is often useful in special effects to make all values scale in parallel so as to keep some cohesion across varying sizes. Once we’ve created a direction and a speed, we multiply them together to form the velocity of the particle. This is then stored in m_vecVelocity
.
Try using different numbers for speed and direction to make particles emit in cones or at very high or low speeds. Velocity plays one of the biggest parts in how a particle acts and is perceived by a viewer.
Finally we’ll tell our particle emitter how long we wish this particle to live.
pParticle->m_flDieTime = 1.0f;
Here we set the m_flDieTime
data member to 1.0. This tells the particle emitter to remove the particle after one second. Try higher or lower values to see how it affects the particles.
At this point we have a fully functioning particle system. Hook up your DispatchEffect call on the server-side and start to tweak.