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 |