Date
: 04/01/99
Author(s) : Haggis
Skill(s) : Advanced
Source Project(s) : Game, CGame
Revision : 1.1
Extra Files : scanner.pk3
|
|
|
|
|
|
|
Well here goes
with my first tutorial. With this you should learn how to add
new files to the compiling process, add new items to the client
HUD and how to get the server to pass specific information to
the client. This tutorial will be quite long as it covers many
aspects of Q3 coding. The code for the scanner itself is based
on an original Q2 tutorial by YaYa
(-*-). If you do use this code or part thereof then can you
please credit those who have put their spare time and knowledge
into these tutorials, namely YaYa
(-*-), Spk and myself
(Haggis).
As this is such a large tutorial, I suggest you read through the
it at least once to get an idea of what is going on. There are
some notes at the end of the tutorial which may help if you get
stuck with something. Now, lets start by adding an extra file.
Adding
files to the build process
We will be adding
a new source file, cg_scanner.c, to the cgame module. First create
the new source file in the cgame folder and then open cgame.bat
in your cgame folder. Towards the end of the file you will see
something like this:
%cc% ../cg_snapshot.c
@if errorlevel 1 goto quit
%cc% ../cg_view.c
@if errorlevel 1 goto quit
%cc% ../cg_weapons.c
@if errorlevel 1 goto quit
q3asm -f ../cgame
:quit
cd ..
Now add the the
following lines so the file looks like this
%cc% ../cg_snapshot.c
@if errorlevel 1 goto quit
%cc% ../cg_view.c
@if errorlevel 1 goto quit
%cc% ../cg_weapons.c
@if errorlevel 1 goto quit
REM **** Haggis : add
cg_scanner.c to the compiling ****
%cc% ../cg_scanner.c
@if errorlevel 1 goto quit
REM ******************************************************
q3asm -f ../cgame
:quit
cd ..
Now we need to add some information
so that q3asm knows how to build cg_scanner into the final qvm
file. Open up cgame.q3asm and scroll towards the bottom of the
file. It should look something like this:
bg_misc
q_math
q_shared
Now add cg_scanner
to the end of the file so it looks like this
bg_misc
q_math
q_shared
cg_scanner
Sending
specific information to the client
Many thanks to
Spk for help with this section.
To keep the bandwidth low Q3 will only send "need to know" information
to the client. That is, the client should never need to know about
the position of an opponent player if they can't see them (you
cant shoot what you cant see, unless you've got the trust wall
piercing rail gun handy). This is no good for a scanner as the
blips on the screen will appear to freeze or dissapear when the
opponent is outside the clients PVS. To keep this information
up to date we can do one of two things:
- Set the server to send all the player information every
frame whether they are visible or not.
- Send only the player positions to the clients less frequently
(say every 250 ms)
The first one
is easily achieved by adding ent->r.svFlags
|= SVF_BROADCAST; to the void
ClientBegin( int clientNum ) function in g_client.c. Place
it just before the call to ClientSpawn(
ent ). However, this is not the way we want to go. It is
the easiest way to achieve the results but not the most efficient.
For this reason we will look at coding the second method.
First we need
to create a function to send information to the clients. The function
trap_SendServerCommand()
will send a string of characters to the client which we can later
decode in cg_servercmds.c. Open up g_cmds.c and add the following
code to the end of the file
void G_SendCommandToClient (gentity_t
*to, char *cmd)
{
if (to == NULL)
trap_SendServerCommand
( -1, cmd );
else
trap_SendServerCommand
( to-g_entities, cmd);
}
Now we need
to make this function available to all the files in the game module
so add the to the end of g_local.h
void G_SendCommandToClient(gentity *to,
char *cmd);
This is just
a wrapper function to the trap_SendServerCommand().
By doing this will make our code easier to read. It will send
the command string cmd to
the specified entity or to all entities if the parameter to
is set to NULL.
Still in g_local.h find the level_locals_t
structure at about line 290 and add this at the end of the structure
...
int bodyQueIndex;
gentity_t *bodyQue[BODY_QUEUE_SIZE];
int lastPlayerLocationTime;
// last time client positions were updated
} level_locals_t;
This variable
will keep track of when we last sent to the clients any player
position information. The time between updates will be set in
a define in bg_public.h. Goto line 429 and add the following
:
#define TEAM_LOCATION_UPDATE_TIME 1000
#define PLAYER_LOCATION_UPDATE_TIME
250
As you can see
the time between updates will be 250ms. Open up q_shared.h
and add these to the end of the file, just before the #endif
typedef enum {
AIGT_SINGLE_PLAYER,
AIGT_TEAM,
AIGT_OTHER
} aiGametype_t;
#define kENTRY_EOL
0
#define kENTRY_INVALID 1
#define kENTRY_VALID 2
typedef struct
{
int valid;
vec3_t pos;
} playerpos_t;
#endif // __Q_SHARED_H
Now we can create
the function which will be called to send this information. Open
up g_main.c and add this to the end of the file :
playerpos_t g_playerOrigins[MAX_CLIENT];
void CheckPlayerPostions(void)
{
int i, valid_count;
gentity_t *loc,
*ent;
char cmd[16*MAX_CLIENT
+ MAX_CLIENT];
if (level.time
- level.lastPlayerLocationTime > PLAYER_LOCATION_UPDATE_TIME)
{
level.lastPlayerLocationTime
= level.time;
valid_count
= 0;
for
(i = 0; i < g_maxclients.integer; i++)
{
ent
= g_entities + i;
if
(!ent->inuse)
{
g_playerOrigins[i].valid
= kENTRY_INVALID;
}
if(!ent->client)
{
g_playerOrigins[i].valid
= kENTRY_INVALID;
}
else
if(ent->health <= 0)
{
g_playerOrigins[i].valid = kENTRY_INVALID;
}
else
{
VectorCopy(
ent->client->ps.origin, g_playerOrigins[i].pos);
g_playerOrigins[i].valid
= kENTRY_VALID;
valid_count++;
}
}
}
Com_sprintf(cmd,
sizeof(cmd), "playerpos %i ", valid_count);
for(i=0; i<g_maxclients.integer;
i++)
{
if(g_playerOrigins[i].valid
== kENTRY_VALID)
{
strcat(cmd,
va(" %f,", g_playerOrigins[i].pos[0]));
strcat(cmd,
va("%f,", g_playerOrigins[i].pos[1]));
strcat(cmd,
va("%f", g_playerOrigins[i].pos[2]));
}
}
G_SendClientCommand(NULL,
cmd);
}
Now we need
to make this function available to all the other functions that
need it. Add this to the end of g_local.h so it now looks
like as follows.
void G_SendCommandToClient(gentity *to,
char *cmd);
void CheckPlayerPostions(void);
Now we have
to find somewhere to call the funtion. Open g_main.c again
and in the funtion above the one you've just created (void
G_RunFrame(int leveltime)) add the line as shown
end = trap_Milliseconds();
CheckPlayerPostions();
CheckTournement();
That's a lot
of code we've just added so lets just take a minute to go through
it. The last thing we did was add the call to CheckPlayerPositions()
to the G_RunFrame()
function. This will ensure that our function will get called once
every frame.
Now lets look at the new code. The defines are used for convenience
and readability and they allow us to mark the entries in the list
as either being valid or invalid. The cmd
parameter at the start of the CheckPlayerPositions()
function is allocated enough room for each client to send up to
3 float numbers as strings. Now we test whether its time to fill
out the g_playerOrigins
structure or to return to the calling function. If it's time to
fill out the list of player positions then the loop will take
each of the clients available and see if it is in use. If it isn't
then an invalid entry marker is placed in the g_playerOrigins
structure and nothing more is done for that entry. If it is a
valid entry (i.e. its in use by a client and has some health left)
then it is marked as a valid entry in the array and the position
is copied over. A total count of the valid entries is also incremented
here.
When the list
has been populated then we can start building the command string
to send to the clients. This is started by creating a string which
contains our commans keyword playerpos
and the number of valid players found. We then go through the
g_playerOrigins
structure to add the valid position entries to the string. Finaly
we use the function we created earlier, G_SendCommandToClient(),
to send the command to all the clients.
Reciving
specific information at the client
Having sent all
this information to the client we now need to process the data
when its recived. The messages are processed in the void
CG_ServerCommand( void ) function in cg_servercmds.c.
We also need somewhere to store this information so open up cg_local.h
and add the following just after the vmCvar_t
:
extern vmCvar_t cg_predictItems;
extern vmCvar_t cg_deferPlayers;
extern playerpos_t cg_playerOrigins[MAX_CLIENTS];
const char *CG_ConfigString( int index
);
This will allow any function within the cgame qvm to have access
to the player positions. Now add the following to the top of cg_servercmd.c
#include "cg_local.h"
playerpos_t cg_playerOrigins[MAX_CLIENTS];
This is where
the actual information about the palyer positions will be stored.
Still in cg_servercmds.c find the void
CG_ServerCommand( void ) function and add these variable
declarations to the start
const char *cmd;
int count;
int i;
const char *ptr;
At the end of
CG_ServerCommand()
and add the following lines of code :
if ( !strcmp( cmd, "clientLevelShot" )
)
{
cg.levelShot =
qtrue;
return;
}
if ( !strcmp( cmd,
"playerpos" ) )
{
memset(cg_playerOrigins,
kENTRY_EOL, sizeof(cg_playerOrigins));
count = atof(CG_Argv(1));
for(i=0;i<count;i++)
{
ptr
= CG_Argv(i+2);
cg_playerOrigins[i].pos[0]
= atof(ptr);
ptr
= strchr(ptr, ',');
ptr++;
cg_playerOrigins[i].pos[1]
= atof(ptr);
ptr
= srtchr(ptr, ',');
ptr++;
cg_playerOrigins[i].pos[2]
= atof(ptr);
cg_playerOrigins[i].valid
= kENTRY_VALID;
}
return;
}
CG_Printf( "Unknown client game command:
%s\n", cmd );
What this does
is wait until the playerpos
command is sent to the client and then starts processing the rest
of the command string. We clear the array of client positions
here with the kENTRY_EOL
value. This will enusre that all the old data is removed and that
if we were to process this array before any data has been entered
then we shouldn't come accross any problems. To parse the comamnd
we first get the number of valid entries in the command string.
Then for each entry we extract the positional information into
the cg_playerOrigins
structure. Finaly we add an entry which specifies the end of the
list.
Right, that concludes the passing of information from the server
to the client now onto using this information for something useful...
Implementing
the scanner
The scanner will be updated and displayed totaly from
the client side of the code (i.e. all from within cgame). We will
create it so that the player can see the scanner only when they
are holding down a predefined key (by using +/- commands). To
start with please ensure you have downloaded the shaders (scanner.pk3)
and placed the file in your mod folder.
First of all, set up a client variable which we can inspect to
determine whether or not to display the scanner. Open up cg_local.h
and add the following to the end of the cg_t
structure (around line 514)
char
testModelName[MAX_QPATH];
qboolean testGun;
int
scanner;
} cg_t;
Now we will add
the commands which will togle the state of the scanner. Open cg_consolecmds.c
and at about line 145 edit the commands array to look like the
one below :
{ "tell_attacker",
CG_TellAttacker_f },
{ "tcmd", CG_TargetCommand_f },
{ "loaddefered", CG_LoadDeferredPlayers
},
{ "+scanner", CG_ScannerOn_f },
{ "-scanner", CG_ScannerOff_f },
};
This will add
two commands to the client command list. If we bind a key to "+scanner"
then CG_ScannerOn_f will
get called on the key press and CG_ScannerOff_f
will get called on the key release. We will need to create the
two new functions so open up cg_weapons.c and add the following
to the end of the file
void CG_ScannerOn_f( void )
{
cg.scanner = 1;
}
void CG_ScannerOff_f( void )
{
cg.scanner = 0;
}
All these functions
do is set or clear the scanner variable in the cg
structure whenever the user presses or releases the scanner key.
Don't forget to add the following to the end of cg_local.h
so that cg_consolecmds.c can find the new functions. While
were here we can also prototype the scanner drawing routine.
void CG_ScannerOn_f( void );
void CG_ScannerOff_f( void );
void CG_DrawScanner( void );
Almost there
now. As the scanner uses some graphics, and we don't want to load
them when we want to use them during the game. We have to pre-cache
them when all the other things for the level are being pre-cached.
This is a lot easier to do than it sounds. Just open cg_local.h
and add this to the end of the cgMedia_t
structure at about line 705.
...
sfxHandle_t countPrepareSound;
qhandle_t scannerShader;
qhandle_t scannerBlipShader;
qhandle_t scannerBlipUpShader;
qhandle_t scannerBlipDownShader;
} cgMedia_t;
And now we need
to add the code that actualy caches these shaders. Open up cg_main.c
and in the CG_RegisterGraphics
function at about line 677 add the following :
cgs.media.bloodMarkShader
= trap_R_RegisterShader( "bloodMark" );
// register the inline models
cgs.numInlineModels = trap_CM_NumInlineModels();
That's all the
preperation done. At last we get to doing some code in cg_scanner.c.
Add the following code :
#include "cg_local.h"
#define kSCANNER_UNIT 24
#define kSCANNER_RANGE 100
void CG_DrawScanner( void )
{
int x,y;
int w,h;
int sx,sy;
vec3_t v, norm,
dp;
float len;
float height;
playerpos_t *player;
centity_t *scanner;
if(cg.scanner
== 0)
return;
x = 30;
y = 30;
w = 160;
h = w;
CG_DrawPic( x,
y, w, h, cgs.media.scannerShader);
scanner = &cg_entities[cg.snap->ps.clientNum];
player = cg_playerOrigins;
while(player->valid
!= kENTRY_EOL)
{
VectorSubtract
(scanner->lerpOrigin, player->pos, v);
height
= v[2];
v[2]
= 0;
len
= VectorLength( v ) / kSCANNER_UNIT;
if(len
< kSCANNER_RANGE)
{
norm[0]
= 0;
norm[1]
= 0;
norm[2]
= -1;
VectorNormalize(
v );
RotatePointAroundVector(
dp, norm, v, scanner->lerpAngles[1]);
VectorScale(dp,len*(w/2)/kSCANNER_RANGE,dp);
sx
= (x + (w/2) + dp[1]) - 4;
sy
= (y + (h/2) + dp[0]) - 4;
if (height
< -32)
CG_DrawPic(
sx, sy, 8, 8, cgs.media.scannerBlipUpShader);
else if
(height > 32)
CG_DrawPic(
sx, sy, 8, 8, cgs.media.scannerBlipDownShader);
else
CG_DrawPic(
sx, sy, 8, 8, cgs.media.scannerBlipShader);
}
player++;
}
}
Not forgetting
to add a call to display the scanner in cg_draw.c, function
CG_Draw2D()
around line 1975
CG_DrawHoldableItem();
CG_DrawReward();
}
That's another
large chunk of code added so lets just go over it quickly. First
there is a set of defines we use to set up the scanner. We will
use the base unit of 24 quake units for each unit we display in
the scanner. The scanner has a range of 100 units which will equate
to 2400 quake units as the total range of the scanner. Adjust
this as you see fit. Once in the CG_DrawScanner
code, the first thing to do is display the main graphic on screen.
The variables x,y,w and h
determine the size and position of the scanner which can be changed
to whatever you like. The player position structure is then processed
to find those players that are in range. If a player is within
range we calculate the position to draw them at. Finally a graphic
is chosen which will display whether the player is on the same
level, above or below the scanner position.
Phew! Thats
it. Now you can do the final compile to create your new game.qvm
and cgqme.qvm files. Place them in your mod folder along
with the scanner.pk3 file
(you did download the pk3 didnt you?) and run Quake 3. Bind a
key to +scanner and start a multiplayer
server. If all has gone well then you should see the scanner in
the top left of the screen with all the clients currently in the
game as pulsing blips.
Room
for improvement
There's plenty you can do to improve the scanner code.
Here's some suggestions:
1) Remove the clients own position from the display. (Hint: nothing
needs to be done on the client side for this, just send the right
information)
2) Get the server to send player position information only if
a client has the scanner displayed.
3) Colour code the scanner blips for different situations (team
colours, etc...)
Notes
As I said to start with, this is a large tutorial
and you should really read through it to begin with and not just
copy and paste all the code blindly. This will help you understand
what is going on and will also help you identify any bugs which
may have cropped up through writing the tutorial or implementing
the code. I suggest you implement the scanner in the sections
described (adding new files, sending specific information, reciving
information then scanner implementation) and attempt to compile
at the end of each section. If you can't seem to get things to
work then start by checking and rechecking the code and then use
a liberal sprinkling of CG_printf()'s
throughout your code to see if any values that you were expecting
don't get processed correctly. For example when implementing the
"+scanner" command you might want to add a couple of CG_printf()'s
to display different message when the "+scanner" and the "-scanner"
commands are interpreted.
If you do have
a problem, a comment or a question then mail me (Haggis)
but please ensure that the subject line contains the phrase "Scanner
Tutorial". If you do use this code in your mods then please give
credit where its due (that means me,
Spk and YaYa
(-*-)).