HUD Elements
January 2024
In games, it is often important to broadcast information to the player through the use of a HUD (head-up display). This often consists the player's health, the amount of ammunition they are carrying, or a message about an objective. Many different elements normally comprise this HUD. This article will explain how to create those elements and use them to display information to the user.
HUD elements usually use the VGUI2 library (except in Counter-Strike: Global Offensive, and some third-party games like Black Mesa) to render their state. This allows them to not only look and feel like the other VGUI elements within the game, but also allows them to use scripted, animated components, greatly increasing their visual quality. This requires them to have both code components declared on the client, and script files residing on the client as well, external to the code.
HUD element classes should descend from the CHudElement
base class. This base class deals with updating, drawing, as well as hiding HUD elements based on certain game state (a HUD element can be set to disappear when the player dies and so on). This class also interprets the HudLayout.res
file in the /scripts
directory of the game to determine the positions and behaviors of all the elements. Most elements will descend from the vgui::Panel
base class. This provides them with a rudimentary canvas on which to draw text, shapes or textures. For more information on the vgui::Panel
class, see the VGUI2 documentation provided in this SDK.
HUD Messages
Elements use a simple system for capturing messages sent from the server. A HUD element declares a callback function and the corresponding message to link it to. When a message is received, the callback function generally reads some piece of information out of the message and displays or otherwise updates its visual state. An example of this is the Damage message sent by the server when a player takes damage. The message is encoded with the amount of damage, as well as the location. The Health HUD element receives the message and plays animations as well as alters its numerical read-out to reflect the new health of the player.
Message handlers are declared by the HUD element via the following macro:
DECLARE_HUD_MESSAGE( CMyHUDClass, MyHUDMessage );
The first parameter specifies the class declaring the message handler, the second is the message in question. The macro expands the message's name into a callback function which must be defined in the HUD element class. In this case, the callback function created would be:
void MsgFunc_MyHUDMessage( bf_read &msg );
This function's prototype and body must be filled out using the above declaration. The bf_read
class is a data buffer with various query methods. It is used to transmit unformatted data between the server and client. The server uses the bf_write
class to send this data.
The HUD element must also include the HOOK_HUD_MESSAGE
macro definition, generally placed within the Init()
function of the HUD element. Extending our above example, the definition would be:
HOOK_HUD_MESSAGE( CMyHUDClass, MyHUDMessage );
This macro registers the message and links it to the callback function we defined earlier. Failure to include this declaration will result in an assertion and failure when a user message is sent from the server.
Sending Messages From the Server
For messages to be properly received by the client, they must be declared and sent from the server. This is accomplished via the Register() function in the usermessages singleton.
void CUserMessages::Register( const char *name, int size )
|
This function creates a message definition and retains it for the life of the session. When a message is sent, the name specified here is used to identify the message as it is sent to the client.
name
size
|
All user messages should be declared in the RegisterUserMessages()
global function. This function is called upon the instantiation of the usermessage singleton. User messages not declared will not be properly received by the client and will produce an error on attempting to do so.
If all the steps above are followed, the HUD element should now have a working infrastructure for sending and receiving messages to and from the server and client. To send a message, we use the UserMessageBegin()
, MessageEnd()
, and supporting macros listed below.
The UserMessageBegin() function is defined as:
void UserMessageBegin( IRecipientFilter& filter, const char *messagename )
|
This function constructs a user message of the given type (by name), and prepares it for accepting data from the user. The filter can be of any IRecipientFilter type (CSingleUserRecipientFilter , CBroadcastRecipientFilter , etc) as defined in ../dlls/recipientfilter.h .
filter
messagename
|
The following macros provide functionality for writing data into the stream sent to the client. They must be received and processed in the order they were sent. The macros are written as follows:
...
WRITE_BYTE( m_uchMyByte );
WRITE_VEC3COORD( m_vecMyOrigin );
WRITE_BOOL( m_bMyState );
...
The following is a description of all available macros for writing data the message stream:
WRITE_BYTE |
One byte |
WRITE_CHAR |
One character |
WRITE_SHORT |
One short |
WRITE_WORD |
One word |
WRITE_LONG |
One long |
WRITE_FLOAT |
One float |
WRITE_ANGLE |
Unsigned 8-bit angle |
WRITE_COORD |
Compressed coordinate value |
WRITE_VEC3COORD |
Compressed coordinate value from Vector type |
WRITE_VEC3NORMAL |
Compressed normal value from Vector type |
WRITE_ANGLES |
Compressed angles value from Vector type |
WRITE_STRING |
Character string |
WRITE_ENTITY |
Entity index (short) |
WRITE_BOOL |
One bit boolean value |
WRITE_UBITLONG |
Unsigned bit long value |
WRITE_SBITLONG |
Signed bit long value |
WRITE_BITS |
Number of bit values, as specified via parameter |
Following the WRITE_
macros, the message must be terminated and sent via the MessageEnd()
function.
Receiving Messages on the Client
Once a message has been sent by the server, the client will receive it via the callback function hooked to that message. The receiving callback function is passed a bf_read class instance which contains the data passed from the server. The class contains utility functions to read formatted data out of the stream. Again, the data must be read in the order it was sent.
Now that data is sent and received between the server and client, the painting functions (as described in the VGUI2 document) may be used to draw any information it wishes based on that data.
To catch these messages on the client side, they must be hooked, adding a message hook requires that the instance of its class won't get destroyed.
class CHudThingy : public CHudElement , public vgui::Panel
{
DECLARE_CLASS_SIMPLE( CHudThingy, vgui::Panel ); // THIS IS IMPORTANT
....
void MsgFunc_SayText(bf_read &msg) { /* Do something with the message */ };
}
DECLARE_HUD_MESSAGE( CHudThingy, SayText );
// Inits the advanced chat
void CHudAdvancedChat::Init( void )
{
...
HOOK_HUD_MESSAGE( CHudThingy, SayText );
}
Example Code
An example HUD element can be found in the following files, included with the sample application:
../cl_dll/sdk/sdk_hud_message.cpp
../dlls/sdk/sdk_env_message.cpp
../game_shared/sdk/sdk_usermessages.cpp
Showing User Messages In game(code- server plugin)
In this case this function can be added to the project, this is fully commented.
void YourPlugin::SayTextMsg(int PlayerIndexN, const char *Message)
{
MRecipientFilter filter;
if(PlayerIndexN == 0)
{
filter.AddAllPlayers(MaxClients); // We grab the maxclients at the ServerActivate Void
}
else
{
filter.AddRecipient(PlayerIndexN); // Adds the player
}
bf_write *pWrite=engine->UserMessageBegin(&filter, 3); // 3 for Say_Text
if( !pWrite )
{
//TODO: Action to perform when something goes wrong
}
else
{
pWrite->WriteByte(PlayerIndexN); // Players index, to send a global message from the server make it 0
pWrite->WriteString(Message); //the message itself
pWrite->WriteByte(0); //0 to phrase for colour 1 to ignore it
engine->MessageEnd(); //finish off
}
}
Usage:
void YourPlugin::ClientCommand(edict_t *pEntity)
{
const char *pcmd = m_Engine->Cmd_Argv(0);
if ( !pEntity || pEntity->IsFree() )
return; // Continue only if the entity exists
if ( FStrEq( pcmd, "ClientMsg" ) )
SayTextMsg( engine->IndexOfEdict(pEntity), "Hello World!"); // Only the person who typed the command sees this!
else if ( FStrEq( pcmd, "SayServer" ) )
SayTextMsg( 0, "Hello World!"); // Everyone in game sees this:)
...
The RecipientFilter would have to be modified, please look here for more information.