Networking Entities: Difference between revisions

From Valve Developer Community
Jump to navigation Jump to search
m (Nesciuse moved page Networking Entities/en to Networking Entities without leaving a redirect: Move en subpage to basepage)
 
(70 intermediate revisions by 23 users not shown)
Line 1: Line 1:
[[Category:Programming]]
{{LanguageBar}}
=Overview=
{{toc-right}}


The Valve Source engine allows up to 255 players to play at the same time in a shared, virtual, real-time world. To synchronize the user input and world changes between players, Source uses a server-client architecture that communicates via UDP/IP network packets. The server as central authority processes player input and updates the world according to the game & physics rules. The server frequently broadcasts world updates to all connected clients. Logical and physical objects in the game world are called 'entities' and represented in the source code as classes derived from a shared base entity class. Some objects live only on the server (server-side only entities) and some objects live only on the client (client-side only entities), but most objects live on the server and as a corresponding copy on the client. The base entity networking code makes sure that these objects stay synchronized for all players.
Source allows up to 255 players to play at the same time in a shared, virtual, real-time world. To synchronize the user input and world changes between players, Source uses a [[wikipedia:Client-server|server-client architecture]] that communicates via [[wikipedia:User Datagram Protocol|UDP/IP]] network packets. The server, as central authority, processes player input and updates the world according to the game and physics rules. The server frequently broadcasts world updates to all connected clients.


Therefore the networking code has to detect changes in the server-side object, then serialize (transforming into a bit-stream) changed member variables, sending it as a data packet via the network and unserialize the data on the client (updating corresponding member variables in the client-side object). Data packets are not sent with every single change is made in some object; rather frequent snapshots (usually 20/sec) are made that contain all entity changes since the last update. Also not all entity changes are sent to all clients all the time. Only entities that are of possible interest for a client (visible, audible etc) are updated frequently to keep the network bandwidth as low as possible.
Logical and physical objects in the game world are called '[[entities]]' and are represented in the source code as classes derived from a shared base entity class. Some objects live only on the server (server-side only entities) and some objects live only on the client (client-side only entities), but most [[edict_t|cross over]]. The engine's entity networking system makes sure that these objects stay synchronized for all players.


The networking code has to:


=An Example=
# Detect changes in the server-side object
# Serialize (transform into a bit-stream) the changes
# Send the bit-stream as a network packet
# Unserialize the data on the client and update the corresponding client-side object.{{Note|If the client-side object does not exist, the client will automatically create it.}}


This all sounds quite complex and difficult to handle, but most of the work is done in the background by the Source engine. For a mod author it is pretty simple to create a new networked entity class.
Data packets are not sent with every single change made to some object; rather, snapshots (usually 20/sec) are made that contain all entity changes since the last update. Furthmore, not all entity changes are sent to all clients all the time: to keep the network bandwidth as low as possible, only entities that are of possible interest for a client (visible, audible etc.) are updated frequently.


For an example in the SDK, search for <code>m_iPing</code>.  This illustrates networking an array, and the <code>m_iPing</code> variable appears in the code only a dozen times, so it's easy to find all the parts.
A Source server can handle [[Entity limit|up to 2048 networked entities at the same time]], each entity may have 1024 different member variables that are networked to clients including individual members of an array, and each entity can network up to 2KB of serialised data per-update (e.g. 2048 ASCII characters).


The next example should give an idea of how easy it can be. Still it can become very complex once you start optimizing the network bit-stream to reduce the network bandwidth.
==An Example==
This all sounds quite complex and difficult to handle, but most of the work is done in the background by the engine. For a mod author it is pretty simple to create a new networked entity class (in fact for simple entities, you often don't need to do anything at all).


Server-side entity (server.dll):
For an example in the SDK, search for <code>m_iPing</code>.  This illustrates networking an [[array]], and the <code>m_iPing</code> variable appears in the code only a dozen times, so it's easy to find all the parts.


<pre>
This example gives you an idea of how easy it can be - though things ''can'' become very complex once you start optimizing to reduce bandwidth requirements.
 
===Server-side===
 
<syntaxhighlight lang="cpp">
class CMyEntity : public CBaseEntity
class CMyEntity : public CBaseEntity
{
{
Line 24: Line 33:
DECLARE_SERVERCLASS();  // make this entity networkable
DECLARE_SERVERCLASS();  // make this entity networkable


int UpdateTransmitState() // set transmit filter to transmit always
int UpdateTransmitState() // always send to all clients
{
{
return SetTransmitState( FL_EDICT_ALWAYS );
return SetTransmitState( FL_EDICT_ALWAYS );
Line 31: Line 40:
public:
public:
// public networked member variables:
// public networked member variables:
CNetworkVar( int, m_nMyInteger ); // int values from 0..255
CNetworkVar( int, m_nMyInteger ); // integer
CNetworkVar( float, m_fMyFloat ); // any float value
CNetworkVar( float, m_fMyFloat ); // floating point
}
};


//Link a global entity name to this class (name used in Hammer etc.)
//Link a global entity name to this class (name used in Hammer etc.)
LINK_ENTITY_TO_CLASS( myentity, CMyEntity );
LINK_ENTITY_TO_CLASS( myentity, CMyEntity );


//server data table describing networked member variables (SendProps)
// Server data table describing networked member variables (SendProps)
// DO NOT create this in the header! Put it in the main CPP file.
IMPLEMENT_SERVERCLASS_ST( CMyEntity, DT_MyEntity )
IMPLEMENT_SERVERCLASS_ST( CMyEntity, DT_MyEntity )
SendPropInt( SENDINFO( m_nMyInteger ), 8, SPROP_UNSIGNED ),
SendPropInt( SENDINFO( m_nMyInteger ), 8, SPROP_UNSIGNED ),
Line 44: Line 54:
END_SEND_TABLE()
END_SEND_TABLE()


void SomewhereInYourGameCode( void )
void SomewhereInYourGameCode()
{
{
CreateEntityByName( "myentity" ); // create an object of this entity class
CreateEntityByName( "myentity" ); // create an object of this entity class
}
}
</pre>
</syntaxhighlight>


Client-side entity (client.dll):
===Client-side===


<pre>
<syntaxhighlight lang="cpp>
class C_MyEntity : public C_BaseEntity
class C_MyEntity : public C_BaseEntity
{
{
public:
public:
DECLARE_CLASS( C_MyEntity, C_BaseEntity ); // setup class macros
DECLARE_CLASS( C_MyEntity, C_BaseEntity ); // generic entity class macro
DECLARE_CLIENTCLASS(); // this is a client represtation for a server class  
DECLARE_CLIENTCLASS(); // this is a client representation of a server class  


public:
public:
Line 65: Line 75:
};
};


// link data table DT_Myentity to client class and map variables (RecvProps)
//Link a global entity name to this class (name used in Hammer etc.)
LINK_ENTITY_TO_CLASS( myentity, C_MyEntity );
 
// Link data table DT_MyEntity to client class and map variables (RecvProps)
// DO NOT create this in the header! Put it in the main CPP file.
IMPLEMENT_CLIENTCLASS_DT( C_MyEntity, DT_MyEntity, CMyEntity )
IMPLEMENT_CLIENTCLASS_DT( C_MyEntity, DT_MyEntity, CMyEntity )
RecvPropInt( RECVINFO( m_nMyInteger ) ),
RecvPropInt( RECVINFO( m_nMyInteger ) ),
RecvPropFloat( RECVINFO( m_fMyFloat )),
RecvPropFloat( RECVINFO( m_fMyFloat )),
END_RECV_TABLE()
END_RECV_TABLE()
</pre>
</syntaxhighlight>
 
=Networking entities=


As stated above each game object is an entity, an instance of an entity class. An entity class has, beside a unique class name, a global name that is linked with that entity class. The linkage is done by a macro:
== Networking entities ==


: <code>LINK_ENTITY_TO_CLASS( globalname, CEntityClass );</code>
There are several stages to linking an entity on the server with an entity on the client. The first is to link both C++ classes to the same "Hammer class" with <code>[[LINK_ENTITY_TO_CLASS()]]</code>.


The entity factory that creates world entities placed in a map uses these global names to find the matching entity class. Server-side implementations of an entity class are usually named like CName, while client-side implementations are named C_Name with an additional underscore.  
{{note|Server-side implementations of an entity should be called <code>CMyEntity</code>, while their client-side equivalents should be <code>C_MyEntity</code>. Theoretically they could be called anything, but some of Valve's code assumes you have followed this convention.}}


A Source engine server can handle up to 2048 entities at the same time (see <code>MAX_EDICT_BITS</code> in \public\const.h), where each entity may have 1024 different member variables that are networked to clients (<code>MAX_PACKEDENTITY_PROPS</code>). (A CNetworkArray consisting of X items counts as X properties towards the 1024 limit.) Also, each entity can only send 2K of encoded data at a time.  See [[SDK Known Issues List]] for more information.
You must now tell the server that the entity class should be networked and that a corresponding client class exists with the '''<code>DECLARE_SERVERCLASS()</code>''' macro, which will register the class in a global server class list and reserve a unique class ID. Place it in the class definition (i.e. H file). The corresponding macros '''<code>IMPLEMENT_SERVERCLASS_ST</code>''' and '''<code>END_SEND_TABLE()</code>''' must be placed in the class's implementation (i.e. CPP file) one after another to register the server class and its SendTable (these are covered in the next section).


A common way to address a specific entity is by its entity index ( <code>CBaseEntity::entindex()</code> ). Each time a new entity is instantiated, the engine looks up an unused entity index and assigns it to the new entity. Once the entity object is destroyed, its entity index becomes available again and may be reused for a different entity. Therefore the entity index is not a good way to reference a specific entity over longer times. A better and safer way is to use <code>EHANDLEs</code> (or <code>CBaseHandle</code>) to keep track of entity instances. <code>EHANDLEs</code> encapsulate a 32-bit ID that is unique for an entity instance during the whole game and can be used on both server and client to refer to entity objects (<code>EHANDLEs</code> are a combination of entity index and an increasing serial number). An <code>EHANDLE</code> can be transformed to CBaseEntity and vice versa just by using its overloaded operators. If an <code>EHANDLE</code> resolves to NULL, the entity object is not valid anymore.
Finally you must do the same on the client, this time with '''<code>DECLARE_CLIENTCLASS()</code>''' in the H file and '''<code>IMPLEMENT_CLIENTCLASS_DT</code>''' and '''<code><nowiki />END_RECV_TABLE()</code>'''.


<pre>
When a client connects to a server, they exchange a list of known classes and if the client doesn't implement all server classes the connection is stopped with a message "Client missing DT class <whatever>".
// find a player, if no player is available pPlayer and hPlayer are NULL
EHANDLE hPlayer = gEntList.FindEntityByClassname( NULL, "player" );
CBaseEnity *pPlayer = hPlayer; // convert EHANDLE into entity pointer
m_hPlayer = pPlayer; // convert entity pointer to EHANDLE
</pre>


To tell the server engine that an entity class should be networked and a corresponding client class exists, the <code>DECLARE_SERVERCLASS</code> macro must be placed in the entity's definition. This macro will register the class in a global server class list and reserve a unique class ID. The macro <code>DECLARE_SERVERCLASS</code> has to be added for each derived networkable entity class again. Also the macro <code>IMPLEMENT_SERVERCLASS</code> must be placed in the class implementation to register the server class and its data send table. This is usually done when declaring the server send table (see below).
==Network Variables==
 
On the client side a corresponding class has to be created which needs to place the macro <code>DECLARE_CLIENTCLASS</code> in its class definition and implement the macro <code>IMPLEMENT_CLIENTCLASS_DT</code>. This macro links the client class to a given server class name. When a client connects to a server, they exchange a list of known classes and if the client doesn't implement all server classes the connection is stopped with a message "Client missing DT class CName".
 
=Network Variables=


Entity classes have member variables just like any other classes. Some of these member variables may be server-side only, meaning they are not replicated on clients. More interesting are the member variables, which need to be replicated for the client copy of this entity. Networked variables are essential entity properties like position, angle or health. Anything that is needed to display an entity in its current state on the client must be networked.  
Entity classes have member variables just like any other classes. Some of these member variables may be server-side only, meaning they are not replicated on clients. More interesting are the member variables, which need to be replicated for the client copy of this entity. Networked variables are essential entity properties like position, angle or health. Anything that is needed to display an entity in its current state on the client must be networked.  


Whenever a networked member variable changes, the Source engine must know about it to include an update message for this entity in the next snapshot broadcast. To signal a change of a networked variable, the function <code>NetworkStateChanged()</code> of this entity must be called to set the internal flag <code>FL_EDICT_CHANGED</code>. Once the engine has sent the next update, this flag will be cleared again. Now it wouldn't be very convenient to call <code>NetworkStateChanged()</code> every time you change a member variable, therefore networked variables use a special helper macro <code>CNetworkVar</code> that will replace the original variable type (int, float, bool etc) with a modified type that automatically signals a state change to its parent entity. There exits special macros for the <code>Vector</code> and <code>QAngle</code> class, as well as for arrays and <code>EHANDLES</code>. The practical usage of these <code>CNetworkVars</code> doesn't change, so you can use them just like the original data types (except networked arrays, you have to use <code>Set()</code> or <code>GetForModify()</code> when changing elements). The following example shows how the different CNetwork* macros are used when defining member variables (the comments show the non-networked version):
Whenever a networked member variable changes, the Source engine must know about it to include an update message for this entity in the next snapshot broadcast. To signal a change of a networked variable, the function <code>[[NetworkStateChanged()]]</code> of this entity must be called to set the internal flag <code>FL_EDICT_CHANGED</code>. Once the engine has sent the next update, this flag will be cleared again. Now it wouldn't be very convenient to call <code>NetworkStateChanged()</code> every time you change a member variable, therefore networked variables use a special helper macro <code>[[CNetworkVar]]</code> that will replace the original variable type (int, float, bool etc) with a modified type that automatically signals a state change to its parent entity. There exists special macros for the <code>Vector</code> and <code>QAngle</code> class, as well as for arrays and <code>EHANDLES</code>. The practical usage of these <code>CNetworkVars</code> doesn't change, so you can use them just like the original data types (except networked arrays, you have to use <code>Set()</code> or <code>GetForModify()</code> when changing elements). The following example shows how the different CNetwork* macros are used when defining member variables (the comments show the non-networked version):
 
<syntaxhighlight lang="cpp">
<pre>
CNetworkVar( int, m_iMyInt ); // int m_iMyInt;
CNetworkVar( int, m_iMyInt ); // int m_iMyInt;
CNetworkVar( float, m_fMyFloat ); // float m_fMyFloat;
CNetworkVar( float, m_fMyFloat ); // float m_fMyFloat;
CNetworkVector( m_vecMyVector );  // Vector m_vecMyVector;
CNetworkVector( m_vecMyVector );  // Vector m_vecMyVector;
CNetworkQAngle( m_angMyAngle );  // QAngle m_angMyAngle;
CNetworkQAngle( m_angMyAngle );  // QAngle m_angMyAngle;
CNetworkArray( int, m_iMyArray, 64 ); // int m_iMyArray[64];
CNetworkArray( int, m_iMyArray, 64 ); // int m_iMyArray[64];
CNetworkHandle( CBaseEntity, m_hMyEntity ); // EHANDLE m_hMyEntity;
CNetworkHandle( CBaseEntity, m_hMyEntity ); // EHANDLE m_hMyEntity;
CNetworkString( m_szMyString );  // const char *m_szMyString;
</pre>
</syntaxhighlight>
{{warning|A <code>CNetworkArray</code> cannot be assigned to with normal syntax. Use <code>Set(slot,value)</code> instead. Also, for forward-compatibility, consider using <code>Get(slot)</code> when returning.}}


=Network Data Tables=
==Network Data Tables==


When an entity signals a change and the engine is building the snapshot update, the engine needs to know how to convert a variable value into a bit stream. Of course it could just transfer the memory footprint of the member variable but that would be way too much data in most cases and not very efficient in terms of bandwidth usage. Therefore each entity class keeps a data table that describes how to encode each of its member variables. These tables are called SendTables and must have a unique name, usually like DT_EntityClassName.  
When an entity signals a change and the engine is building the snapshot update, the engine needs to know how to convert a variable value into a bit stream. Of course it could just transfer the memory footprint of the member variable but that would be way too much data in most cases and not very efficient in terms of bandwidth usage. Therefore each entity class keeps a data table that describes how to encode each of its member variables. These tables are called '''Send Tables''' and must have a unique name, usually like <code>DT_EntityClassName</code>.  


Entries of this table are SendProps, objects that keep the encoding description for a member variable. The Source engine provides various different data encoders for the commonly used data types like integer, float, vector and text string. SendProps also store information on how many bits should be used, minimum and maximum values, special encoding flags and send proxy functions (explained later).
Entries of this table are <code>SendProps</code>, objects that keep the encoding description for a member variable. The Source engine provides various different data encoders for the commonly used data types like integer, float, vector and text string. SendProps also store information on how many bits should be used, minimum and maximum values, special encoding flags and send proxy functions (explained later).


Usually you don't create and fill SendProps by yourself but rather use one of the SendProp* helper functions ( SendPropInt(),SendPropFloat() etc). These functions help to set up all important encoding properties in a single line. The SENDINFO macro helps to calculate the member variable size and relative offset to the entity address. Here's an example of a SendTable for the network variables defined earlier.
Usually you don't create and fill SendProps by yourself but rather use one of the SendProp* helper functions ( <code>SendPropInt()</code>,<code>SendPropFloat()</code>, etc). These functions help to set up all important encoding properties in a single line. The <code>SENDINFO</code> macro helps to calculate the member variable size and relative offset to the entity address. Here's an example of a SendTable for the network variables defined earlier.


<pre>
<syntaxhighlight lang="cpp">
IMPLEMENT_SERVERCLASS_ST(CMyClass, DT_MyClass)
IMPLEMENT_SERVERCLASS_ST(CMyClass, DT_MyClass)
  SendPropInt( SENDINFO(m_iMyInt), 4, SPROP_UNSIGNED ),
    SendPropInt( SENDINFO(m_iMyInt), 4, SPROP_UNSIGNED ),
  SendPropFloat( SENDINFO(m_fMyFloat), -1, SPROP_COORD),
    SendPropFloat( SENDINFO(m_fMyFloat), -1, SPROP_COORD),
  SendPropVector( SENDINFO(m_vecMyVector), -1, SPROP_COORD ),
    SendPropVector( SENDINFO(m_vecMyVector), -1, SPROP_COORD ),
  SendPropQAngles( SENDINFO(m_angMyAngle), 13, SPROP_CHANGES_OFTEN ),
    SendPropQAngles( SENDINFO(m_angMyAngle), 13, SPROP_CHANGES_OFTEN ),
  SendPropArray3( SENDINFO_ARRAY3(m_iMyArray), SendPropInt( SENDINFO_ARRAY(m_iMyArray), 10, SPROP_UNSIGNED ) ),
    SendPropArray3( SENDINFO_ARRAY3(m_iMyArray), SendPropInt( SENDINFO_ARRAY(m_iMyArray), 10, SPROP_UNSIGNED ) ),
  SendPropEHandle( SENDINFO(m_hMyEntity)),
    SendPropEHandle( SENDINFO(m_hMyEntity)),
    SendPropString( SENDINFO(m_szMyString) ),
END_SEND_TABLE()
END_SEND_TABLE()
</pre>
</syntaxhighlight>
 
The macro <code>IMPLEMENT_SERVERCLASS_ST</code> automatically links the Send Table of the base class the entity it is derived from, so all inherited properties are already included.


The macro <code>IMPLEMENT_SERVERCLASS_ST</code> automatically links the SendTable of the base class the entity it is derived from, so all inherited properties are already included. If you for some reason don't want to include base class properties, use the <code>IMPLEMENT_SERVERCLASS_ST_NOBASE</code> macro. Otherwise single properties of the base class can be excluded by using the <code>SendPropExclude()</code> helper function. Instead of adding a new SendProp, it removes an existing one from an inherited SendTable.
{{tip|If you for some reason don't want to include base class properties, use <code>IMPLEMENT_SERVERCLASS_ST_NOBASE()</code>. Otherwise single properties of the base class can be excluded by using <code>SendPropExclude()</code>. Instead of adding a new SendProp, it removes an existing one from an inherited Send Table.}}


The first place to start optomizing the bit-stream size is of course the number of bits that should be used for transmission (-1=default). When you know that an integer value can only be a number between 0 and 15, you just need 4 bits instead of 32 (set also flag <code>SPROP_UNSIGNED</code>). Other optimizations can be archived by using proper SendProps flags:
The first place to start optimizing the bit-stream size is of course the number of bits that should be used for transmission (-1=default). When you know that an integer value can only be a number between 0 and 15, you just need 4 bits instead of 32 (set also flag <code>SPROP_UNSIGNED</code>). Other optimizations can be archived by using proper SendProps flags:


{|
; <code>SPROP_UNSIGNED</code>
|SPROP_UNSIGNED
: Encodes an integer as an unsigned integer, don't send a sign bit.
|Encodes an integer as an unsigned integer, don't send a sign bit.
; <code>SPROP_COORD</code>
|-
: Encodes float or Vector components as a world coordinate. The data is compressed, a 0.0 just needs 2 bits and other values may use up to 21 bits.
|SPROP_COORD
; <code>SPROP_NOSCALE</code>
|Encodes float or Vector components as a world coordinate. The data is compressed, a 0.0 just needs 2 bits and other values may use up to 21 bits.
: Write float or Vector components as full 32-bit value to make sure no data loss occurs because of compression.  
|-
; <code>SPROP_ROUNDDOWN</code>
|SPROP_NOSCALE
: Limit high float value to range minus one bit unit.
|Write float or Vector components as full 32-bit value to make sure no data loss occurs because of compression.  
; <code>SPROP_ROUNDUP</code>
|-
: Limit low float value to range minus one bit unit.
|SPROP_ROUNDDOWN
; <code>SPROP_NORMAL</code>
|Limit high float value to range minus one bit unit.
: Float value is a normal in range between -1 to +1, uses 12 bits to encode.
|-
; <code>SPROP_EXCLUDE</code>
|SPROP_ROUNDUP
: Exclude a SendProp again that was added by a base class Send Table. {{note|Don't set this flag manually; use <code>SendPropExclude()</code> instead.}}
|Limit low float value to range minus one bit unit.
; <code>SPROP_CHANGES_OFTEN</code>
|-
: Some properties change very often like player position and view angle (almost with every snapshot). Add this flag for frequently changing SendProps, so the engine can optimize the Send Tables indices to reduce networking overhead.
|SPROP_NORMAL
|Float value is a normal in range between -1 to +1, uses 12 bits to encode.
|-
|SPROP_EXCLUDE
|Exclude a SendProp again that was added by a base class SendTable. Don't set this flag manually, use the SendPropExclude() function instead.
|-
|SPROP_CHANGES_OFTEN
|Some properties change very often like player position and view angle (almost with every snapshot). Add this flag for frequently changing SendProps, so the engine can optimize the SendTables indices to reduce networking overhead.
|}


On the client side you must declare a ReceiveTable similar to the SendTable, so the client knows where to store the transmitted entity properties. If the variables names remain the same in the client-side class, ReceiveTables are just a simple list of received properties (property order doesn't have to match SendTable order). The macro <code>IMPLEMENT_CLIENTCLASS_DT</code> is used to define the ReceiveTable and also links client to the server classes and their SendTable names.  
On the client side you must declare a '''Receive Table''' similar to the Send Table, so the client knows where to store the transmitted entity properties. If the variables names remain the same in the client-side class, Receive Tables are just a simple list of received properties (property order doesn't have to match Send Table order). The macro <code>IMPLEMENT_CLIENTCLASS_DT</code> is used to define the Receive Table and also links client to the server classes and their Send Table names.  


<pre>
<syntaxhighlight lang="cpp">
IMPLEMENT_CLIENTCLASS_DT(C_MyClass, DT_MyClass, CMyClass )
IMPLEMENT_CLIENTCLASS_DT(C_MyClass, DT_MyClass, CMyClass )
RecvPropInt( RECVINFO ( m_iMyInt ) ),
RecvPropInt( RECVINFO ( m_iMyInt ) ),
RecvPropFloat( RECVINFO ( m_fMyFloat ) ),
RecvPropFloat( RECVINFO ( m_fMyFloat ) ),
RecvPropVector( RECVINFO ( m_vecMyVector ) ),
RecvPropVector( RECVINFO ( m_vecMyVector ) ),
RecvPropQAngles( RECVINFO ( m_angMyAngle ) ),
RecvPropQAngles( RECVINFO ( m_angMyAngle ) ),
RecvPropArray3( RECVINFO_ARRAY(m_iMyArray), RecvPropInt( RECVINFO(m_iMyArray [0]))),
RecvPropArray3( RECVINFO_ARRAY(m_iMyArray), RecvPropInt( RECVINFO(m_iMyArray [0]))),
RecvPropEHandle( RECVINFO (m_hMyEntity) ),
RecvPropEHandle( RECVINFO (m_hMyEntity) ),
RecvPropString( RECVINFO (m_szMyString) ),
END_RECV_TABLE()
END_RECV_TABLE()
</pre>
</syntaxhighlight>


The relative offset and size of a member variable is calculated by the <code>RECVINFO</code> macro. If the server and client variable name is different, the <code>RECVINFO_NAME</code> must be used.
The relative offset and size of a member variable is calculated by the <code>RECVINFO</code> macro. If the server and client variable name is different, the <code>RECVINFO_NAME</code> must be used.


=Transmission Filters=
==Transmission Filters==
 
{{note|If an entity transmits, it will force all of its parents to transmit as well.}}
 
Transmitting all entity updates to all clients would be an unnecessary waste of bandwidth, as a player usually sees only a small subset of the world. In general a server needs to update only entities that are in the local vicinity of a player, but there are also cases where an entity is only of interest to players on a certain team or of a certain combat class.
 
Areas or rooms visible from a player's position are called the [[PVS|Potential Visiblity Set]]. A player's PVS is usually used to filter entities before transmitted to the client, but more complex filter rules can also be defined in an entity's '''<code>UpdateTransmitState()</code>''' and '''<code>ShouldTransmit()</code>''' virtual functions.


Transmitting all entity updates to all clients would be an unnecessary waste of bandwidth assuming that a player usually sees only a small subset of all objects in the world. In general a server needs to update only entities that are in the local vicinity of a player. Areas or rooms visible from a player's position are called the "Potential Visible Set" (PVS) of a player. A player's PVS is usually used to filter physical entities before transmitted to the client, though entities can define more complex filter rules in their virtual functions <code>UpdateTransmitState()</code> and <code>ShouldTransmit(…)</code>. Specifically, logical entities use these filters since they have no 'position' and are often interesting only for players in a certain team or players of a certain game class. An entity sets its global transmission state in <code>UpdateTransmitState()</code> where it can choose one of the following states:
=== UpdateTransmitState() ===


{|
An entity sets its global transmission state in <code>UpdateTransmitState()</code>, where it can choose one of the following states:
|FL_EDICT_ALWAYS
 
|Always transmit this entity.
; <code>FL_EDICT_ALWAYS</code>
|-
: Always transmit.
|FL_EDICT_DONTSEND
; <code>FL_EDICT_DONTSEND</code>
|Don't ever transmit this entity.
: Don't ever transmit.
|-
; <code>FL_EDICT_PVSCHECK</code>
|FL_EDICT_PVSCHECK
: Have the engine check against the PVS (this could also be done manually).
|Always transmit entity, but check against the PVS.
; <code>FL_EDICT_FULLCHECK</code>
|-
: Call <code>ShouldTransmit()</code> to decide whether or not to transmit. This creates lots of extra function calls, so only use it when needed.
|FL_EDICT_FULLCHECK
 
|Call ShouldTransmit() to decide whether or not to transmit the entity. This is expensive, so only use it when needed.
If an entity changes its state so the transmission state would change too (e.g. becomes invisible etc), <code>UpdateTransmitState()</code> will be called.
|}


If an entity changes its state so the transmission state would change too (e.g. becomes invisible etc), <code>UpdateTransmitState()</code> has to be called. In general you could mark all entities as <code>FL_EDICT_FULLCHECK</code> and place all transmission rules in <code>ShouldTransmit(…)</code>, but that would be very expensive since the engine whould have to call <code>ShouldTransmit(…)</code> for every entity for every client about 30 times a second (see <code>CServerGameEnts::CheckTransmit(…)</code>). There are better ways to waste your server's CPU power. So whenever possible choose one of the other transmission states.
=== ShouldTransmit() ===


Still some entities have quite complex transmission rules (player entities, weapons etc) and calling <code>ShouldTransmit()</code> is unavoidable. A derived implementation of <code>CBaseEntity::ShouldTransmit(const CCheckTransmitInfo *pInfo)</code> must return one of the transmission flags except <code>FL_EDICT_FULLCHECK</code> (that would be recursive). In this case the transmission flags have the following meanings:
Some entities have complex transmission rules and the performance impact of calling <code>ShouldTransmit()</code> is unavoidable. A derived implementation of <code>CBaseEntity::ShouldTransmit(const CCheckTransmitInfo *pInfo)</code> must return one of the following transmission flags:


{|
; <code>FL_EDICT_ALWAYS</code>
|FL_EDICT_ALWAYS
: Transmit this time.
|Transmit the entity this time.
; <code>FL_EDICT_DONTSEND</code>
|-
: Don't transmit this time.
|FL_EDICT_DONTSEND
; <code>FL_EDICT_PVSCHECK</code>
|Don't transmit the entity this time.
: Transmit this time if the entity is inside the PVS.
|-
|FL_EDICT_PVSCHECK
|Transmit the entity if it's inside the client's PVS.
|}


The argument structure passed to <code>CCheckTransmitInfo</code> gives information about the receiving client, its current PVS and what other entities are already marked for transmission.
The argument structure passed to <code>CCheckTransmitInfo</code> gives information about the receiving client, its current PVS and what other entities are already marked for transmission.


==Send & Receive Proxies==
'''Send and receive proxies''' are callback functions implemented in Send/ReceiveProps. They are executed whenever the property is transmitted, and are commonly used to compress a value for transmission (in which case one is required at each end), to send one value to many locations, or simply to detect when a networked variable changes.
{{warning|Do not run entity logic from proxies under any circumstances. They can be run at unexpected times (e.g. during [[prediction]] validation) or not run at all (e.g. [[full update]]s). Use <code>[[PostDataUpdate()]]</code> instead.}}
=== Proxies on datatables ===
You can filter which players receive a datatable by including it in a parent table using <code>SendPropDataTable()</code>, then applying a SendProxy.
In this case replace <code>DVariant* pOut</code> from the SendProxy argument list below with <code>CSendProxyRecipients* pRecipients</code>, an object which holds a list of clients who will receive the table. It takes the index of the client(s), with the first player at 0.


=Send & Receive Proxies=
There are two proxies already set up for the most common scenario of sending high-precision data to the local player for use in [[prediction]]:


Send and receive proxies are callback functions linked to specific Send/ReceiveProps. If installed for an entity property, these callback functions are executed whenever it's being sent or received. Proxy functions are installed when declaring entity properties in SendTables or ReceiveTables. These proxy functions allow you to modify outgoing and incoming data so you can add custom encoder and decoder implementations. A receive proxy can also be used to detect changes on a entity property without changing the incoming data. But be aware that a receive proxy is not always called if the property value really changes, for example when server is sending a full, not delta, compressed snapshot update. The following example strips the lower 2 bit of an integer before transmission, which saves bandwidth but causes a loss of data precision.
* <code>SendProxy_SendLocalDataTable</code>
* <code>SendProxy_SendNonLocalDataTable</code>


Installing the server send proxy:
=== Arguments ===
{| class=standard-table
|-
! SendProxy (Server)
! RecvProxy (Client)
|-
| width=50% style="padding:5pt;vertical-align:top;" |
; <code>SendProp* pProp</code>
: The SendProp that is using this proxy.
; <code>void* pStructBase</code> / <code>pStruct</code>
: The entity that owns the SendProp that is using this proxy. Cast it to fit your requirements.
; <code>void* pData</code>
: The raw data that is to be manipulated. Again, cast as required.
; <code>DVariant* pOut</code>
: Object that receives the output data to be sent to clients. Assign using one of its function.
; <code>int iElement</code>
: The array element index, or 0 if this isn't an array.
; <code>int objectID</code>
: The [[entity index]] of the object being referred to.
| width=50% style="padding:5pt;vertical-align:top;" |
; <code>CRecvProxyData* pData</code>
: An object containing data from the SendProp:
:* <code>RecvProp* m_pRecvProp</code>
:* <code>DVariant* m_Value</code>
:* <code>int m_iElement</code>
:* <code>int m_ObjectID</code>
: See the left column for details on what these are.
; <code>void* pStruct</code>
: The client-side entity that is currently being dealt with. Cast it to fit your requirements.
; <code>void* pOut</code>
: The value that will be applied to the client entity when the proxy has finished running. Cast it to the type you require, or alternatively cast values you assign to it to <code>void*</code>.
|}


<pre>
=== Example ===
void SendProxy_MyProxy( const SendProp *pProp, const void *pStruct,  
 
const void *pData, DVariant *pOut, int iElement, int objectID )
This example strips the lower two bits of an integer, which saves bandwidth but causes a loss of precision.
 
<syntaxhighlight lang="cpp">
void SendProxy_MyProxy( const SendProp* pProp, const void* pStruct,  
const void* pData, DVariant* pOut, int iElement, int objectID )
{
{
// get original variable value
// get raw value
int value = *(int*)pData;
int value = *(int*)pData;


Line 229: Line 275:
IMPLEMENT_SERVERCLASS_ST(CMyClass, DT_MyClass)
IMPLEMENT_SERVERCLASS_ST(CMyClass, DT_MyClass)
SendPropInt( SENDINFO(m_iMyInt ), 4, SPROP_UNSIGNED, SendProxy_MyProxy ),
SendPropInt( SENDINFO(m_iMyInt ), 4, SPROP_UNSIGNED, SendProxy_MyProxy ),
...
END_SEND_TABLE()
END_SEND_TABLE()
</pre>
</syntaxhighlight>
 
Installing the client receive proxy:


<pre>
<syntaxhighlight lang="cpp">
void RecvProxy_MyProxy( const CRecvProxyData *pData, void *pStruct, void *pOut )
void RecvProxy_MyProxy( const CRecvProxyData* pData, void* pStruct, void* pOut )
{
{
// get the transmitted value
// get the transmitted value
int value = *((unsigned long*)&pData->m_Value.m_Int);
int value = *((unsigned long*)&pData->m_Value.m_Int);


// restore value and write to destination address
// restore value to original magnitude
*((unsigned long*)pOut) = value << 2;
*((unsigned long*)pOut) = value << 2;
}
}


IMPLEMENT_CLIENTCLASS_DT(C_MyClass, DT_MyClass, CMyClass )
IMPLEMENT_CLIENTCLASS_DT(C_MyClass, DT_MyClass, CMyClass )
RecvPropInt( RECVINFO ( m_iMyInt ), 0, RecvProxy_MyProxy ),
RecvPropInt( RECVINFO ( m_iMyInt ), 0, RecvProxy_MyProxy ),
...
END_RECV_TABLE()
END_RECV_TABLE()
</pre>
</syntaxhighlight>
 
This example converts a received [[W:HSL and HSV|hue]] value into two separate colour variables on the prop's attached entity.
 
{{tip|Since the proxy function is not a part of the target entity's class, it may be unable to access private or protected members. Declare it in the class with the <code>friend</code> keyword, e.g. <code>friend void RecyProxy_MyProxy(args)</code>, to grant permission.}}
 
<syntaxhighlight lang="cpp">
void RecvProxy_PlayerHue( const CRecvProxyData* pData, void* pStruct, void* pOut )
{
HSVtoRGB( Vector(pData->m_Value.m_Int,.9,.6), static_cast<C_DeathmatchPlayer*>(pStruct)->m_vPlayerColour_Dark );
HSVtoRGB( Vector(pData->m_Value.m_Int,.4,.9), static_cast<C_DeathmatchPlayer*>(pStruct)->m_vPlayerColour_Light );
}


IMPLEMENT_CLIENTCLASS_DT(C_DeathmatchPlayer, DT_DeathmatchPlayer, CDeathmatchPlayer )
RecvPropInt( RECVINFO(m_PlayerHue),0, RecvProxy_PlayerHue ),
END_NETWORK_TABLE()
</syntaxhighlight>


=Optimizing Bandwidth=
==Optimizing Bandwidth==


Once the network variable and data tables are set up and all entities are working properly, the fun part of network coding begins: optimization. The source engine provides a set of tools to monitor and analyze network traffic. The goal of your optimizations is to lower the average bandwidth usage and avoid network traffic spikes (single, extremely large packets).  
Once the network variable and data tables are set up and all entities are working properly, the fun part of network coding begins: optimization. The source engine provides a set of tools to monitor and analyze network traffic. The goal of your optimizations is to lower the average bandwidth usage and avoid network traffic spikes (single, extremely large packets).  


A well-known tool is Netgraph, which can be enabled by executing <code>"net_graph 2"</code> in the developer console. Netgraph shows the most important networking data in a compact form in real-time. Every incoming packet is displayed as color-coded line wandering from right to left, where the line hight represents packet size. The line colors represent different data groups as shown in this diagram:
===Netgraph===
 
A well-known tool is Netgraph, which can be enabled by executing <code>net_graph 2</code> in the developer console. Netgraph shows the most important networking data in a compact form in real-time. Every incoming packet is displayed as color-coded line travelling from right to left, where the line hight represents packet size (NOT latency!). The line colors represent different data groups as shown in this diagram:
 
[[file:Net graph.jpg|net_graph|center]]
 
;fps
:Current frames per seconds at which the screen is refreshing.
;ping
:AKA latency. Network packet travel time between server and client in milliseconds.
;in
:Size of last received packet in bytes, average incoming kB/second and (on the far right), the value of [[cl_updaterate]] above the actual average of packets/second received.
;out
:Size of last-sent packet, average outgoing kB/second and (on the far right) average packets/second sent above the value of [[cl_cmdrate]].
;lerp
:The largest amount of latency the client is configured to compensate for. Default is usually 100ms.
 
Netgraph's position on-screen can be altered with the convars <code>net_graphheight pixels</code> and <code>net_graphpos 1|2|3</code>.


[[Image:Net graph.jpg]]  
{{tip|Use [[net_graphshowlatency]] to display latency on the netgraph.}}


{|
===cl_entityreport===
|'''fps'''
|Current frames per seconds the screen is refreshing.
|-
|'''ping'''
|Network packet travel time between server and client in milliseconds.
|-
|'''in'''
|Size of last received packet in bytes, average incoming kB/ second and average packets/second received.
|-
|'''out'''
|Size of last send packet, average outgoing kB/second and average packets/second send.
|}


The netgraph on screen position can be customized using the console variable <code>"net_graphheight pixels"</code> and <code>"net_graphpos 1|2|3"</code>.
Another visual tool to show entity networking data in real-time is <code>cl_entityreport startindex</code>. When enabling this console variable, a cell will be shown for each networked entity, containing the entity index, class name and a traffic indicator. Depending on your screen resolution hundreds of entities and their activities can be displayed at the same time. The traffic indicator is a small bar showing the transferred bits arrived with the last packet. The red line above the bar shows recent peaks. The entity text is color-coded representing the current transmission status:


Another visual tool to show entity networking data in real-time is <code>"cl_entityreport startindex"</code>. When enabling this console variable, a cell will be shown for each networked entity, containing the entity index, class name and a traffic indicator. Depending on your screen resolution hundreds of entities and their activities can be displayed at the same time. The traffic indicator is a small bar showing the transferred bits arrived with the last packet. The red line above the bar shows recent peaks. The entity text is color-coded representing the current transmission status:
[[file:Cl entityreport.jpg|cl_entityreport]]


[[Image:Cl entityreport.jpg]]
;none
:Entity index never used/transmitted.
;flashing
:Entity PVS state change.
;green
:Entity in PVS, but not updated recently.
;blue
:Entity in PVS generating ongoing traffic.
;red
:Entity still exists outside PVS, but not updated anymore.


{|
===dtwatchent===
|'''none'''
|Entity index never used/transmitted.
|-
|'''flashing'''
|Entity PVS state change.
|-
|'''green'''
|Entity in PVS, but not updated recently.
|-
|'''blue'''
|Entity in PVS generating ongoing traffic.
|-
|'''red'''
|Entity still exists outside PVS, but not updated anymore.
|}


Once you've identified a single entity constantly generating traffic or peaks you can watch that entity a bit closer using the console tool <code>"dtwatchent entityindex"</code>. This will generate console output with each received update showing all changed member variables. For each changed property the variable name, type, SendTable index, bits used for transmission and new value is listed. Here an example output for the local player entity <code>"dtwatchent 1"</code>:
Once you've identified a single entity constantly generating traffic or peaks you can watch that entity a bit closer using the console tool <code>dtwatchent entityindex</code>. This will generate console output with each received update showing all changed member variables. For each changed property the variable name, type, SendTable index, bits used for transmission and new value is listed. Here an example output for the local player entity <code>dtwatchent 1</code>:
{{codeBlock|<nowiki> delta entity: 1
+ m_flSimulationTime, DPT_Int, index 0, bits 8, value 17
+ m_vecOrigin, DPT_Vector, index 1, bits 52, value (171.156,-83.656,0.063)
+ m_nTickBase, DPT_Int, index 7, bits 32, value 5018
+ m_vecVelocity[0], DPT_Float, index 8, bits 20, value 11.865
+ m_vecVelocity[1], DPT_Float, index 9, bits 20, value -50.936
= 146 bits (19 bytes)
</nowiki>}}


<pre>
===DTI===
delta entity: 1
+ m_flSimulationTime, DPT_Int, index 0, bits 8, value 17
+ m_vecOrigin, DPT_Vector, index 1, bits 52, value (171.156,-83.656,0.063)
+ m_nTickBase, DPT_Int, index 7, bits 32, value 5018
+ m_vecVelocity[0], DPT_Float, index 8, bits 20, value 11.865
+ m_vecVelocity[1], DPT_Float, index 9, bits 20, value -50.936
= 146 bits (19 bytes)
</pre>


An even deeper analysis of all entity classes and average bandwidth usage of their properties is possible by using the Data Table Instrumentation (DTI). To enable DTI, the Source engine (client) must be started with a special <code>"-dti"</code> command line parameter, e.g <code>"hl2.exe -game mymod -dti"</code>. Then DTI runs automatically in the background collecting data about all transmission activities. To gather a good sample of data, just connect to a game server of your mod and play for a couple of minutes. Then quit the game or run the console command <code>"dti_flush"</code> and the engine will write all collected data to files in your mod directory. The created files contain the data in a tabulator separated text format, which can be imported by data processing tools like MS Excel for further analysis. The most interesting file here is <code>"dti_client.txt"</code> where each data record contains the following fields:
An even deeper analysis of all entity classes and average bandwidth usage of their properties is possible by using the Data Table Instrumentation (DTI). To enable DTI, the Source engine (client) must be started with a special <code>-dti</code> command line parameter, e.g <code>hl2.exe -game mymod -dti</code>. Then DTI runs automatically in the background collecting data about all transmission activities. To gather a good sample of data, just connect to a game server of your mod and play for a couple of minutes. Then quit the game or run the console command <code>dti_flush</code> and the engine will write all collected data to files in your mod directory. The created files contain the data in a tabulator separated text format, which can be imported by data processing tools like MS Excel for further analysis. The most interesting file here is <code>dti_client.txt</code> where each data record contains the following fields:


: Entity class; Property Name; Decode Count; Total Bits; Average Bits; Total Index Bits; Average Index Bits
*Entity class
*Property Name
*Decode Count
*Total Bits
*Average Bits
*Total Index Bits
*Average Index Bits


A good way to search for most expensive entity properties is to sort data by "Total Bits" or "Decode Count".
A good way to search for most expensive entity properties is to sort data by "Total Bits" or "Decode Count".


=Mismatched Class Tables=
==Mismatched Class Tables==
To work, the server and client must use the same class tables, so that each knows how to send and recieve data.
 
To work, the server and client must use the same class tables, so that each knows how to send and receive data.
 
If a server is upgraded with a class table change, any connecting client will receive a "Server uses different class tables" error message and be immediately dumped out.  It is not currently known whether there is any way to tell the HL2.exe core to display some other message, or in any way make a server upgrade more seamless for a novice player.
 
==Other References==
[[Source Multiplayer Networking]]


If a server is upgraded with a class table change, any connecting client will recieve a "Server uses different class tables" error message and be immediately dumped out.  It is not currently known whether there is any way to tell the HL2.exe core to display some other message, or in any way make a server upgrade more seemless for a novice player.
[[Category:Networking]]

Latest revision as of 08:29, 12 July 2024

English (en)Português do Brasil (pt-br)中文 (zh)Translate (Translate)

Source allows up to 255 players to play at the same time in a shared, virtual, real-time world. To synchronize the user input and world changes between players, Source uses a server-client architecture that communicates via UDP/IP network packets. The server, as central authority, processes player input and updates the world according to the game and physics rules. The server frequently broadcasts world updates to all connected clients.

Logical and physical objects in the game world are called 'entities' and are represented in the source code as classes derived from a shared base entity class. Some objects live only on the server (server-side only entities) and some objects live only on the client (client-side only entities), but most cross over. The engine's entity networking system makes sure that these objects stay synchronized for all players.

The networking code has to:

  1. Detect changes in the server-side object
  2. Serialize (transform into a bit-stream) the changes
  3. Send the bit-stream as a network packet
  4. Unserialize the data on the client and update the corresponding client-side object.
    Note.pngNote:If the client-side object does not exist, the client will automatically create it.

Data packets are not sent with every single change made to some object; rather, snapshots (usually 20/sec) are made that contain all entity changes since the last update. Furthmore, not all entity changes are sent to all clients all the time: to keep the network bandwidth as low as possible, only entities that are of possible interest for a client (visible, audible etc.) are updated frequently.

A Source server can handle up to 2048 networked entities at the same time, each entity may have 1024 different member variables that are networked to clients including individual members of an array, and each entity can network up to 2KB of serialised data per-update (e.g. 2048 ASCII characters).

An Example

This all sounds quite complex and difficult to handle, but most of the work is done in the background by the engine. For a mod author it is pretty simple to create a new networked entity class (in fact for simple entities, you often don't need to do anything at all).

For an example in the SDK, search for m_iPing. This illustrates networking an array, and the m_iPing variable appears in the code only a dozen times, so it's easy to find all the parts.

This example gives you an idea of how easy it can be - though things can become very complex once you start optimizing to reduce bandwidth requirements.

Server-side

class CMyEntity : public CBaseEntity
{
public:
	DECLARE_CLASS(CMyEntity, CBaseEntity );	// setup some macros
	DECLARE_SERVERCLASS();  // make this entity networkable

	int UpdateTransmitState()	// always send to all clients
	{
		return SetTransmitState( FL_EDICT_ALWAYS );
	}

public:
	// public networked member variables:
	CNetworkVar( int, m_nMyInteger ); // integer
	CNetworkVar( float, m_fMyFloat ); // floating point
};

//Link a global entity name to this class (name used in Hammer etc.)
LINK_ENTITY_TO_CLASS( myentity, CMyEntity );

// Server data table describing networked member variables (SendProps)
// DO NOT create this in the header! Put it in the main CPP file.
IMPLEMENT_SERVERCLASS_ST( CMyEntity, DT_MyEntity )
	SendPropInt(	SENDINFO( m_nMyInteger ), 8, SPROP_UNSIGNED ),
	SendPropFloat( SENDINFO( m_fMyFloat ), 0, SPROP_NOSCALE),
END_SEND_TABLE()

void SomewhereInYourGameCode()
{
	CreateEntityByName( "myentity" ); // create an object of this entity class
}

Client-side

class C_MyEntity : public C_BaseEntity
{
public:
	DECLARE_CLASS( C_MyEntity, C_BaseEntity ); // generic entity class macro
	DECLARE_CLIENTCLASS(); // this is a client representation of a server class 

public:
	// networked variables as defined in server class
	int	m_nMyInteger;
	float	m_fMyFloat;
};

//Link a global entity name to this class (name used in Hammer etc.)
LINK_ENTITY_TO_CLASS( myentity, C_MyEntity );

// Link data table DT_MyEntity to client class and map variables (RecvProps)
// DO NOT create this in the header! Put it in the main CPP file.
IMPLEMENT_CLIENTCLASS_DT( C_MyEntity, DT_MyEntity, CMyEntity )
	RecvPropInt( RECVINFO( m_nMyInteger ) ),
	RecvPropFloat( RECVINFO( m_fMyFloat )),
END_RECV_TABLE()

Networking entities

There are several stages to linking an entity on the server with an entity on the client. The first is to link both C++ classes to the same "Hammer class" with LINK_ENTITY_TO_CLASS().

Note.pngNote:Server-side implementations of an entity should be called CMyEntity, while their client-side equivalents should be C_MyEntity. Theoretically they could be called anything, but some of Valve's code assumes you have followed this convention.

You must now tell the server that the entity class should be networked and that a corresponding client class exists with the DECLARE_SERVERCLASS() macro, which will register the class in a global server class list and reserve a unique class ID. Place it in the class definition (i.e. H file). The corresponding macros IMPLEMENT_SERVERCLASS_ST and END_SEND_TABLE() must be placed in the class's implementation (i.e. CPP file) one after another to register the server class and its SendTable (these are covered in the next section).

Finally you must do the same on the client, this time with DECLARE_CLIENTCLASS() in the H file and IMPLEMENT_CLIENTCLASS_DT and END_RECV_TABLE().

When a client connects to a server, they exchange a list of known classes and if the client doesn't implement all server classes the connection is stopped with a message "Client missing DT class <whatever>".

Network Variables

Entity classes have member variables just like any other classes. Some of these member variables may be server-side only, meaning they are not replicated on clients. More interesting are the member variables, which need to be replicated for the client copy of this entity. Networked variables are essential entity properties like position, angle or health. Anything that is needed to display an entity in its current state on the client must be networked.

Whenever a networked member variable changes, the Source engine must know about it to include an update message for this entity in the next snapshot broadcast. To signal a change of a networked variable, the function NetworkStateChanged() of this entity must be called to set the internal flag FL_EDICT_CHANGED. Once the engine has sent the next update, this flag will be cleared again. Now it wouldn't be very convenient to call NetworkStateChanged() every time you change a member variable, therefore networked variables use a special helper macro CNetworkVar that will replace the original variable type (int, float, bool etc) with a modified type that automatically signals a state change to its parent entity. There exists special macros for the Vector and QAngle class, as well as for arrays and EHANDLES. The practical usage of these CNetworkVars doesn't change, so you can use them just like the original data types (except networked arrays, you have to use Set() or GetForModify() when changing elements). The following example shows how the different CNetwork* macros are used when defining member variables (the comments show the non-networked version):

 CNetworkVar( int, m_iMyInt );			// int m_iMyInt;
 CNetworkVar( float, m_fMyFloat );		// float m_fMyFloat;
 CNetworkVector( m_vecMyVector );  		// Vector m_vecMyVector;
 CNetworkQAngle( m_angMyAngle );  		// QAngle m_angMyAngle;
 CNetworkArray( int, m_iMyArray, 64 ); 		// int m_iMyArray[64];
 CNetworkHandle( CBaseEntity, m_hMyEntity );	// EHANDLE m_hMyEntity;
 CNetworkString( m_szMyString );  		// const char *m_szMyString;
Warning.pngWarning:A CNetworkArray cannot be assigned to with normal syntax. Use Set(slot,value) instead. Also, for forward-compatibility, consider using Get(slot) when returning.

Network Data Tables

When an entity signals a change and the engine is building the snapshot update, the engine needs to know how to convert a variable value into a bit stream. Of course it could just transfer the memory footprint of the member variable but that would be way too much data in most cases and not very efficient in terms of bandwidth usage. Therefore each entity class keeps a data table that describes how to encode each of its member variables. These tables are called Send Tables and must have a unique name, usually like DT_EntityClassName.

Entries of this table are SendProps, objects that keep the encoding description for a member variable. The Source engine provides various different data encoders for the commonly used data types like integer, float, vector and text string. SendProps also store information on how many bits should be used, minimum and maximum values, special encoding flags and send proxy functions (explained later).

Usually you don't create and fill SendProps by yourself but rather use one of the SendProp* helper functions ( SendPropInt(),SendPropFloat(), etc). These functions help to set up all important encoding properties in a single line. The SENDINFO macro helps to calculate the member variable size and relative offset to the entity address. Here's an example of a SendTable for the network variables defined earlier.

IMPLEMENT_SERVERCLASS_ST(CMyClass, DT_MyClass)
    SendPropInt( SENDINFO(m_iMyInt), 4, SPROP_UNSIGNED ),
    SendPropFloat( SENDINFO(m_fMyFloat), -1, SPROP_COORD),
    SendPropVector( SENDINFO(m_vecMyVector), -1, SPROP_COORD ),		
    SendPropQAngles( SENDINFO(m_angMyAngle), 13, SPROP_CHANGES_OFTEN ),
    SendPropArray3( SENDINFO_ARRAY3(m_iMyArray), SendPropInt( SENDINFO_ARRAY(m_iMyArray), 10, SPROP_UNSIGNED ) ),
    SendPropEHandle( SENDINFO(m_hMyEntity)),
    SendPropString( SENDINFO(m_szMyString) ),
END_SEND_TABLE()

The macro IMPLEMENT_SERVERCLASS_ST automatically links the Send Table of the base class the entity it is derived from, so all inherited properties are already included.

Tip.pngTip:If you for some reason don't want to include base class properties, use IMPLEMENT_SERVERCLASS_ST_NOBASE(). Otherwise single properties of the base class can be excluded by using SendPropExclude(). Instead of adding a new SendProp, it removes an existing one from an inherited Send Table.

The first place to start optimizing the bit-stream size is of course the number of bits that should be used for transmission (-1=default). When you know that an integer value can only be a number between 0 and 15, you just need 4 bits instead of 32 (set also flag SPROP_UNSIGNED). Other optimizations can be archived by using proper SendProps flags:

SPROP_UNSIGNED
Encodes an integer as an unsigned integer, don't send a sign bit.
SPROP_COORD
Encodes float or Vector components as a world coordinate. The data is compressed, a 0.0 just needs 2 bits and other values may use up to 21 bits.
SPROP_NOSCALE
Write float or Vector components as full 32-bit value to make sure no data loss occurs because of compression.
SPROP_ROUNDDOWN
Limit high float value to range minus one bit unit.
SPROP_ROUNDUP
Limit low float value to range minus one bit unit.
SPROP_NORMAL
Float value is a normal in range between -1 to +1, uses 12 bits to encode.
SPROP_EXCLUDE
Exclude a SendProp again that was added by a base class Send Table.
Note.pngNote:Don't set this flag manually; use SendPropExclude() instead.
SPROP_CHANGES_OFTEN
Some properties change very often like player position and view angle (almost with every snapshot). Add this flag for frequently changing SendProps, so the engine can optimize the Send Tables indices to reduce networking overhead.

On the client side you must declare a Receive Table similar to the Send Table, so the client knows where to store the transmitted entity properties. If the variables names remain the same in the client-side class, Receive Tables are just a simple list of received properties (property order doesn't have to match Send Table order). The macro IMPLEMENT_CLIENTCLASS_DT is used to define the Receive Table and also links client to the server classes and their Send Table names.

IMPLEMENT_CLIENTCLASS_DT(C_MyClass, DT_MyClass, CMyClass )
 	RecvPropInt( RECVINFO ( m_iMyInt ) ),
 	RecvPropFloat( RECVINFO ( m_fMyFloat ) ),
 	RecvPropVector( RECVINFO ( m_vecMyVector ) ),		
 	RecvPropQAngles( RECVINFO ( m_angMyAngle ) ),
 	RecvPropArray3( RECVINFO_ARRAY(m_iMyArray), RecvPropInt( RECVINFO(m_iMyArray [0]))),
 	RecvPropEHandle( RECVINFO (m_hMyEntity) ),
 	RecvPropString( RECVINFO (m_szMyString) ),
END_RECV_TABLE()

The relative offset and size of a member variable is calculated by the RECVINFO macro. If the server and client variable name is different, the RECVINFO_NAME must be used.

Transmission Filters

Note.pngNote:If an entity transmits, it will force all of its parents to transmit as well.

Transmitting all entity updates to all clients would be an unnecessary waste of bandwidth, as a player usually sees only a small subset of the world. In general a server needs to update only entities that are in the local vicinity of a player, but there are also cases where an entity is only of interest to players on a certain team or of a certain combat class.

Areas or rooms visible from a player's position are called the Potential Visiblity Set. A player's PVS is usually used to filter entities before transmitted to the client, but more complex filter rules can also be defined in an entity's UpdateTransmitState() and ShouldTransmit() virtual functions.

UpdateTransmitState()

An entity sets its global transmission state in UpdateTransmitState(), where it can choose one of the following states:

FL_EDICT_ALWAYS
Always transmit.
FL_EDICT_DONTSEND
Don't ever transmit.
FL_EDICT_PVSCHECK
Have the engine check against the PVS (this could also be done manually).
FL_EDICT_FULLCHECK
Call ShouldTransmit() to decide whether or not to transmit. This creates lots of extra function calls, so only use it when needed.

If an entity changes its state so the transmission state would change too (e.g. becomes invisible etc), UpdateTransmitState() will be called.

ShouldTransmit()

Some entities have complex transmission rules and the performance impact of calling ShouldTransmit() is unavoidable. A derived implementation of CBaseEntity::ShouldTransmit(const CCheckTransmitInfo *pInfo) must return one of the following transmission flags:

FL_EDICT_ALWAYS
Transmit this time.
FL_EDICT_DONTSEND
Don't transmit this time.
FL_EDICT_PVSCHECK
Transmit this time if the entity is inside the PVS.

The argument structure passed to CCheckTransmitInfo gives information about the receiving client, its current PVS and what other entities are already marked for transmission.

Send & Receive Proxies

Send and receive proxies are callback functions implemented in Send/ReceiveProps. They are executed whenever the property is transmitted, and are commonly used to compress a value for transmission (in which case one is required at each end), to send one value to many locations, or simply to detect when a networked variable changes.

Warning.pngWarning:Do not run entity logic from proxies under any circumstances. They can be run at unexpected times (e.g. during prediction validation) or not run at all (e.g. full updates). Use PostDataUpdate() instead.

Proxies on datatables

You can filter which players receive a datatable by including it in a parent table using SendPropDataTable(), then applying a SendProxy.

In this case replace DVariant* pOut from the SendProxy argument list below with CSendProxyRecipients* pRecipients, an object which holds a list of clients who will receive the table. It takes the index of the client(s), with the first player at 0.

There are two proxies already set up for the most common scenario of sending high-precision data to the local player for use in prediction:

  • SendProxy_SendLocalDataTable
  • SendProxy_SendNonLocalDataTable

Arguments

SendProxy (Server) RecvProxy (Client)
SendProp* pProp
The SendProp that is using this proxy.
void* pStructBase / pStruct
The entity that owns the SendProp that is using this proxy. Cast it to fit your requirements.
void* pData
The raw data that is to be manipulated. Again, cast as required.
DVariant* pOut
Object that receives the output data to be sent to clients. Assign using one of its function.
int iElement
The array element index, or 0 if this isn't an array.
int objectID
The entity index of the object being referred to.
CRecvProxyData* pData
An object containing data from the SendProp:
  • RecvProp* m_pRecvProp
  • DVariant* m_Value
  • int m_iElement
  • int m_ObjectID
See the left column for details on what these are.
void* pStruct
The client-side entity that is currently being dealt with. Cast it to fit your requirements.
void* pOut
The value that will be applied to the client entity when the proxy has finished running. Cast it to the type you require, or alternatively cast values you assign to it to void*.

Example

This example strips the lower two bits of an integer, which saves bandwidth but causes a loss of precision.

void SendProxy_MyProxy( const SendProp* pProp, const void* pStruct, 
	const void* pData, DVariant* pOut, int iElement, int objectID )
{
	// get raw value
	int value = *(int*)pData;

	// prepare value for transmission, will lose precision
	*((unsigned int*)&pOut->m_Int) = value >> 2;
}

IMPLEMENT_SERVERCLASS_ST(CMyClass, DT_MyClass)
	SendPropInt( SENDINFO(m_iMyInt ), 4, SPROP_UNSIGNED, SendProxy_MyProxy ),
END_SEND_TABLE()
void RecvProxy_MyProxy( const CRecvProxyData* pData, void* pStruct, void* pOut )
{
	// get the transmitted value
	int value = *((unsigned long*)&pData->m_Value.m_Int);

	// restore value to original magnitude
	*((unsigned long*)pOut) = value << 2;
}

IMPLEMENT_CLIENTCLASS_DT(C_MyClass, DT_MyClass, CMyClass )
	RecvPropInt( RECVINFO ( m_iMyInt ), 0, RecvProxy_MyProxy ),
END_RECV_TABLE()

This example converts a received hue value into two separate colour variables on the prop's attached entity.

Tip.pngTip:Since the proxy function is not a part of the target entity's class, it may be unable to access private or protected members. Declare it in the class with the friend keyword, e.g. friend void RecyProxy_MyProxy(args), to grant permission.
void RecvProxy_PlayerHue( const CRecvProxyData* pData, void* pStruct, void* pOut )
{
	HSVtoRGB( Vector(pData->m_Value.m_Int,.9,.6), static_cast<C_DeathmatchPlayer*>(pStruct)->m_vPlayerColour_Dark );
	HSVtoRGB( Vector(pData->m_Value.m_Int,.4,.9), static_cast<C_DeathmatchPlayer*>(pStruct)->m_vPlayerColour_Light );
}

IMPLEMENT_CLIENTCLASS_DT(C_DeathmatchPlayer, DT_DeathmatchPlayer, CDeathmatchPlayer )
	RecvPropInt( RECVINFO(m_PlayerHue),0, RecvProxy_PlayerHue ),
END_NETWORK_TABLE()

Optimizing Bandwidth

Once the network variable and data tables are set up and all entities are working properly, the fun part of network coding begins: optimization. The source engine provides a set of tools to monitor and analyze network traffic. The goal of your optimizations is to lower the average bandwidth usage and avoid network traffic spikes (single, extremely large packets).

Netgraph

A well-known tool is Netgraph, which can be enabled by executing net_graph 2 in the developer console. Netgraph shows the most important networking data in a compact form in real-time. Every incoming packet is displayed as color-coded line travelling from right to left, where the line hight represents packet size (NOT latency!). The line colors represent different data groups as shown in this diagram:

net_graph
fps
Current frames per seconds at which the screen is refreshing.
ping
AKA latency. Network packet travel time between server and client in milliseconds.
in
Size of last received packet in bytes, average incoming kB/second and (on the far right), the value of cl_updaterate above the actual average of packets/second received.
out
Size of last-sent packet, average outgoing kB/second and (on the far right) average packets/second sent above the value of cl_cmdrate.
lerp
The largest amount of latency the client is configured to compensate for. Default is usually 100ms.

Netgraph's position on-screen can be altered with the convars net_graphheight pixels and net_graphpos 1|2|3.

Tip.pngTip:Use net_graphshowlatency to display latency on the netgraph.

cl_entityreport

Another visual tool to show entity networking data in real-time is cl_entityreport startindex. When enabling this console variable, a cell will be shown for each networked entity, containing the entity index, class name and a traffic indicator. Depending on your screen resolution hundreds of entities and their activities can be displayed at the same time. The traffic indicator is a small bar showing the transferred bits arrived with the last packet. The red line above the bar shows recent peaks. The entity text is color-coded representing the current transmission status:

cl_entityreport

none
Entity index never used/transmitted.
flashing
Entity PVS state change.
green
Entity in PVS, but not updated recently.
blue
Entity in PVS generating ongoing traffic.
red
Entity still exists outside PVS, but not updated anymore.

dtwatchent

Once you've identified a single entity constantly generating traffic or peaks you can watch that entity a bit closer using the console tool dtwatchent entityindex. This will generate console output with each received update showing all changed member variables. For each changed property the variable name, type, SendTable index, bits used for transmission and new value is listed. Here an example output for the local player entity dtwatchent 1:

delta entity: 1 + m_flSimulationTime, DPT_Int, index 0, bits 8, value 17 + m_vecOrigin, DPT_Vector, index 1, bits 52, value (171.156,-83.656,0.063) + m_nTickBase, DPT_Int, index 7, bits 32, value 5018 + m_vecVelocity[0], DPT_Float, index 8, bits 20, value 11.865 + m_vecVelocity[1], DPT_Float, index 9, bits 20, value -50.936 = 146 bits (19 bytes)

DTI

An even deeper analysis of all entity classes and average bandwidth usage of their properties is possible by using the Data Table Instrumentation (DTI). To enable DTI, the Source engine (client) must be started with a special -dti command line parameter, e.g hl2.exe -game mymod -dti. Then DTI runs automatically in the background collecting data about all transmission activities. To gather a good sample of data, just connect to a game server of your mod and play for a couple of minutes. Then quit the game or run the console command dti_flush and the engine will write all collected data to files in your mod directory. The created files contain the data in a tabulator separated text format, which can be imported by data processing tools like MS Excel for further analysis. The most interesting file here is dti_client.txt where each data record contains the following fields:

  • Entity class
  • Property Name
  • Decode Count
  • Total Bits
  • Average Bits
  • Total Index Bits
  • Average Index Bits

A good way to search for most expensive entity properties is to sort data by "Total Bits" or "Decode Count".

Mismatched Class Tables

To work, the server and client must use the same class tables, so that each knows how to send and receive data.

If a server is upgraded with a class table change, any connecting client will receive a "Server uses different class tables" error message and be immediately dumped out. It is not currently known whether there is any way to tell the HL2.exe core to display some other message, or in any way make a server upgrade more seamless for a novice player.

Other References

Source Multiplayer Networking