
Quake DeveLS - Scanner
Author: Yaya (-*-)
Difficulty: Medium
NOTE: The file graphics.zip
must be downloaded in order for this tutorial to work!
This Quake mod is
*currently* designed for Deatchmatch only
Preamble
--------
Recently, the idea for a Quake2 scanner came up and I had to have a go at
implementing it. Below is my first attempt at implementing such a device.
There's a fair bit too it, so mind as you go ! :)
First, create a file called "scanner.h" in your Quake 2 DLL project
workspace. Into this, put the following lines of code :
// scanner consts & macros
#define SCANNER_UNIT 32
#define SCANNER_RANGE 100
#define SCANNER_UPDATE_FREQ 1
#define PIC_SCANNER "pics/scanner/scanner.pcx"
#define PIC_SCANNER_TAG "scanner/scanner"
#define PIC_DOT "pics/scanner/dot.pcx"
#define PIC_DOT_TAG "scanner/dot"
#define PIC_ACIDDOT "pics/scanner/aciddot.pcx"
#define PIC_ACIDDOT_TAG "scanner/aciddot"
#define PIC_INVDOT "pics/scanner/invdot.pcx"
#define PIC_INVDOT_TAG "scanner/invdot"
#define PIC_QUADDOT "pics/scanner/quaddot.pcx"
#define PIC_QUADDOT_TAG "scanner/quaddot"
#define PIC_DOWN "pics/scanner/down.pcx"
#define PIC_DOWN_TAG "scanner/down"
#define PIC_UP "pics/scanner/up.pcx"
#define PIC_UP_TAG "scanner/up"
#define PIC_SCANNER_ICON "pics/scanner/scanicon.pcx"
#define PIC_SCANNER_ICON_TAG "scanner/scanicon"
#define SAFE_STRCAT(org,add,maxlen) if ((strlen(org) + strlen(add)) < maxlen) strcat(org,add);
#define LAYOUT_MAX_LENGTH 1400
// scanner functions
void Toggle_Scanner (edict_t *ent);void ShowScanner(edict_t *ent,char *layout);void ClearScanner(gclient_t *client);qboolean Pickup_Scanner (edict_t *ent, edict_t *other);
Explanation : These lines of code are the defining parameters for how the
scanner will operate, and function prototypes. I like to package up my code
like this so that I can easily change things. Don't worry about their exact
function yet, as they'll be explained as they are used in this tutorial.
Next insert the following line at the end of the
"client_persistant_t" structure in "g_local.h"
int scanner_active;
Explanation : This will hold the status for the scanner in the clients
structure. Bit 0 will flag the on/off state, and bit 1 the "just
changed" flag (for immediate update of the scanner when switching on or
off the scores or inventory)
Now insert this line at the end of the function
"InitClientPersistant" in the file "p_client.c" :
ClearScanner(client);
and this as the first line in function "player_die" in
"p_client.c" :
ClearScanner(self->client);
and these in a new file called "scanner.c" :
#include "g_local.h"
#include "scanner.h"
void ClearScanner(gclient_t *client){ client->pers.scanner_active = 0;}
Explanation : Simply clear the players scanner when spawned and when the player
dies.
Now onto something more meaty... :)
Our scanner is going to use bitmap graphics for its display, and for once, the
in-built resources of Quake 2 aren't going to be enough. If you download the
file "graphics.zip", you'll find some graphics as drawn by me ! Btw,
I'm a programmer and not an artist ... :)
Quake 2 can load in graphics in the PCX format so open the Zip file and extract
the contents to your Quake\baseq2 directory, preserving the internal zip
directory structure. You should now have files like :
\quake2\baseq2\pics\scanner\scanner.pcx \quake2\baseq2\pics\scanner\dot.pcx ...
Through trial and error, I found the width of a bitmap should be a multiple of
8, or certainly no smaller, and that the transparent colour is the last one in
the palette. Also note, that you'll have to unpack this zip to every
Quake\baseq2 directory on each machine connected to the game so that all
players can access these client-side resources.
Well, I suppose we'd better load them. Put the following lines at the end of
the function "SP_worldspawn" in g_spawn.c
gi.imageindex (PIC_SCANNER); gi.imageindex (PIC_DOT); gi.imageindex (PIC_INVDOT); gi.imageindex (PIC_QUADDOT); gi.imageindex (PIC_UP); gi.imageindex (PIC_DOWN);
These will cache our new graphics into the Quake game system, using the file
name specifications (see "scanner.h"). Quake will look for files in
the current game's directory structure (this defaults to the "baseq2"
directory). We'll use these later.
Now drop the following into the function "ClientCommand" in
"g_cmds.c", along with the standard client command checks.
else if (Q_stricmp (cmd, "scanner") == 0) Toggle_Scanner (ent);
and the following into "scanner.c" :
void Toggle_Scanner (edict_t *ent)
{ if ((!ent->client) || (ent->health<=0)) return; // toggle low on/off bit (and clear scores/inventory display if required) if ((ent->client->pers.scanner_active ^= 1) & 1) {ent -> client -> showinventory = 0;
ent -> client -> showscores = 0;
} // set "just changed" bit ent->client->pers.scanner_active |= 2;}
Explanation : This will toggle the scanner low on/off bit. If that bit is set,
we're going to clear the variables that flag the display of the scores (in
deathmatch) and the inventory. The reason for this is that this mod is going to
"piccy-back" these display methods to display the scanner. Obviously
we don't want the scanner on with either of these panels. (We're going to be
implementing code that will be used by the "gi.WriteByte(svc_layout)"
function. For an explanation of this method and the parameters involved, see
druid's excellent "Enhanced Deathmatch Scoreboard" tutorial)
To make sure the scanner is removed when the inventory is selected, put these
lines at the end of the function "Cmd_Inven_f" in
"g_cmds.c" (we'll handle the deathmatch score board case shortly)
if (cl->pers.scanner_active & 1) cl->pers.scanner_active = 2;
Now move to the function "DeathmatchScoreboardMessage" in
"p_hud.c". These code is responsible for building the sprite display
list for the top frags. Put these as the first lines in this function :
if (ent -> client -> showscores || ent -> client -> showinventory) if (ent->client->pers.scanner_active) ent->client->pers.scanner_active = 2;
Explanation : If the scores or inventory come on while the scanner is active,
turn it off and flag for an immediate update.
We now need to wedge our code into this routines. Just after the above put :
if (ent -> client -> showscores) {
We're now going to make the scores an optional display. Note : the opening
brace. Move to the end of the function and add the following :
// added ... } else *string = 0; // Scanner active ? if (ent->client->pers.scanner_active & 1) ShowScanner(ent,string);// normal quake code ...
gi.WriteByte (svc_layout); gi.WriteString (string);
Explanation
: If the scores aren't to be displayed clear the stats string. Note : the
closing brace matching the one earlier. If the scanner is active, then add it
to the stat string before sending out to the client.
Next we need to alter how and when Quake 2 calls the deathmatch score board
message display. Alter the lines in "G_SetStats" in
"p_hud.c" (the code near the comment : // layouts)
if (ent->client->pers.health <= 0 || level.intermissiontime || ent->client->showscores)
to :
if (ent->client->pers.health <= 0 || level.intermissiontime || ent->client->showscores || ent->client->pers.scanner_active)
Explanation : Bit 0 of "ent->client->ps.stats[STATS_LAYOUTS] is the
scoreboard update flag, which we need to flag as true if the scanner is on.
Now go to the end of funtion "ClientEndServerFrame" in
"p_view.c" and change :
if (ent->client->showscores && deathmatch->value && !(level.framenum & 31) ) { DeathmatchScoreboardMessage (ent, ent->enemy); gi.unicast (ent, false); }
to :
if (((ent->client->showscores || ent->client->pers.scanner_active) && deathmatch->value && !(level.framenum & SCANNER_UPDATE_FREQ)) || (ent->client->pers.scanner_active & 2)) { DeathmatchScoreboardMessage (ent, ent->enemy); gi.unicast (ent, false); // added ... ent->client->pers.scanner_active &= ~2; }
Explanation : This will cause the death match score board message code to be
called when the scanner is active. Note the update frequency constant.
Normally, Quake updates the scoreboard every 32nd frame - we can change this to
cause a smoother scanner display. This works fine on a local LAN (I have the
frequency set to every other frame in "scanner.h"), but you may need
to experiment with this figure on web play. (I'm unable to practically do this,
but I've made the code easy to change)
Right, we're just about getting there ! We've now got all the auxiliary
routines in, and just need to include "scanner.h" in the following
Quake files (put the line '#include "scanner.h"' just after any other
includes)
g_cmds.c g_spawn.c p_client.c p_hud.c p_view.c
Phew ! Fancy some scanner code ? :) Here's the scanner display function you
drop into "scanner.c" :
void ShowScanner(edict_t *ent,char *layout)
{int i;
edict_t *player = g_edicts;
char stats[64],
*tag;vec3_t v;
// Main scanner graphic draw Com_sprintf (stats, sizeof(stats),"xv 80 yv 40 picn %s ", PIC_SCANNER_TAG); SAFE_STRCAT(layout,stats,LAYOUT_MAX_LENGTH); // Players dots for (i=0 ; i < game.maxclients ; i++) {float len;
int hd;
// move to player edict player++; // in use if (!player->inuse || !player->client || (player == ent) || (player -> health <= 0)) continue; // calc player to enemy vector VectorSubtract (ent->s.origin, player->s.origin, v); // save height differential hd = v[2] / SCANNER_UNIT; // remove height component v[2] = 0; // calc length of distance from top down view (no z) len = VectorLength (v) / SCANNER_UNIT; // in range ? if (len <= SCANNER_RANGE) {int sx,
sy;vec3_t dp;
vec3_t normal = {0,0,-1};
// normal vector to enemy VectorNormalize(v); // rotate round player view angle (yaw) RotatePointAroundVector( dp, normal, v, ent->s.angles[1]); // scale to fit scanner range (80 = pixel range of scanner) VectorScale(dp,len*80/SCANNER_RANGE,dp); // calc screen (x,y) (2 = half dot width) sx = (160 + dp[1]) - 2; sy = (120 + dp[0]) - 2; // setup dot graphic tag = PIC_DOT_TAG; if (player->client->quad_framenum > level.framenum) tag = PIC_QUADDOT_TAG; if (player->client->invincible_framenum > level.framenum) tag = PIC_INVDOT_TAG; // Set output ... Com_sprintf (stats, sizeof(stats),"xv %i yv %i picn %s ", sx, sy, tag); SAFE_STRCAT(layout,stats,LAYOUT_MAX_LENGTH); // clear stats*stats = 0;
// set up/down arrow if (hd < 0) Com_sprintf (stats, sizeof(stats),"yv %i picn %s ", sy - 5,PIC_UP_TAG); else if (hd > 0) Com_sprintf (stats, sizeof(stats),"yv %i picn %s ", sy + 5,PIC_DOWN_TAG); // any up/down ? if (*stats) SAFE_STRCAT(layout,stats,LAYOUT_MAX_LENGTH); } }}
Explanation : First add the main graphic to the display out. This graphic is
160x160 pixels so the screen coords (top left of sprite display) are at (80,40)
on the virtual screen which is 320x200 pixels. We add a reference to the
graphic using the _TAG constants which are the file name without the extension
(in this case .PCX). We then added this string to the output layout string. The
macro SAFE_STRCAT is used to ensure no overflow of the string buffer. Note :
this maximum length of 1400 bytes was determined by a comment I found in the
code ...! Now we loop through the active players (ignoring self), to find those
in range (from the top-down view). I've used metres as my units for the range,
so I scale to this Quake's coords by dividing by SCANNER_UNIT. (32 units is
about one metre in Quake's world). If in range, we rotate the normalised vector
to the enemy, by the players view Yaw angle (the left/right facing angle), to
give our on-screen dot position (we scale it by scanner pixel radius to fit)
Now we've got the on-screen (x,y), we can work out the graphic to display. I've
done three types of scanner "dot": for a "normal" player;
player with Quad Damage and player with Invulnerability. This information is
now added to the layout string. For an extra touch, we're going to add a small
arrow to show if the enemy is above or below (to the nearest metre). This
follows the same methods.
And that's about it ! :) The only thing left is for you to bind "cmd
scanner" to a suitable key and boot-up Quake 2 !! It's a long tutorial but
introduces some handy methods, and there are loads of possibilities for further
modifications :
1) Display different information - e.g. Proximity Mine locations (see Chris Hilton's tutorial) 2) Intelligent positioning on large-screen displays ? (it's currently centred and non-scaled) 3) Adjustable scanner ranges (maybe using some cells in the process ?) 4) Single Player adaptation. 5) Work out how to allow Escape to remove the scanner and bring up the options.Currently, you'll have to turn the scanner off first. If anyone knows a work-around,
please mail me. :)
Enjoy !
Tutorial by Yaya (-*-) .
|
This site, and all
content and graphics displayed on it, |