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_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	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_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);

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;

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