Entidades na Rede
O Source permite que até 255 jogadores joguem ao mesmo tempo em um mundo virtual compartilhado e em tempo real. Para sincronizar a entrada do usuário e as mudanças no mundo entre os jogadores, o Source utiliza uma arquitetura cliente-servidor que se comunica por meio de pacotes de rede UDP/IP. O servidor, como autoridade central, processa a entrada do jogador e atualiza o mundo de acordo com as regras do jogo e da física. O servidor frequentemente transmite atualizações do mundo para todos os clientes conectados.
Objetos lógicos e físicos no mundo do jogo são chamados de 'entidades' e são representados no código-fonte como classes derivadas de uma classe base de entidade compartilhada. Alguns objetos existem apenas no servidor (entidades apenas do lado do servidor) e alguns objetos existem apenas no cliente (entidades apenas do lado do cliente), mas a maioria se cruza. O sistema de rede de entidades do motor garante que esses objetos permaneçam sincronizados para todos os jogadores.
O código de rede deve:
- Detectar alterações no objeto do lado do servidor.
- Serializar (transformar em um fluxo de bits) as alterações.
- Enviar o fluxo de bits como um pacote de rede.
- Desserializar os dados no cliente e atualizar o objeto correspondente do lado do cliente.
Notar:Se o objeto do lado do cliente não existir, o cliente o criará automaticamente.
Os pacotes de dados não são enviados a cada alteração feita em algum objeto; em vez disso, são feitas instantâneas (geralmente 20/segundo) que contêm todas as mudanças de entidade desde a última atualização. Além disso, nem todas as alterações de entidade são enviadas para todos os clientes o tempo todo: para manter a largura de banda da rede o mais baixa possível, apenas entidades que possam interessar a um cliente (visíveis, audíveis etc.) são atualizadas com frequência.
Um servidor Source pode lidar com até 2048 entidades em rede ao mesmo tempo, cada entidade pode ter 1024 variáveis de membro diferentes que são transmitidas para os clientes, incluindo membros individuais de uma matriz, e cada entidade pode transmitir até 2KB de dados serializados por atualização (por exemplo, 2048 caracteres ASCII).
Um Exemplo
Tudo isso parece bastante complexo e difícil de lidar, mas a maior parte do trabalho é feita nos bastidores pelo motor. Para um autor de modificações, é bastante simples criar uma nova classe de entidade em rede (na verdade, para entidades simples, muitas vezes você não precisa fazer nada).
Para um exemplo no SDK, procure por m_iPing
. Isso ilustra a rede de uma matriz, e a variável m_iPing
aparece no código apenas algumas vezes, então é fácil encontrar todas as partes.
Este exemplo lhe dá uma ideia de como pode ser fácil - embora as coisas possam se tornar muito complexas uma vez que você começa a otimizar para reduzir os requisitos de largura de banda.
Lado do Servidor
class CMyEntity : public CBaseEntity
{
public:
DECLARE_CLASS(CMyEntity, CBaseEntity ); // configurar alguns macros
DECLARE_SERVERCLASS(); // tornar esta entidade em rede
int UpdateTransmitState() // sempre enviar para todos os clientes
{
return SetTransmitState( FL_EDICT_ALWAYS );
}
public:
// variáveis de membro em rede públicas:
CNetworkVar( int, m_nMyInteger ); // inteiro
CNetworkVar( float, m_fMyFloat ); // ponto flutuante
};
//Vincular um nome de entidade global a esta classe (nome usado no Hammer etc.)
LINK_ENTITY_TO_CLASS( myentity, CMyEntity );
// Tabela de dados do servidor descrevendo variáveis de membro em rede (SendProps)
// NÃO crie isso no cabeçalho! Coloque-o no arquivo CPP principal.
IMPLEMENT_SERVERCLASS_ST( CMyEntity, DT_MyEntity )
SendPropInt( SENDINFO( m_nMyInteger ), 8, SPROP_UNSIGNED ),
SendPropFloat( SENDINFO( m_fMyFloat ), 0, SPROP_NOSCALE),
END_SEND_TABLE()
void EmAlgumLugarNoSeuCodigoDoJogo()
{
CreateEntityByName( "myentity" ); // criar um objeto desta classe de entidade
}
Lado do Cliente
class C_MyEntity : public C_BaseEntity
{
public:
DECLARE_CLASS( C_MyEntity, C_BaseEntity ); // macro de classe de entidade genérica
DECLARE_CLIENTCLASS(); // esta é uma representação do cliente de uma classe de servidor
public:
// variáveis em rede conforme definido na classe do servidor
int m_nMyInteger;
float m_fMyFloat;
};
//Vincular um nome de entidade global a esta classe (nome usado no Hammer etc.)
LINK_ENTITY_TO_CLASS( myentity, C_MyEntity );
// Vincular tabela de dados DT_MyEntity à classe cliente e mapear variáveis (RecvProps)
// NÃO crie isso no cabeçalho! Coloque-o no arquivo CPP principal.
IMPLEMENT_CLIENTCLASS_DT( C_MyEntity, DT_MyEntity, CMyEntity )
RecvPropInt( RECVINFO( m_nMyInteger ) ),
RecvPropFloat( RECVINFO( m_fMyFloat )),
END_RECV_TABLE()
Entidades de Rede
Há várias etapas para vincular uma entidade no servidor com uma entidade no cliente. A primeira é vincular ambas as classes C++ à mesma "classe Hammer" com LINK_ENTITY_TO_CLASS()
.

CMyEntity
, enquanto suas equivalentes do lado do cliente devem ser C_MyEntity
. Teoricamente, elas poderiam ser chamadas de qualquer coisa, mas parte do código da Valve assume que você seguiu esta convenção.Agora você deve informar ao servidor que a classe da entidade deve ser em rede e que existe uma classe cliente correspondente com o macro DECLARE_SERVERCLASS()
, que registrará a classe em uma lista global de classes do servidor e reservará um ID de classe exclusivo. Coloque-o na definição da classe (ou seja, arquivo H). Os macros correspondentes IMPLEMENT_SERVERCLASS_ST
e END_SEND_TABLE()
devem ser colocados na implementação da classe (ou seja, arquivo CPP) um após o outro para registrar a classe do servidor e sua SendTable (estas são cobertas na próxima seção).
Finalmente, você deve fazer o mesmo no cliente, desta vez com DECLARE_CLIENTCLASS()
no arquivo H e IMPLEMENT_CLIENTCLASS_DT
e END_RECV_TABLE()
.
Quando um cliente se conecta a um servidor, eles trocam uma lista de classes conhecidas e se o cliente não implementa todas as classes do servidor, a conexão é interrompida com uma mensagem "Cliente ausente da classe DT <qualquer>".
Variáveis de Rede
As classes de entidade têm variáveis de membro como qualquer outra classe. Algumas dessas variáveis de membro podem ser apenas do lado do servidor, o que significa que elas não são replicadas nos clientes. Mais interessantes são as variáveis de membro que precisam ser replicadas para a cópia do cliente desta entidade. As variáveis de rede são propriedades essenciais da entidade, como posição, ângulo ou saúde. Tudo o que é necessário para exibir uma entidade em seu estado atual no cliente deve ser em rede.
Sempre que uma variável de membro em rede muda, o motor Source deve saber disso para incluir uma mensagem de atualização para esta entidade no próximo snapshot transmitido. Para sinalizar uma mudança de uma variável de rede, a função NetworkStateChanged()
desta entidade deve ser chamada para definir a bandeira interna FL_EDICT_CHANGED
. Uma vez que o motor tenha enviado a próxima atualização, esta bandeira será apagada novamente. Agora, não seria muito conveniente chamar NetworkStateChanged()
toda vez que você muda uma variável de membro, portanto, as variáveis de rede usam um macro auxiliar especial CNetworkVar
que substituirá o tipo de variável original (int, float, bool etc) por um tipo modificado que sinaliza automaticamente uma mudança de estado para sua entidade pai. Existem macros especiais para a classe Vector
e QAngle
, bem como para matrizes e EHANDLES
. O uso prático desses CNetworkVars
não muda, então você pode usá-los como os tipos de dados originais (exceto matrizes em rede, você deve usar Set()
ou GetForModify()
ao alterar elementos). O exemplo a seguir mostra como os diferentes macros CNetwork* são usados ao definir variáveis de membro (os comentários mostram a versão não em rede):
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 CNetworkString( m_szMyString ); // const char *m_szMyString;

CNetworkArray
não pode ser atribuído com a sintaxe normal. Use Set(slot,value)
em vez disso. Além disso, para compatibilidade futura, considere usar Get(slot)
ao retornar.Tabelas de Dados em Rede
Quando uma entidade sinaliza uma alteração e o motor está construindo a atualização do instantâneo, ele precisa saber como converter um valor de variável em um fluxo de bits. Claro, ele poderia simplesmente transferir a pegada de memória da variável de membro, mas isso seria muito dados na maioria dos casos e não muito eficiente em termos de uso de largura de banda. Portanto, cada classe de entidade mantém uma tabela de dados que descreve como codificar cada uma de suas variáveis de membro. Essas tabelas são chamadas de Tabelas de Envio e devem ter um nome único, geralmente como DT_NomeDaClasseEntidade
.
As entradas desta tabela são SendProps
, objetos que mantêm a descrição de codificação para uma variável de membro. O motor Source fornece vários codificadores de dados diferentes para os tipos de dados comumente usados, como inteiro, float, vetor e string de texto. SendProps também armazenam informações sobre quantos bits devem ser usados, valores mínimo e máximo, flags de codificação especiais e funções de proxy de envio (explicadas posteriormente).
Normalmente, você não cria e preenche SendProps por conta própria, mas sim usa uma das funções auxiliares SendProp* ( SendPropInt()
, SendPropFloat()
, etc). Essas funções ajudam a configurar todas as propriedades de codificação importantes em uma única linha. O macro SENDINFO
ajuda a calcular o tamanho da variável de membro e o deslocamento relativo para o endereço da entidade. Aqui está um exemplo de uma SendTable para as variáveis de rede definidas anteriormente.
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()
O macro IMPLEMENT_SERVERCLASS_ST
vincula automaticamente a Tabela de Envio da classe base à entidade da qual é derivada, portanto, todas as propriedades herdadas já estão incluídas.

IMPLEMENT_SERVERCLASS_ST_NOBASE()
. Caso contrário, propriedades individuais da classe base podem ser excluídas usando SendPropExclude()
. Em vez de adicionar um novo SendProp, ele remove um existente de uma Tabela de Envio herdada.O primeiro lugar para começar a otimizar o tamanho do fluxo de bits é, é claro, o número de bits que devem ser usados para a transmissão (-1=padrão). Quando você sabe que um valor inteiro só pode ser um número entre 0 e 15, você só precisa de 4 bits em vez de 32 (defina também a flag SPROP_UNSIGNED
). Outras otimizações podem ser alcançadas usando as flags apropriadas de SendProps:
SPROP_UNSIGNED
- Codifica um inteiro como um inteiro sem sinal, não envia um bit de sinal.
SPROP_COORD
- Codifica os componentes float ou Vector como uma coordenada mundial. Os dados são comprimidos, um 0.0 só precisa de 2 bits e outros valores podem usar até 21 bits.
SPROP_NOSCALE
- Escreva os componentes float ou Vector como valor completo de 32 bits para garantir que não ocorra perda de dados devido à compressão.
SPROP_ROUNDDOWN
- Limita o valor float alto ao intervalo menos uma unidade de bit.
SPROP_ROUNDUP
- Limita o valor float baixo ao intervalo menos uma unidade de bit.
SPROP_NORMAL
- O valor float é um normal no intervalo entre -1 e +1, usa 12 bits para codificar.
SPROP_EXCLUDE
- Exclui novamente um SendProp que foi adicionado por uma Tabela de Envio da classe base.
Notar:Não defina esta flag manualmente; use
SendPropExclude()
em vez disso. SPROP_CHANGES_OFTEN
- Algumas propriedades mudam com muita frequência, como posição do jogador e ângulo de visão (quase em cada instantâneo). Adicione esta flag para SendProps que mudam frequentemente, para que o motor possa otimizar os índices das Tabelas de Envio para reduzir a sobrecarga de rede.
No lado do cliente, você deve declarar uma Tabela de Recebimento semelhante à Tabela de Envio, para que o cliente saiba onde armazenar as propriedades da entidade transmitidas. Se os nomes das variáveis permanecerem os mesmos na classe do lado do cliente, as Tabelas de Recebimento são apenas uma lista simples de propriedades recebidas (a ordem das propriedades não precisa coincidir com a ordem da Tabela de Envio). O macro IMPLEMENT_CLIENTCLASS_DT
é usado para definir a Tabela de Recebimento e também vincula o cliente às classes do servidor e seus nomes de Tabela de Envio.
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 offset relativo e o tamanho de uma variável de membro são calculados pelo macro RECVINFO
. Se o nome da variável no servidor e no cliente for diferente, deve-se usar RECVINFO_NAME
.
Filtros de Transmissão

Transmitir todas as atualizações da entidade para todos os clientes seria um desperdício desnecessário de largura de banda, já que um jogador geralmente vê apenas um pequeno subconjunto do mundo. Em geral, um servidor precisa atualizar apenas as entidades que estão nas proximidades locais de um jogador, mas também há casos em que uma entidade só interessa aos jogadores de um determinado time ou de uma determinada classe de combate.
Áreas ou salas visíveis a partir da posição de um jogador são chamadas de Conjunto de Visibilidade Potencial. O PVS de um jogador geralmente é usado para filtrar entidades antes de transmiti-las para o cliente, mas regras de filtro mais complexas também podem ser definidas nas funções virtuais UpdateTransmitState()
e ShouldTransmit()
de uma entidade.
UpdateTransmitState()
Uma entidade define seu estado global de transmissão em UpdateTransmitState()
, onde pode escolher um dos seguintes estados:
FL_EDICT_ALWAYS
- Sempre transmitir.
FL_EDICT_DONTSEND
- Nunca transmitir.
FL_EDICT_PVSCHECK
- Fazer o motor verificar o PVS (isso também poderia ser feito manualmente).
FL_EDICT_FULLCHECK
- Chamar
ShouldTransmit()
para decidir se deve ou não transmitir. Isso gera muitas chamadas de função extras, então use apenas quando necessário.
Se uma entidade alterar seu estado, de modo que o estado de transmissão também seja alterado (por exemplo, se tornar invisível etc.), UpdateTransmitState()
será chamado.
ShouldTransmit()
Algumas entidades têm regras de transmissão complexas e o impacto de desempenho ao chamar ShouldTransmit()
é inevitável. Uma implementação derivada de CBaseEntity::ShouldTransmit(const CCheckTransmitInfo *pInfo)
deve retornar uma das seguintes flags de transmissão:
FL_EDICT_ALWAYS
- Transmitir desta vez.
FL_EDICT_DONTSEND
- Não transmitir desta vez.
FL_EDICT_PVSCHECK
- Transmitir desta vez se a entidade estiver dentro do PVS.
A estrutura de argumentos passada para CCheckTransmitInfo
fornece informações sobre o cliente receptor, seu PVS atual e quais outras entidades já estão marcadas para transmissão.
Proxies de Envio e Recebimento
Os proxis de envio e recebimento são funções de retorno de chamada implementadas em Send/ReceiveProps. Eles são executados sempre que a propriedade é transmitida e são comumente usados para comprimir um valor para transmissão (nesse caso, um é necessário em cada extremidade), para enviar um valor para muitos locais ou simplesmente para detectar quando uma variável em rede muda.

PostDataUpdate()
em vez disso.Proxis em tabelas de dados
Você pode filtrar quais jogadores recebem uma tabela de dados incluindo-a em uma tabela pai usando SendPropDataTable()
, e depois aplicando um SendProxy.
Neste caso, substitua DVariant* pOut
da lista de argumentos do proxy de envio abaixo por CSendProxyRecipients* pRecipients
, um objeto que contém uma lista de clientes que receberão a tabela. Ele assume o índice do cliente(s), com o primeiro jogador sendo 0.
Já existem dois proxis configurados para o cenário mais comum de enviar dados de alta precisão para o jogador local para uso na previsão:
SendProxy_SendLocalDataTable
SendProxy_SendNonLocalDataTable
Argumentos
SendProxy (Server) | RecvProxy (Client) |
---|---|
|
|
Um exemplo
Este exemplo remove os dois bits inferiores de um inteiro, o que economiza largura de banda, mas causa perda de precisão.
void SendProxy_MyProxy(const SendProp* pProp, const void* pStruct,
const void* pData, DVariant* pOut, int iElement, int objectID)
{
// obter valor bruto
int value = *(int*)pData;
// preparar valor para transmissão, perderá precisão
*((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()
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)
{
// obter o valor transmitido
int value = *((unsigned long*)&pData->m_Value.m_Int);
// restaurar valor para a magnitude original
*((unsigned long*)pOut) = value << 2;
}
IMPLEMENT_CLIENTCLASS_DT(C_MyClass, DT_MyClass, CMyClass)
RecvPropInt(RECVINFO(m_iMyInt), 0, RecvProxy_MyProxy),
END_RECV_TABLE()
Este exemplo converte um valor de matiz recebido em duas variáveis de cor separadas na entidade anexada à propriedade.

friend void RecyProxy_MyProxy(args)
, para conceder permissão.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:
- 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
.

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