Urban Software - UrbanCTF, UrbanDM, Earn A Weapon, CTG...
Player Scanner - covers editing the client HUD and passing selected information from the server to the client

 

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


    // Haggis
    /*
    ============================
    G_SendCommandToClient
    Send the given command to the specified (or all) clients
    ============================
    */

    void G_SendCommandToClient (gentity_t *to, char *cmd)
    {
        if (to == NULL)
            // send to all clients

            trap_SendServerCommand ( -1, cmd );
        else
            // send to specified client
            trap_SendServerCommand ( to-g_entities, cmd);
    }
    // Haggis

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

    // Haggis
    void G_SendCommandToClient(gentity *to, char *cmd);
    // Haggis

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; // dead bodies
        gentity_t *bodyQue[BODY_QUEUE_SIZE];
        // Haggis
        int lastPlayerLocationTime; // last time client positions were updated
        // Haggis

    } 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 :

    // Time between location updates
    #define TEAM_LOCATION_UPDATE_TIME 1000
    // Haggis
    #define PLAYER_LOCATION_UPDATE_TIME 250
    // Haggis

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, // Q3A single player mode
        AIGT_TEAM, // any team game
        AIGT_OTHER // anything else!
    } aiGametype_t;

    // Haggis
    #define kENTRY_EOL 0       //marks the end of the list
    #define kENTRY_INVALID 1   //marks a valid entry in the list
    #define kENTRY_VALID 2     //marks an invalid entry in the list

    typedef struct 
    {
        int valid;
        vec3_t pos;
    } playerpos_t;

    // Haggis

    #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 :

    // Haggis
    playerpos_t g_playerOrigins[MAX_CLIENT]; //global storage for player positions

    void CheckPlayerPostions(void)
    {
        int i, valid_count;
        gentity_t *loc, *ent;
        char cmd[16*MAX_CLIENT + MAX_CLIENT]; // make sure our command string is
                                              //
large enough for all the data

        // do we need to update the positions yet?
        if (level.time - level.lastPlayerLocationTime > PLAYER_LOCATION_UPDATE_TIME) 
        {
            //store the current time so we know when to update next
            level.lastPlayerLocationTime = level.time;

            //for each possible client
            valid_count = 0;

            for (i = 0; i < g_maxclients.integer; i++) 
            {
                //get a pointer to the entity
                ent = g_entities + i;

                //see if we have a valid entry
                if (!ent->inuse)
                {
                    //mark as an invalid entry
                    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
                {
                    //get and store the client position
                    VectorCopy( ent->client->ps.origin, g_playerOrigins[i].pos);

                    //mark as valid entry
                    g_playerOrigins[i].valid = kENTRY_VALID;

                    //increase the valid counter
                    valid_count++;
                }
             }
          }

        //build the command string to send
        Com_sprintf(cmd, sizeof(cmd), "playerpos %i ", valid_count);
        for(i=0; i<g_maxclients.integer; i++)
        {
            //if weve got a valid entry then add the position to the command string
            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]));
            }
        }
    
        //finally broadcast the command
        G_SendClientCommand(NULL, cmd);
    }
    // Haggis

 

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.

    // Haggis
    void G_SendCommandToClient(gentity *to, char *cmd);
    void CheckPlayerPostions(void);
    // Haggis

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

    // Haggis
    CheckPlayerPostions();
    // Haggis

    // see if it is time to do a tournement restart
    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;

    // Haggis
    extern playerpos_t cg_playerOrigins[MAX_CLIENTS];
    // Haggis


    //
    // cg_main.c
    //

    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"

    // Haggis
    playerpos_t cg_playerOrigins[MAX_CLIENTS];
    //Haggis

    /*
    =================
    CG_ParseScores
    ...

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;
    // Haggis
    int count;
    int i;
    const char *ptr;

    // Haggis

At the end of CG_ServerCommand() and add the following lines of code :

    // clientLevelShot is sent before taking a special screenshot for
    // the menu system during development

    if ( !strcmp( cmd, "clientLevelShot" ) ) 
    {
        cg.levelShot = qtrue;
        return;
    }

    // Haggis
    if ( !strcmp( cmd, "playerpos" ) ) 
    {
        // -- expand the comma delimited string into the player positions --

        //clear out old list of positions
        memset(cg_playerOrigins, kENTRY_EOL, sizeof(cg_playerOrigins));

        //get the number of entries in the list
        count = atof(CG_Argv(1));
        for(i=0;i<count;i++)
        {
            //set the string pointer to the correct set of parameters
            ptr = CG_Argv(i+2);

            //read in the first number
            cg_playerOrigins[i].pos[0] = atof(ptr);

            //move the ptr on until we come to a comma
            ptr = strchr(ptr, ',');

            //skip over the comma
            ptr++;

            //read in the next number
            cg_playerOrigins[i].pos[1] = atof(ptr);

            //move the ptr on until we come to a comma
            ptr = srtchr(ptr, ',');

            //skip over the comma
            ptr++;

            //read in the final number
            cg_playerOrigins[i].pos[2] = atof(ptr);

            //mark the entry as valid
            cg_playerOrigins[i].valid = kENTRY_VALID;
        }
        return;
    }

    // Haggis

    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;

        // Haggis
        int scanner;
        // Haggis

    } 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 },
    // Haggis
    { "+scanner", CG_ScannerOn_f },
    { "-scanner", CG_ScannerOff_f },
    // Haggis
};

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


    // Haggis
    /*
    =====================
    CG_ScannerOn_f - turns on the scanner
    =====================
    */

    void CG_ScannerOn_f( void )
    {
        cg.scanner = 1;
    }

    /*
    =====================
    CG_ScannerOff_f - turns off the scanner
    =====================
    */

    void CG_ScannerOff_f( void )
    {
        cg.scanner = 0;
    }
    // Haggis

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.

    // Haggis
    void CG_ScannerOn_f( void );
    void CG_ScannerOff_f( void );
    void CG_DrawScanner( void );
    // Haggis

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;

        // Haggis
        qhandle_t scannerShader;
        qhandle_t scannerBlipShader;
        qhandle_t scannerBlipUpShader;
        qhandle_t scannerBlipDownShader;
        // Haggis

    } 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" );

    // Haggis
    cgs.media.scannerShader = trap_R_RegisterShader("Scanner");
    cgs.media.scannerBlipShader = trap_R_RegisterShader("ScannerBlip");
    cgs.media.scannerBlipUpShader = trap_R_RegisterShader("ScannerBlipUp");
    cgs.media.scannerBlipDownShader = trap_R_RegisterShader("ScannerBlipDown");

    // Haggis


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

    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;

        //dont draw anything if the scanner is off
        if(cg.scanner == 0)
            return;

        x = 30; //offset from left of screen
        y = 30; //offset from top of scren
        w = 160; //width of scanner on screen
        h = w; //height of scanner on screen

        //draw the scanner
        CG_DrawPic( x, y, w, h, cgs.media.scannerShader);

        //get info about the scanner positon
        scanner = &cg_entities[cg.snap->ps.clientNum];

        //check the stored player positions
        player = cg_playerOrigins;
        while(player->valid != kENTRY_EOL)
        {
            //get a vector from the scanner position to the current player
            VectorSubtract (scanner->lerpOrigin, player->pos, v);

            //store the height component
            height = v[2];

            //remove the height component from the vector
            v[2] = 0;

            //calc the distance to the player and scale it to the scanner scale
            len = VectorLength( v ) / kSCANNER_UNIT;

            //is the player within range?
            if(len < kSCANNER_RANGE)
            {
                //create a vector pointing stright down
                norm[0] = 0;
                norm[1] = 0;
                norm[2] = -1;

                //normalise the vector to the player
                VectorNormalize( v );

                //rotate the player about the scanners view angle
                RotatePointAroundVector( dp, norm, v, scanner->lerpAngles[1]);

                //scale to fit current size of scanner
                VectorScale(dp,len*(w/2)/kSCANNER_RANGE,dp);

                // calc screen (x,y) (-4 = half dot width, so we can centre the graphic)
                sx = (x + (w/2) + dp[1]) - 4;
                sy = (y + (h/2) + dp[0]) - 4;

                //draw the dot
                if (height < -32) //player is above scanner
                    CG_DrawPic( sx, sy, 8, 8, cgs.media.scannerBlipUpShader);
                else if (height > 32) //player is below scanner
                    CG_DrawPic( sx, sy, 8, 8, cgs.media.scannerBlipDownShader);
                else
                    CG_DrawPic( sx, sy, 8, 8, cgs.media.scannerBlipShader);
            }
            
            //move on to next entry in the array
            
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();
        // Haggis
        CG_DrawScanner();
        // Haggis

    }

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


Tutorial by Haggis