Networking Entities: Difference between revisions
Line 19: | Line 19: | ||
==An Example== | ==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 <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. | |||
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=== | |||
{{codeBlock|<nowiki>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 | |||
} | |||
</nowiki>}} | |||
===Client-side=== | |||
{{codeBlock|<nowiki>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() | |||
</nowiki>}} | |||
== Networking entities == | == Networking entities == |
Revision as of 21:17, 11 April 2024
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:
- 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.
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
Client-side
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()
.

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):

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
Transmission Filters

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
Optimizing Bandwidth
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.