Special Effects - Server Control
Earlier we built a sparkle effect using the DispatchEffect function. In this tutorial we will build on that knowledge to create a new entity that will demonstrate how to create client-side special effects that can be controlled by server-side logic.
1) Open the env_sparkler.cpp file
We'll add more code to this file to create an entity that will act as a persistent effect that we can control on the server-side.
2) Create the following class framework in env_sparkler.cpp
class CSparkler : public CBaseEntity { public: DECLARE_SERVERCLASS(); DECLARE_DATADESC(); DECLARE_CLASS( CSparkler, CBaseEntity ); private: CNetworkVar( bool, m_bEmit ); }; LINK_ENTITY_TO_CLASS( env_sparkler, CSparkler );
This block of code declares our sparkler class. Note the addition of the DECLARE_SERVERCLASS
macro definition. This causes us to create a data-table, which allows us to transmit data between the server and client. In this entity, we will use this ability to turn our effect on and off, as well as changing its size. The class member m_bEmit
is declared using the CNetworkVar
type. This properly registers the member for use later in network communications.
The LINK_ENTITY_TO_CLASS declaration simply gives the game engine a label with which to identify the class with. In this case env_sparkler becomes the entity classname of the CSparkler
class.
3) Add the server-side network data-table
IMPLEMENT_SERVERCLASS_ST( CSparkler, DT_Sparkler ) SendPropInt( SENDINFO( m_bEmit ), 1, SPROP_UNSIGNED ), END_SEND_TABLE()
Here we declare our data-table for this entity. In IMPLEMENT_SERVERCLASS
we hook this class (CSparkler
) to the data-table DT_Sparkler
. This identifier will later help us link the server and client entities together, so that they can communicate.
The second line declares the m_bEmit
data member and defines its characteristics for network transmission. See the Data Description Table document for more information on how to declare and transmit data using data-tables. In this case, we wish to transmit a boolean value, which will control whether we should be emitting particles on the client-side.
4) Open the file c_env_sparkler.cpp
This will be the client-side component of our entity. This will handle the actual creation and emission of particles, based on the server-side's requests.
5) Create the following class framework in c_env_sparkler.cpp
#include "cbase.h" class C_Sparkler : public C_BaseEntity { public: DECLARE_CLIENTCLASS(); DECLARE_CLASS( C_Sparkler, C_BaseEntity ); private: bool m_bEmit; };
This block is nearly identical to the server-side version, but here we use DECLARE_CLIENTCLASS
to declare this as being client-side. Also of note is that the client-side declaration of m_bEmit
is simply declared as a boolean value and does not use the CNetworkVar
type.
6) Add the client-side network data-table
IMPLEMENT_CLIENTCLASS_DT( C_Sparkler, DT_Sparkler, CSparkler ) RecvPropInt( RECVINFO( m_bEmit ) ), END_RECV_TABLE()
Again, this is very similar to its server-side counterpart. The top declaration of IMPLEMENT_CLIENTCLASS_DT
serves to link the server, client and data-table all together. Now the engine can resolve the relationship between the client and server-side versions of the class.
7) Add the Spawn() function for the entity
Even though the majority of the work for this entity is being done on the client-side, we still need to take care of the server's end of things. We'll quickly add a Spawn()
function for the entity and setup its initial state.
void CSparkler::Spawn( void ) { SetMoveType( MOVETYPE_NONE ); // Will not move on its own SetSolid( SOLID_NONE ); // Will not collide with anything UTIL_SetSize( this, -Vector(2,2,2), Vector(2,2,2) ); // Set a size for culling AddEFlags( EFL_FORCE_CHECK_TRANSMIT ); }
Most of the function calls in this function should look familiar if you've read through previous documents on entity creation. Because this entity will not move of its own volition, we set its movement type to MOVETYPE_NONE
. Likewise, because it will not collide with other entities, we set its solid type to SOLID_NONE
. We also set its bounding box size to be 4 units square for culling purposes.
The last call to AddEFLags()
causes the EFL_FORCE_CHECK_TRANSMIT
flag to be added to the entity. This is necessary because the entity does not have a model and without this flag the entity would not be sent across to the client.
At this point, changing the server-side value of m_bEmit
will also change the client-side version of m_bEmit
. We can now add logic to the game code to toggle this value.
8) Add an input to CSparkler to allow toggling
Add the following declarations in the appropriate places in env_sparkler.cpp
file. For more information on entity I/O, refer to the Entity I/O Document or the included example files.
void InputToggle( inputdata_t &input ); . . . BEGIN_DATADESC( CSparkler ) DEFINE_FIELD( m_bEmit, FIELD_BOOLEAN ), DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), END_DATADESC() . . . void CSparkler::InputToggle( inputdata_t &input ) { m_bEmit = !m_bEmit; }
By adding the appropriate entries to the FGD file (See the MyMod.fgd that accompanies this document for the proper entry), the entity is now able to toggle its m_bEmit
field via entity I/O in a game map. An example map has been provided, called sdk_fx_server.vmf
. Now we have all the framework in place to create the particles and control their state. Now we'll need to actually create those particles on the client at the appropriate times.
9) Add particle creation code to the client-side
To begin, we need to know when the entity is first instantiated on the client. To do this, we check for a special condition in the OnDataChanged()
function for our client-side entity. This function is called whenever networked member data in the class is altered on the server-side and received on the client-side. It also provides us with special information about when the first and last updates are received for this entity.
void C_Sparkler::OnDataChanged( DataUpdateType_t updateType ) { BaseClass::OnDataChanged( updateType ); if ( updateType == DATA_UPDATE_CREATED ) { SetNextClientThink( CLIENT_THINK_ALWAYS ); } }
Here we check for the updateType
parameter being DATA_UPDATE_CREATED
. This notifies us that this is the first instance of data being changed on the client, denoting the entity's creation. We also set the entity's client-side ClientThink()
function to always be executed by calling the SetNextClientThink()
function with the value CLIENT_THINK_ALWAYS
. This assures that for the life of this entity, it will always receive a ClientThink()
function call on every client frame. We'll use this function later to emit our particles.
{{note|It's vital that we call the base class' OnDataChanged()
function before doing anything else in this function. Failure to do so will cause the entity to act in unexpected ways with regards to how it receives and updates its internal data.
Now we can setup our ClientThink()
function to handle emitting particles. We'll also use this function to control when our particles are allowed to emit by checking the m_bEmit
data member being changed on the server.
void C_Sparkler::ClientThink( void ) { if ( m_bEmit == false ) return; }
For every frame that the client executes, our entity will receive a call to the above function. This makes it an ideal place to emit particles and control other internal state for the entity. You'll also notice that we opt out of this function unless our m_bEmit
is set. By doing this we'll refuse to emit particles if the boolean is not set. This framework now needs the code to emit particles.
10) Set up a persistent particle emitter inside the C_Sparkler
class
Unlike our previous foray into emitting particles, these particles will be emitted every client frame for the life of the entity. While creating a temporary emitter was practical for a "one-off" special effect, this would prove inefficient for a persistent effect, like the one we're creating. To implement this properly, we'll need to create a particle emitter instance that is owned by our C_Sparkler
class. To do this, we'll add a data member to the class.
. . . private: bool m_bEmit; // Determines whether or not we should emit particles CSmartPtr<CSimpleEmitter> m_hEmitter; . . .
This should be very familiar from our last use of particles. Instead of creating the emitter when we want to emit a group of particles, we'll create the emitter once at the creation of the entity and use it over the lifetime of that entity. To create the emitter, we'll use the OnDataChanged()
function again.
void C_Sparkler::OnDataChanged( DataUpdateType_t updateType ) { BaseClass::OnDataChanged( updateType ); if ( updateType == DATA_UPDATE_CREATED ) { m_hEmitter = CSimpleEmitter::Create( "env_sparkler" ); SetNextClientThink( CLIENT_THINK_ALWAYS ); } }
Again, the code is identical to our last use of particles, the only difference being where the creation is happening. Another necessary addition to the code will be a data member to hold the material handle used for the particles we'll emit. We do this to avoid doing unnecessary searches through the material list every frame.
. . . private: bool m_bEmit; // Determines whether or not we should emit particles CSmartPtr<CSimpleEmitter> m_hEmitter; PMaterialHandle m_hMaterial; . . .
Now that we have the material handle, we need to set it to reference the appropriate instance of the material. Again, we accomplish this in the OnDataChanged()
function.
. . . if ( updateType == DATA_UPDATE_CREATED ) { m_hEmitter = CSimpleEmitter::Create( "env_sparkler" ); if ( m_hEmitter.IsValid() ) { m_hMaterial = m_hEmitter->GetPMaterial( "effects/yellowflare" ); } SetNextClientThink( CLIENT_THINK_ALWAYS ); } . . .
Apart from checking that the emitter was properly spawned, the code should be self-explanatory. We've now setup our emitter and have a valid handle to the material we'd like to use for our particles. We can now create those particles.
void C_Sparkler::ClientThink( void ) { if ( m_hEmitter == NULL ) return; if ( m_bEmit == false ) return; SimpleParticle *pParticle; float scale = 8.0f; for ( int i = 0; i < 64; i++ ) { pParticle = m_hEmitter->AddSimpleParticle( m_hMaterial, GetAbsOrigin() ); if ( pParticle == NULL ) return; 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 ) ); pParticle->m_uchColor[0] = 255; pParticle->m_uchColor[1] = 255; pParticle->m_uchColor[2] = 255; pParticle->m_uchStartAlpha = 255; 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; pParticle->m_flDieTime = random->RandomFloat( 0.25f, 0.5f ); } }
The particle creation code is exactly the same code that we used previously to emit particles, with a few simple modifications. However, there is one crucial problem with the code above. Because we now spawn these particles every client frame, the number of particles being created will vary depending on the framerate of the user. At high framerates, massive amounts of particles will be created. At low framerates, there will be very few particles. We therefore need a way to spawn a fixed number of particles regardless of our framerate. To do this, we use a TimedEvent
data member to help us track time and distribute our particle creation across a fixed amount of time that we'll specify.
First, we must add the TimedEvent
data member to our class.
CSmartPtr<CSimpleEmitter> m_hEmitter; PMaterialHandle m_hMaterial; TimedEvent m_tParticleTimer;
Next we'll specify how many events we'd like have triggered over one second. To do this, we'll add the initialization to the same code block we set up our other particle data members in.
if ( m_hEmitter.IsValid() ) { m_hMaterial = m_hEmitter->GetPMaterial( "effects/yellowflare" ); } m_tParticleTimer.Init( 128 ); SetNextClientThink( CLIENT_THINK_ALWAYS );
This function call simply tells the m_tParticleTimer
to fire 128 events per second. In our context, that means we'll use it to emit 128 particles every second, regardless of framerate. This code will replace the for loop in our particle creation code.
. . . float curTime = gpGlobals->frametime; while ( m_tParticleTimer.NextEvent( curTime ) ) { pParticle = m_hEmitter->AddSimpleParticle( m_hMaterial, GetAbsOrigin() ); . . .
11) Compile the code and test
We're ready to see the new effect. Compile and load the map sdk_fx_server.vmf
. Step on the platform directly ahead of you, and the effect should begin to emit in the room. Try changing the number of particles emitted per second, or attach the entity to various other entities in hierarchy, like the waving citizen in this map.
12) Add a new parameter
Now that we've got a functioning entity, it is straightforward to add other parameters to control it from the server. We'll quickly add a scale parameter to the sparkler to let us control the size of the effect via entity I/O. Doing this directly mirrors the process of adding the earlier boolean value. Add the following lines to env_sparkler.cpp
.
. . . CNetworkVar( bool, m_bEmit ); CNetworkVar( float, m_flScale ); . . . DEFINE_KEYFIELD( m_flScale, FIELD_FLOAT, "scale" ), DEFINE_INPUTFUNC( FIELD_FLOAT, "Scale", InputScale ), . . . SendPropInt( SENDINFO( m_bEmit ), 1, SPROP_UNSIGNED ), SendPropFloat( SENDINFO( m_flScale ), 0, SPROP_NOSCALE ), . . . void CSparkler::InputScale( inputdata_t &input ) { m_flScale = input.value.Float(); } . . .
The only real difference here is that the m_flScale
variable is defined in the data description table as a key-value, instead of a regular value. This links the variable into the map data defined for the entity.
Next add the following lines to c_env_sparkler.cpp
.
. . . bool m_bEmit; float m_flScale; . . . RecvPropInt( RECVINFO( m_bEmit ) ), RecvPropFloat( RECVINFO( m_flScale ) ), . . . pParticle->m_uchStartSize = (unsigned char) m_flScale; pParticle->m_uchEndSize = 0; . . . float speed = random->RandomFloat( 4.0f, 8.0f ) * m_flScale;
At this point the entity can also be scaled up and down via entity I/O on the server. In our test map, we use this to control the size via a button. This should provide a simple basis for creating server-controlled special effects. Try parenting the env_sparkler entity to a moving entity, or changing its scale based on proximity to another object.