Networking Events & Messages

From Valve Developer Community
Jump to navigation Jump to search
English (en)Русский (ru)Translate (Translate)


Most network bandwidth between server and client is spent on entity updates (compressed snapshots). Whenever a durable and reliable state must be transmitted, it should be encoded as an entity state and not sent as a message or event. GoldSrc did a lot of reliable state updates via user messages, which created some problems that happen when packets are getting dropped and reliable messages are delivered too late or out of sync with entity changes. Also HLTV broadcasts and demos have problems with events and messages since they can play them back, but don't revert their impact when jumping back in time. That process is much easier with entities since they can easily be reverted to any previous state.

Still, there are situations where sending event-like messages are good to use. The Source Engine uses messages mainly to update general HUD and screen information like chat messages or short visual effects that don't have large impacts. The Source Engine uses three different message systems: game events, user messages and entity messages . Game events should be fired when a general, global event occurs that may be interesting for all players or other subsystems (logging, game analyzers etc) to know about. User messages are just like the old-style GoldSrc user messages and should be used to transmit specific information to single clients. Entity messages are unreliable messages broadcasted by entities to signal unreliable state changes. In general entity messages are not used that often since the normal entity networking allows easy and reliable transmission, therefore they are not discussed here.

Beside events and messages there are two other networking systems to transmit data to clients. First the temporary entities used to create short-lived, non-solid entities, which are not synchronized or updated after their initial spawn. These temp entities are mostly used for visual in game effects like explosions or bullet impacts. Temp entity messages are unreliable and get dropped if too many temp entities are created at the same time (max 32 in multiplayer, 255 in single player per client update).

The last system to mention is the server string table container. String tables are simple index tables that contain strings and optional binary data per entry (up to 4kB). Server string tables are mirrored on all clients and any change or addition made are replicated instantly. String tables can be used to avoid transmitting same strings over and over again and just send their matching string table index instead.

Game Events

Game events are generic gameplay related events that are defined in resource files. Mods can extend existing game events or define new game events. The central object on both the server and client that controls and dispatches game events is the game event manager. This manager loads events, registers event listeners, fires events and delivers them to registered event listeners (see interface IGameEventManager). Event listeners can be local objects on the server (server-side listeners) or remote listeners on a client. The event manager serializes events to send them via the network connection and processes them on the client.

Before using game events they must be defined in a game events resource file and loaded by the game event manager on both server and client. A game event has a unique name and a number of data fields where each data field can be a integer, float or string. Basic game events are defined in resources\gameevents.res (For mods, this is resources\modevents.res). For example, here is the definition for the game event "player_death" that is fired when a player dies:

"player_death"	// a game event, unique name may be 32 characters long
{
	"userid"	short  	// user ID who died				
	"attacker"	short 	// user ID who killed
}

Possible types for data fields are:

string
a zero terminated string
bool
unsigned int, 1 bit
byte
unsigned int, 8 bit
short
signed int, 16 bit
long
signed int, 32 bit
float
float, 32 bit

The game event manager must load all game event resource files before any game events are used:

gameeventmanager->LoadEventsFromFile("resource/gameevents.res");

After that the server can fire these events, which are stored as KeyValues objects. The KeyValues class allows simple write & read access to its data entries:

IGameEvent* pEvent = gameeventmanager->CreateEvent("player_death");
// The event object will be NULL if there aren't any clients connected to the server.
// Always perform a NULL check before setting properties and sending the event.
if (pEvent)
{
    pEvent->SetInt("userid", pVictim->GetUserID());
    pEvent->SetInt("attacker", pScorer->GetUserID());
    gameeventmanager->FireEvent(pEvent);
}

After firing the event, the allocated KeyValues memory is freed by the game event manager. The game event manager serializes the data using the given type information from the resource file and distributes the events to all clients. On the client side an event listener object must register as a listener for this event. There is no limit on how many listeners may register for the same game event (like user messages, which can install only one callback hook). A simple event listener for the "player_death" event could look like this:

class CMyListener : public IGameEventListener2
{
	CMyListener()
	{
		// add myself as client-side listener for this event
		gameeventmanager->AddListener(this, "player_death", false);
	}

	void FireGameEvent(IGameEvent* pEvent)
	{
		// check event type and print message
		if (!strcmp("player_death", pEvent->GetName()))
		{
			Msg("Player ID %i killed player ID %i\n", 
				pEvent->GetInt("attacker"), pEvent->GetInt("userid"));
		}
	}
};
Note.pngNote:If your client is receiving the event, but its values are all empty, check the gameevents.res or modevents.res files and see if the event's key data types are not set to 'local'.

User Messages

User messages, like games events, have unique names that are used to identify themselves. Like the game event manager, user messages are controlled by the CUserMessages class, which registers and installs callback hooks. But unlike game events, user messages aren't automatically serialized or unserialized, that has to be done manually when sending the user message and also when receiving it on the client. Therefore both the client and server code must updated whenever a user message changes.

User messages are registered in the shared function RegisterUserMessages(). Each message reserves a unique name and tells the system its payload size in bytes (or -1 if the message has a dynamic size like strings):

usermessages->Register( "MyMessage", 2 ); // send 2 bytes payload

To send user messages the server code needs to specify a group of recipient clients first which is done by creating a CRecipientFilter object. Sending a user message starts with the command UserMessageBegin() followed by a block of WRITE_* commands to fill the user message with data (see enginecallback.h for all available WRITE_* commands). The messages are finished and sent when executing the MessageEnd() command:

CSingleUserRecipientFilter filter ( pBasePlayer ); // set recipient
filter.MakeReliable();  // reliable transmission

UserMessageBegin( filter, "MyMessage" ); // create message 
	WRITE_BYTE( 4 ); // fill message
	WRITE_BYTE( 2 ); // fill message
MessageEnd(); //send message

It's not possible to start a second user message block within an already started user message. The payload of a user message is limited to 255 bytes, if this size is exceeded within a single user message block, the message is not sent and a warning will be shown.

On the client side, the shared function RegisterUserMessages() has to be called to register the same message names & sizes like on the server. Also for every user message a message handler (callback hook) has to be installed. It's not possible to install multiple handlers for the same user message. A message handler gets the message as a bf_read object (class to read bit streams) and must be a function defined as follows:

// declare the user message handler
void __MsgFunc_MyHandler( bf_read &msg )
{
	int x = msg.ReadByte();
	int y = msg.ReadByte();
}

// register message handler once
usermessages->HookMessage( "MyMessage", __MsgFunc_MyHandler );

Usually helper macros like HOOK_MESSAGE or HOOK_HUD_MESSAGE are used to setup the callback functions (defined in hud_macros.h).

Temporary Entities

See Temporary Entity.

String Tables

String tables are replicated data containers with indexed entries that contain a text string and optional binary user data (4 kB maximum). String tables are created on the server and updates are replicated instantly and reliable to all clients. The engine provides an interface INetworkStringTableContainer to both server and client code to manage these string tables. To create a new string table object, a unique table name and the maximum number of entries (must be power of 2) have to be specified. When a string table is created, an access interface INetworkStringTable is returned that can be used to add new entries, find existing entries or change their binary user data. An entry string itself can't be changed any more once it has been added. The same interfaces are used client-side to find or access string tables, but they can't be modified here.

String tables are a very simple and efficient way to transmit larger blocks of text strings (material or resource names etc). The general idea is to save bandwidth by transmitting just string table indices for often used strings. This way the same text string is never transmitted twice. Still, changes to the binary user data may cause expensive network load since they are transmitted as raw data and aren't delta compressed. If binary user data updates too often, you should consider using an entity object instead.

Server code:

INetworkStringTable *g_pMyStringTable = NULL;

CServerGameDLL::CreateNetworkStringTables( void )
{
	g_pMyStringTable= networkstringtable->CreateStringTable( "MyStringTable", 32 );
	... 
}

void InitMyStringTable()
{
	int data = 42; // some binary data
	int index1 = g_pMyStringTable->AddString( true, "SomeString" ); // the 'true' is to indicate this is a server string, 'false' would indicate a client string
	int index2 = g_pMyStringTable->AddString( true, "SomeData", sizeof(data), &data );
	...
);

Client code:

INetworkStringTable *g_pMyStringTable = NULL;

void CHLClient::InstallStringTableCallback( const char *tableName )
{
	if ( !strcmp(tableName, "MyStringTable") )
	{
		// Look up the table 
		g_pMyStringTable = networkstringtable->FindTable( tableName );
	}
	...
}

void UseMyStringTable( int index1, int index2 )
{
	Msg( " %s \n", g_pMyStringTable ->GetString( index1 ) );
	int data = *(int *) g_pMyStringTable ->GetStringUserData( index2, NULL);
}

Server Commands

Another possibility is to use ServerCmd(). This is the easiest way for the client to send information to the server, and skips data tables, but it requires a relatively large amount of bandwidth.