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())



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 */

{
/* 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 */
/* Misc counters and such */ /* Store the clients in order sorted by frags */ /* Misc info about the players */ /* Where to draw on the screen */ /* Some pointers to make life easier when typing */ /* A string for the dog tag (shows who you are and who last killed you */ /* 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)*/ /* Seems to be something they didn't get around to, for now, just initializing the variables */ /* Only because we're limited to size of the screen with those huge pictures */ /* Setting those pointers to save some typing */ /* Where to draw the image on the screen. Picnum is never actually used besides this. Not sure what it's for. */ /* These refer to the image names as we'll shortly see */ /* 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. */ /* This is a built-in layout command (as described above) that also takes care of things like writing "Score:", "Ping:", "Time:" */ /* Copy all this into the string and make sure it's still small enough */ /* 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)
Ideas for improvement
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- ([email protected])


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