There's a lot of Mods who'd
like to do an automatic player-ID system like TFC's, so we thought we'd whip up a quick
explanation on how TFC does it. It's fast and light on network traffic, and can actually
be used for a lot more than just player-ID. TFC has a generic
status bar that sends a bunch of different details to the client. It's used to show player
IDs, sentrygun health, and a bunch of different numerical values. We wanted to keep the
network traffic down, so the status bar format is sent once, and after that only the
variables need to be sent.
The first thing we do for every player is initialise their status
bar. We call this function when we initialise the player's HUD in
UpdateClientData(). It clears the player's Status Bar text,
for reasons you'll see further down.
// Initialise the player's status bar
void CBasePlayer::InitStatusBar()
{
m_flStatusBarDisappearDelay = 0;
m_SbarString1[0] = m_SbarString0[0] = 0;
}
The above variables are defined in CBasePlayer, as follows:
int m_izSBarState[ SBAR_END ];
float m_flNextSBarUpdateTime;
float m_flStatusBarDisappearDelay;
char m_SbarString0[ SBAR_STRING_SIZE ];
char m_SbarString1[ SBAR_STRING_SIZE ];
We have these defines:
#define MAX_ID_RANGE 2048
#define SBAR_STRING_SIZE 128
enum sbar_data
{
SBAR_ID_TARGETNAME = 1,
SBAR_ID_TARGETHEALTH,
SBAR_ID_TARGETARMOR,
SBAR_END,
};
Once we've got the player's Status Bar initialised, we want to
update it regularly. We chose to update it every 0.2 seconds, which we felt was fast
enough... the faster it is, the more CPU time it'll take, so we don't recommend making it
faster.
At the end of UpdateClientData(), we
have this code:
// Update Status Bar
if ( m_flNextSBarUpdateTime < gpGlobals->time )
{
UpdateStatusBar();
m_flNextSBarUpdateTime = gpGlobals->time + 0.2;
}
The main function behind the status bar generates the new status
bar, compares it to the last one sent to the player, and only sends it if it's changed.
The status bar is made up of two parts, the Status Bar Text and the Status Bar State. The
Text is a string that's vaguely printf formatted, and the State is an array of int's that
go with it to create the status bar the client sees. Here's an example:
Status Text: "1 %p1\n2 Health: %i2%%\n3
Armor: %i3%%"
State Array: { 0, 2, 100, 50 }
The format of the status bar is set by the Status Text. In the above
case, the player's status bar would look like this:
<Name of Client 2> Health: 100 Armor: 50
Here's how it works. The Status Text is broken up into a substrings,
separated by '\n'. The first thing in each substring is an
index into the State array. If the value of that array is non-zero, this substring is seen
by the player. So, for instance, the second substring in the above Status Text: "2 Health: %i2%%\n" indexes m_izSBarState[2]. If m_izSBarState[2] is non-zero, the player will see this substring in
the status bar, minus the index itself (e.g. they won't see the 2). This is done so you
can send down a large status string containing all possible parts of your status bar,
instead of sending a new one down everytime you want to change it's format. In the
substrings, you can use some escape sequences, which are replaced by values taken from the
State array. These are as follows:
%pX : Replaced with the name of the
Client specified by index X in the State array.
%iX : Replaced with the integer value of index X in the State
array.
%% : Replaced with a single %
Note that all indexes into the State Array start at 1, not 0. Also
note that there's actually two StatusBars, each a single line. TFC uses the first one to
do ID stuff, and the second one to do TFC Class specific information (Engineer sentrygun
health, Spy's current disguise, etc).
So, to do a simple ID status bar, we just need to send down the
Status Text in the above example, and then just change the values in the State Array to
update the status bar. Here's a simplified version of the TFC UpdateStatusBar()
function, with some comments:
void CBasePlayer::UpdateStatusBar()
{
int newSBarState[ SBAR_END ];
char sbuf0[ SBAR_STRING_SIZE ];
char sbuf1[ SBAR_STRING_SIZE ];
memset( newSBarState, 0, sizeof(newSBarState)
);
strcpy( sbuf0, m_SbarString0 );
strcpy( sbuf1, m_SbarString1 );
Here we create the two Status Text strings (one for each Status
Bar). sbuf0 and sbuf1 are going to store the new State Text strings we want the client to
see, and at the end we'll compare them to the current Status Text strings the client has,
and if they're different, we'll send the new ones down.
// Find an ID Target
TraceResult tr;
UTIL_MakeVectors( pev->v_angle + pev->punchangle );
Vector vecSrc = EyePosition();
Vector vecEnd = vecSrc + (gpGlobals->v_forward * MAX_ID_RANGE);
UTIL_TraceLine( vecSrc, vecEnd, dont_ignore_monsters, edict(), &tr);
if (tr.flFraction != 1.0)
{
if ( !FNullEnt( tr.pHit ) )
{
CBaseEntity *pEntity = CBaseEntity::Instance( tr.pHit );
if (pEntity->Classify() == CLASS_PLAYER )
{
newSBarState[ SBAR_ID_TARGETNAME ] = ENTINDEX( pEntity->edict() );
strcpy( sbuf1, "1 %p1\n2 Health: %i2%%\n3 Armor: %i3%%" );
// allies and medics get to see the targets
health
if ( IsAlly(pEntity) || pev->playerclass == PC_MEDIC )
{
newSBarState[ SBAR_ID_TARGETHEALTH ] = 100 * (pEntity->pev->health /
pEntity->pev->max_health);
newSBarState[ SBAR_ID_TARGETARMOR ] = 100 * (pEntity->pev->armorvalue /
pEntity->maxarmor);
}
m_flStatusBarDisappearDelay =
gpGlobals->time + 1.0;
}
}
else if ( m_flStatusBarDisappearDelay > gpGlobals->time )
{
// hold the values for a short amount of time after viewing the object
newSBarState[ SBAR_ID_TARGETNAME ] = m_izSBarState[ SBAR_ID_TARGETNAME ];
newSBarState[ SBAR_ID_TARGETHEALTH ] = m_izSBarState[ SBAR_ID_TARGETHEALTH ];
newSBarState[ SBAR_ID_TARGETARMOR ] = m_izSBarState[ SBAR_ID_TARGETARMOR ];
}
}
The above code "fires" an invisible bullet out from the
player. If it hits a player, it copies the ID Status Text into sbuf1, and sets the correct
values in the State Array. Next is a chunk of code that checks this player's Class and
sets up sbuf0... it works just the same as sbuf1, so I haven't included that code.
BOOL bForceResend = FALSE;
if ( strcmp( sbuf0, m_SbarString0 ) )
{
MESSAGE_BEGIN( MSG_ONE, gmsgStatusText, NULL, pev );
WRITE_BYTE( 0 );
WRITE_STRING( sbuf0 );
MESSAGE_END();
strcpy( m_SbarString0, sbuf0 );
// make sure everything's resent
bForceResend = TRUE;
}
if ( strcmp( sbuf1, m_SbarString1 ) )
{
MESSAGE_BEGIN( MSG_ONE, gmsgStatusText, NULL, pev );
WRITE_BYTE( 1 );
WRITE_STRING( sbuf1 );
MESSAGE_END();
strcpy( m_SbarString1, sbuf1 );
// make sure everything's resent
bForceResend = TRUE;
}
// Check values and send if they don't match
for (int i = 1; i < SBAR_END; i++)
{
if ( newSBarState[i] != m_izSBarState[i] || bForceResend )
{
MESSAGE_BEGIN( MSG_ONE, gmsgStatusValue, NULL, pev );
WRITE_BYTE( i );
WRITE_SHORT( newSBarState[i] );
MESSAGE_END();
m_izSBarState[i] = newSBarState[i];
}
}
This is the final piece of UpdateStatusBar().
It checks both sbuf0 and sbuf1,
and compares them to m_SbarString0 and m_SbarString1.
If they are different, it sends the new Status Text down to the client, and saves off a
copy of the Text for the next time we update the status bar. This way, we only need to
send a new Status Text to the client when you change the actual Text, which in the case of
the ID, you only do once, the first time they ID someone. Then finally, it iterates
through the State Array and sends down anything that's changed. Obviously, when the Status
Text changes, we need to resend all the State Array, hence the bForceResend.
And that's all there is to it. The Valve client DLL has everything
you need on the client end, so unless you've written your own client dll, you just need to
put the above code into your game DLL, and you'll be good to go.
All the parsing of the Status Text/State is done in the
statusbar.cpp file in the Valve Client DLL, so you've got the code to that and can change
it if you want to add new escape sequences, etc.
I hope that's enough for you guys/gals. I did have to mess with this
code a bit to get it into a readable form, and may have broken it in the process. If
there's problems with it, let me know. It'd be great if all the multiplayer Mods had good
ID functionality, and I'm sure you'll all figure out new things to do with this stuff.
Update:
Some people have been caught on the fact that I didn't include the code to register the
two user messages that send down the statusbar info. Sorry about that.
To register them, add this to the top of player.cpp, where all the other
user messages are defined:
int gmsgStatusText = 0;
int gmsgStatusValue = 0;
And then find CBasePlayer::Spawn(), where we register all the other
user messages and add these lines:
gmsgStatusText = REG_USER_MSG("StatusText", -1);
gmsgStatusValue = REG_USER_MSG("StatusValue", 3);
- Robin |