Quake DeveLS - Enhanced Deathmatch Scoreboard

Author: druid
Difficulty:
Hard

Intro
Hello everyone. Today we're going to learn a little bit about writing stuff to the screen and focus on implementing that to rewrite the deathmatch scoreboard.

Why?
First of all, why do we want to rewrite the deathmatch scoreboard? Well, if you've ever played any games with over 12 people, you may be a little disturbed since you won't be seeing all of them. Especially in CTF, you need to see everyone? Recently (a few days before I wrote this) id had that base100 server running w/ 70 people. Unless you are really, really good, you won't even see where you are on the list, which is kind of lame. Don't get me wrong, I think the scoreboard as it is is cool for small games since you can see what the person looks like and all, but for larger games, it's just not feasible.

Intro to gi.WriteByte(svc_layout)
First, let's take a look at what's offered to us from the gamex86.dll side for this sort of thing. I'm going to be focusing on what you can do after you do gi.WriteByte(svc_layout). What follows after that (using gi.WriteString(char *), is a null terminated string that contains all of the necessary drawing instructions. The engine then parses it, processes it and draws what was instructed.

Layout information
Here's what I know is possible to send and the layout, there may be more, but I haven't had time to explore it enough.
(for the sample layouts, anything in [ ] should be replaced w/ whatever type it identifies. ex: [int]) (And don't forget the space at the end, this preps it for other things if you are copying it to a bigger string before sending it with gi.WriteString())

Drawing an image:

If you look at the pak0.pak file, you'll see under pics/ (it's near the bottom, just after sounds) a whole bunch of .pcx files that are fair game for this. All of them can be used (the turtle, all the menu graphics, etc). Here's the format for the string you want to write:


"xv [int] yv [int] picn [string] "

xv is the x-value
yv is the y-value
picn is the picture name (no extension) (ex: turtle)

Drawing a full client score:

This outputs the neat little format that you see in the regular scoreboard (minus the portrait and optional dog tag). It's fairly simple, and is just there to save time.


"client [int] [int] [int] [int] [int] [int] "

All the integers:

1. x-value (bottom left)
2. y-value
3. player number
4. score
5. ping
6. time (in minutes) in the map

Writing text:

Outputs a string to the screen. You can specify x,y and pick one of two colors (maybe more?). This makes it possible to highlight some of the text you need people to quickly see.


"xv [int] yv [int] string \"[string]\" "

or

"xv [int] yv [int] string2 \"[string]\" "
xv is the x-value
yv is the y-value
"string" means draw in
white
"string2" means write in
green
string is what to output, must be in quotes (thus use
\")



Original Code
Ok, now that's out of the way, let's take a look at the code as it stands, and I'll throw in some comments where necessary. It's all in p_hud.c under the function DeathmatchScoreboardMessage.

/*
==================
DeathmatchScoreboardMessage
==================
*/
void DeathmatchScoreboardMessage (edict_t *ent, edict_t *killer)


/* OK, the parameters are pretty much self-explanatory. The entity to whom this scoreboard is being shown to, and who last killed him/her */

{

char entry[1024];
char string[1400];
int stringlength;

/* These three are used for storing and keeping track of the string that will eventually be written to gi. Stringlength is just to make sure we don't create a string that is too long for the array */

int i, j, k;

/* Misc counters and such */

int sorted[MAX_CLIENTS];
int sortedscores[MAX_CLIENTS];

/* Store the clients in order sorted by frags */

int score, total;
int picnum;

/* Misc info about the players */

int x, y;

/* Where to draw on the screen */

gclient_t *cl;
edict_t *cl_ent;

/* Some pointers to make life easier when typing */

char *tag;

/* A string for the dog tag (shows who you are and who last killed you */

// sort the clients by score

/* This is pretty simple, and doesn't need to be looked at in the scope of this tutorial (see the bottom for ideas for improvement)*/

// print level name and exit rules
string[0] = 0;
stringlength = strlen(string);

/* Seems to be something they didn't get around to, for now, just initializing the variables */

// add the clients in sorted order
if (total > 12)

total = 12;

 

/* Only because we're limited to size of the screen with those huge pictures */

for (i=0 ; i < total ; i++)
{

cl = &game.clients[sorted[i]];
cl_ent = g_edicts + 1 + sorted[i];

/* Setting those pointers to save some typing */

picnum = gi.imageindex ("i_fixme");
x = (i>=6) ? 160 : 0;
y = 32 + 32 * (i%6);

/* Where to draw the image on the screen. Picnum is never actually used besides this. Not sure what it's for. */

// add a dogtag
if (cl_ent == ent)

tag = "tag1";

else if (cl_ent == killer)

tag = "tag2";

else

tag = NULL;

/* These refer to the image names as we'll shortly see */

if (tag)

{

Com_sprintf (entry, sizeof(entry),

"xv %i yv %i picn %s ",x+32, y, tag);

j = strlen(entry);
if (stringlength + j > 1024)

break;

strcpy (string + stringlength, entry);
stringlength += j;

}

/* This creates a string in the format that the gi will recognize. As you can see the "tag1" or "tag2" are the names of the images. */

// send the layout
Com_sprintf (entry, sizeof(entry),

"client %i %i %i %i %i %i ",
x, y, sorted[i], cl->resp.score, cl->ping, (level.framenum - cl->resp.enterframe)/600);

/* This is a built-in layout command (as described above) that also takes care of things like writing "Score:", "Ping:", "Time:" */

j = strlen(entry);
if (stringlength + j > 1024)

break;

strcpy (string + stringlength, entry);
stringlength += j;

}

/* Copy all this into the string and make sure it's still small enough */

gi.WriteByte (svc_layout);
gi.WriteString (string);

/* OK, that's all the clients, send it to the engine */ }

OK, so there it is, pretty simple, good results, but not optimal for large games. So now we see what kind of improvements we can make.



New and improved scoreboard code
First of all, what my scoreboard looks like is more like Quake 1. I didn't do this because I'm a cynical old jerk, I just thought that having a list like that would be better for displaying a larger amount of information.

Additions and major changes are marked before and after w/
/*=== druid- ====*/
Removals are simply
commented out, for the most part.

Here we go:



/*
==================
DeathmatchScoreboardMessage *Improved!*
==================
*/

void DeathmatchScoreboardMessage (edict_t *ent, edict_t *killer)
{

char entry[1024];
char string[1400];
int stringlength;
int i, j, k;
int sorted[MAX_CLIENTS];
int sortedscores[MAX_CLIENTS];
int score, total;
int picnum;
int x, y;
gclient_t *cl;
edict_t *cl_ent;
char *tag;

// sort the clients by score
total = 0;
for (i=0 ; i
{

cl_ent = g_edicts + 1 + i;
if (!cl_ent->inuse)

continue;

score = game.clients[i].resp.score;
for (j=0 ; j
{

if (score > sortedscores[j])
break;

}
for (k=total ; k>j ; k--)
{

sorted[k] = sorted[k-1];
sortedscores[k] = sortedscores[k-1];

}
sorted[j] = i;
sortedscores[j] = score;
total++;

}

/*=== druid- ====*/


// make a header for the data
Com_sprintf(entry, sizeof(entry),

"xv 32 yv 16 string2 \"Player\" "
"xv 168 yv 16 string2 \"Frags\" "
"xv 216 yv 16 string2 \"Ping\" "
"xv 256 yv 16 string2 \"Time\" "
"xv 32 yv 24 string2 \"--------------------------------\" ");

j = strlen(entry);
if (stringlength + j < 1024)
{

strcpy (string + stringlength, entry);
stringlength += j;

}


/*=== druid- ====*/



// add the clients in sorted order

/*=== druid- ====*/


if (total > 25)

total = 25;

/* The screen is only so big :( */


/*=== druid- ====*/


for (i=0 ; i
{

cl = &game.clients[sorted[i]];
cl_ent = g_edicts + 1 + sorted[i];

picnum = gi.imageindex ("i_fixme");

/*=== druid- ====*/


x = 32;
y = 32 + 8 * i;


/*=== druid- ====*/

 

/*=== druid- ====*/


// "dog tags" are slightly different
// add a dogtag

if (cl_ent == ent)

tag = "-->";

else if (cl_ent == killer)

tag = "-X>";

else

tag = NULL;

if (tag)
{

Com_sprintf (entry, sizeof(entry),

"xv 8 yv %i string \"%s\" ",
y, tag);
j = strlen(entry);
if (stringlength + j > 1024)

break;

strcpy (string + stringlength, entry);
stringlength += j;

}


/*=== druid- ====*/


/* Normal output (takes up too much space!!!)
// send the layout
Com_sprintf (entry, sizeof(entry),

"client %i %i %i %i %i %i ",
x, y, sorted[i], cl->resp.score, cl->ping, (level.framenum - cl->resp.enterframe)/600);

*/

/*=== druid- ====*/


Com_sprintf(entry, sizeof(entry),

"xv 32 yv %i string2 \"%s\" "
"xv 152 yv %i string \"%3i %3i %3i\" ",
y, cl->pers.netname,
y, cl->resp.score,
cl->ping,
(level.framenum - cl->resp.enterframe)/600);


j = strlen(entry);
if (stringlength + j > 1024)

break;

strcpy (string + stringlength, entry);
stringlength += j;


/*=== druid- ====*/

}

gi.WriteByte (svc_layout);
gi.WriteString (string);

}



Remaining issue(s)

Higher resolutions
In Quake1 if you were running higher resolution, you could write a whole lot more to the screen. In Quake2, the coordinates that you are writing to are shifted down and right, so that you're drawable area of the screen is only in the middle. This is a major limitation that I have not found away around yet, but will be a big step in improving the deathmatch scoreboard.

Ideas for improvement

More players!
Which means more compact data. Add a test that either goes to regular output (if <= 12), my output, (if <=25) and an even more compact output (w/ only name and score if necessary) for higher numbers. The maximum length of a name is 15 characters (cl->pers.netname is char[16]), so, with no header, you should be able to squeeze about 60, although some will be overlapping your stats in 320x240. This would also need to color the player's name so he/she could find it quickly.

You could even add a scoreboard that automatically scrolls up and down while the player looks at it. Or an even more complicated one that shows maybe the top 6 in their full glory (w/ portraits and all, and then the rest can be scrolled up and down (either automatically, or using
"+scrollup" / "+scrolldown" messages. I think I'll work on that. And then get a version of the code ready for the next huge player limit test :-).

Intermission
Between levels (at the intermission) you could add a more complete scorelist, showing all the scores, etc, but also having special awards like kamikaze of the match, punching bag of the match, etc. Kind of like in Goldeneye on N64. For CTF it could be how many times each flag was taken/captured/returned, top capturer of the match, or something like that. Or other things, use your imagination.

Teams
For CTF and other mods like it, adding a team column would be very easy.

Final words
Well, I hope everyone got something out of this. If you have questions, comments, or want to encourage me to write more of these as I explore the awesome potential here, write me.

I'm also working on a deathmatch mod that's currently in alpha testing. I need to clean up the display some and add some more features and then I'll release it.

Thanks,
druid- (sfranke@usc.edu)



This site, and all content and graphics displayed on it,
are ©opyrighted to the Quake DeveLS team. All rights received.
Got a suggestion? Comment? Question? Hate mail? Send it to us!
Oh yeah, this site is best viewed in 16 Bit or higher, with the resolution on 800*600.
Thanks to Planet Quake for their great help and support with hosting.
Best viewed with Netscape 4

 

--104A56BD-WebSite-Rules-Byte-Range-Data-104A56BD--