
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, |