Special Effects - Server Control: Difference between revisions
No edit summary |
(→10) Set up a persistent particle emitter inside the C_Sparkler class: link to CSmartPtr page) |
||
(One intermediate revision by one other user not shown) | |||
Line 2: | Line 2: | ||
[[Special Effects - Introduction|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. | [[Special Effects - Introduction|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=== | ===1) Open the env_sparkler.cpp file=== | ||
We | 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=== | ===2) Create the following class framework in env_sparkler.cpp=== | ||
Line 16: | Line 16: | ||
LINK_ENTITY_TO_CLASS( env_sparkler, CSparkler ); | LINK_ENTITY_TO_CLASS( env_sparkler, CSparkler ); | ||
This block of code declares our sparkler class. Note the addition of the <code>DECLARE_SERVERCLASS</code> macro definition. This causes us to create a | This block of code declares our sparkler class. Note the addition of the <code>DECLARE_SERVERCLASS</code> 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 <code>m_bEmit</code> is declared using the <code>CNetworkVar</code> type. This properly registers the member for use later in network communications. | ||
The | 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 <code>CSparkler</code> class. | ||
===3) Add the server-side network data-table=== | ===3) Add the server-side network data-table=== | ||
Line 24: | Line 24: | ||
END_SEND_TABLE() | END_SEND_TABLE() | ||
Here we declare our data-table for this entity. In <code>IMPLEMENT_SERVERCLASS</code> we hook this class (<code>CSparkler</code> | Here we declare our data-table for this entity. In <code>IMPLEMENT_SERVERCLASS</code> we hook this class (<code>CSparkler</code>'')'' to the data-table <code>DT_Sparkler</code>. This identifier will later help us link the server and client entities together, so that they can communicate. | ||
The second line declares the <code>m_bEmit</code> 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. | The second line declares the <code>m_bEmit</code> 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=== | ===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 | 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=== | ===5) Create the following class framework in c_env_sparkler.cpp=== | ||
Line 49: | Line 49: | ||
Again, this is very similar to its server-side counterpart. The top declaration of <code>IMPLEMENT_CLIENTCLASS_DT </code>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. | Again, this is very similar to its server-side counterpart. The top declaration of <code>IMPLEMENT_CLIENTCLASS_DT </code>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=== | ===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 | 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 <code>Spawn()</code> function for the entity and setup its initial state. | ||
void CSparkler::Spawn( void ) | void CSparkler::Spawn( void ) | ||
Line 59: | Line 59: | ||
} | } | ||
Most of the function calls in this function should look familiar if you | 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 <code>MOVETYPE_NONE</code>. Likewise, because it will not collide with other entities, we set its solid type to <code>SOLID_NONE</code>. We also set its bounding box size to be 4 units square for culling purposes. | ||
The last call to <code>AddEFLags() </code>causes the <code>EFL_FORCE_CHECK_TRANSMIT</code> 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. | The last call to <code>AddEFLags() </code>causes the <code>EFL_FORCE_CHECK_TRANSMIT</code> 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 <code>m_bEmit</code> will also change the client-side version of <code>m_bEmit</code>. We can now add logic to the game code to toggle this value. | At this point, changing the server-side value of <code>m_bEmit</code> will also change the client-side version of <code>m_bEmit</code>. We can now add logic to the game code to toggle this value. | ||
Line 81: | Line 81: | ||
} | } | ||
By adding the appropriate entries to the FGD file ( | 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 <code>m_bEmit</code> field via entity I/O in a game map. An example map has been provided, called <code>sdk_fx_server.vmf</code>. 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=== | ===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 <code>OnDataChanged()</code> 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. | 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 <code>OnDataChanged()</code> 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. | ||
Line 94: | Line 94: | ||
} | } | ||
Here we check for the <code>updateType</code> parameter being <code>DATA_UPDATE_CREATED</code>. This notifies us that this is the first instance of data being changed on the client, denoting the entity | Here we check for the <code>updateType</code> parameter being <code>DATA_UPDATE_CREATED</code>. 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 <code>ClientThink()</code> function to always be executed by calling the <code>SetNextClientThink()</code> function with the value <code>CLIENT_THINK_ALWAYS</code>. This assures that for the life of this entity, it will always receive a <code>ClientThink() </code>function call on every client frame. We'll use this function later to emit our particles. | ||
Now we can setup our <code>ClientThink() </code>function to handle emitting particles. We | {{note|It's vital that we call the base class' <code>OnDataChanged()</code> 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 <code>ClientThink() </code>function to handle emitting particles. We'll also use this function to control when our particles are allowed to emit by checking the <code>m_bEmit</code> data member being changed on the server. | |||
void C_Sparkler::ClientThink( void ) | void C_Sparkler::ClientThink( void ) | ||
Line 104: | Line 105: | ||
} | } | ||
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 | 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 <code>m_bEmit</code> 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 <code>C_Sparkler</code> class=== | ===10) Set up a persistent particle emitter inside the <code>C_Sparkler</code> 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 | 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 <code>C_Sparkler</code> class. To do this, we'll add a data member to the class. | ||
. . . | . . . | ||
Line 112: | Line 113: | ||
private: | private: | ||
bool m_bEmit; // Determines whether or not we should emit particles | bool m_bEmit; // Determines whether or not we should emit particles | ||
CSmartPtr<CSimpleEmitter> m_hEmitter; | [[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 | 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 <code>OnDataChanged()</code> function again. | ||
void C_Sparkler::OnDataChanged( DataUpdateType_t updateType ) | void C_Sparkler::OnDataChanged( DataUpdateType_t updateType ) | ||
Line 128: | Line 129: | ||
} | } | ||
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 | 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. | ||
. . . | . . . | ||
Line 156: | Line 157: | ||
Apart from checking that the emitter was properly spawned, the code should be self-explanatory. We | 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 ) | void C_Sparkler::ClientThink( void ) | ||
Line 199: | Line 200: | ||
} | } | ||
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 <code>TimedEvent</code> data member to help us track time and distribute our particle creation across a fixed amount of time that we | 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 <code>TimedEvent</code> 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 <code>TimedEvent</code> data member to our class. | First, we must add the <code>TimedEvent</code> data member to our class. | ||
Line 206: | Line 207: | ||
TimedEvent m_tParticleTimer; | TimedEvent m_tParticleTimer; | ||
Next we | 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() ) | if ( m_hEmitter.IsValid() ) | ||
Line 217: | Line 218: | ||
SetNextClientThink( CLIENT_THINK_ALWAYS ); | SetNextClientThink( CLIENT_THINK_ALWAYS ); | ||
This function call simply tells the <code>m_tParticleTimer</code> to fire 128 events per second. In our context, that means we | This function call simply tells the <code>m_tParticleTimer</code> 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. | ||
. . . | . . . | ||
Line 230: | Line 231: | ||
| | ||
=== 11) Compile the code and test=== | === 11) Compile the code and test=== | ||
We | We're ready to see the new effect. Compile and load the map <code>sdk_fx_server.vmf</code>. 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=== | ===12) Add a new parameter=== | ||
Now that we | 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 <code>env_sparkler.cpp</code>. | ||
. . . | . . . | ||
Line 259: | Line 261: | ||
. . . | . . . | ||
The only real difference here is that the <code>m_flScale </code>variable is defined in the data description table as a | The only real difference here is that the <code>m_flScale </code>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 <code>c_env_sparkler.cpp</code>. | Next add the following lines to <code>c_env_sparkler.cpp</code>. | ||
. . . | . . . | ||
Line 280: | Line 282: | ||
float speed = random->RandomFloat( 4.0f, 8.0f ) * m_flScale; | 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 | 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. | ||
==See also== | ==See also== | ||
*[[Special Effects - Introduction]] | *[[Special Effects - Introduction]] |
Latest revision as of 04:24, 13 August 2018
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.