PAGE_CONTENT = other_viewer.php
Home > Other content > myfirstmod

Tutorial: My First Quake III Arena Mod

I started off with the removing of weapons and ammo boxes so we could get some quick visual result. Now, however, we need to implement a framework for our weapon progression system. Because we want the server admin to be able to configure the order in which weapons are served up, we need to implement a way for him or her to do this. The easiest and most robust way is to offer cvars for this. cvars are variables that can be read and set through the console and are stored in a player's q3config.cfg file. Examples of existing cvars are "fraglimit", "timelimit", "g_gametype", "cg_fov" or "s_volume". Since we are implementing cvars that determine the game rules, we'll be using the g_ prefix.

We'll need to define 9 cvars in total, because we want to give the server admin the possibility to set a list of 9 weapons to progress through. These cvars will be g_weaponOrder1 to g_weaponOrder9. These cvars are defined in two places. First we'll go to g_local.h in the game project. You'll find a list of extern vmCvar_t definitions at around line 700. At the end of this list, simply append new definitions of our new cvars:

extern	vmCvar_t	g_weaponOrder1;
extern	vmCvar_t	g_weaponOrder2;
extern	vmCvar_t	g_weaponOrder3;
extern	vmCvar_t	g_weaponOrder4;
extern	vmCvar_t	g_weaponOrder5;
extern	vmCvar_t	g_weaponOrder6;
extern	vmCvar_t	g_weaponOrder7;
extern	vmCvar_t	g_weaponOrder8;
extern	vmCvar_t	g_weaponOrder9;

When we open up g_main.c you'll find that these cvars are included here as well. In the same way, append our cvars to the end of the list (before or after the #ifdef MISSIONPACK, it doesn't really matter). Note that the extern keyword is absent here. Right below where we pasted our new cvars, you'll find that a cvarTable_t is being constructed. All cvars are initialized with their initial values here. Append a new line for each cvar we want to add like this:

{ &g_rankings, "g_rankings", "0", 0, 0, qfalse},
{ &g_weaponOrder1, "g_weaponOrder1", "5", 0, 0, qtrue },	//RL
{ &g_weaponOrder2, "g_weaponOrder2", "8", 0, 0, qtrue },	//PG
{ &g_weaponOrder3, "g_weaponOrder3", "3", 0, 0, qtrue },	//SG
{ &g_weaponOrder4, "g_weaponOrder4", "6", 0, 0, qtrue },	//LG
{ &g_weaponOrder5, "g_weaponOrder5", "7", 0, 0, qtrue },	//RG
{ &g_weaponOrder6, "g_weaponOrder6", "4", 0, 0, qtrue },	//GL
{ &g_weaponOrder7, "g_weaponOrder7", "2", 0, 0, qtrue },	//MG
{ &g_weaponOrder8, "g_weaponOrder8", "9", 0, 0, qtrue },	//BFG
{ &g_weaponOrder9, "g_weaponOrder9", "", 0, 0, qtrue },		//unused

This sets up our 9 new cvars and gives them their initial value (the third argument). Behind each line I've added comments so you can see what weapon each default value refers to. So in this set up, by default, players will progress from the Rocket Launcher to the Plasmagun, to the Shotgun, to the Lightninggun, to the Railgun, to the Grenade Launcher, to the Machinegun and finally to the BFG. The last cvar is unused but it's included so that the server admin can add the Gauntlet to the rotation as well.

Now we'll need to be able to store which of these weapons a player is currently holding. For this, open up the g_local.h file and locate the gclient_s struct. This struct defines all of the properties a client (a player) has. All the properties in this struct will be cleared with each spawn. Other structs are defined for data that persists throughout a game. Add a new property variable to this list like so:

int	switchTeamTime;		// time the player switched teams

// timeResidual is used to handle events that happen every second
// like health / armor countdowns and regeneration
int	timeResidual;

int	currentWeapon;

You'll notice we've added a currentWeapon integer to the list. It's probably a good idea to add a comment to tell us what exactly this variable represents, as the way we're going to implement it, it does not refer to the number of the actual weapon (like 1 = Gauntlet, 2 = MG, 3 = SG, etc) but to the index of the ordering. So with our default (RL -> PG -> SG -> etc...) ordering, a "currentWeapon" value of 2 means the player is holding the Plasmagun.

So you're probably already thinking "then we need a method to translate that". Yes indeed, we must have a way of determining which weapon the player is holding, based on his currentWeapon value. For that we're going to implement a new function named GetWeapon. I've placed this function in g_combat.c as that's where it's called from most. Below you can see what this method ended up like:

int GetWeapon( int currentOrderNumber ) {
  switch (currentOrderNumber) {
    case 1: if (!strcmp(g_weaponOrder1.string, "")) return -1; else return g_weaponOrder1.integer; break;
    case 2: if (!strcmp(g_weaponOrder2.string, "")) return -1; else return g_weaponOrder2.integer; break;
    case 3: if (!strcmp(g_weaponOrder3.string, "")) return -1; else return g_weaponOrder3.integer; break;
    case 4: if (!strcmp(g_weaponOrder4.string, "")) return -1; else return g_weaponOrder4.integer; break;
    case 5: if (!strcmp(g_weaponOrder5.string, "")) return -1; else return g_weaponOrder5.integer; break;
    case 6: if (!strcmp(g_weaponOrder6.string, "")) return -1; else return g_weaponOrder6.integer; break;
    case 7: if (!strcmp(g_weaponOrder7.string, "")) return -1; else return g_weaponOrder7.integer; break;
    case 8: if (!strcmp(g_weaponOrder8.string, "")) return -1; else return g_weaponOrder8.integer; break;
    case 9: if (!strcmp(g_weaponOrder9.string, "")) return -1; else return g_weaponOrder9.integer; break;
    default: if (!strcmp(g_weaponOrder1.string, "")) return -1; else return g_weaponOrder1.integer; break
  }
}

This function now takes an order number (like stored in currentWeapon) and returns the related weapon value. Because the server admin is free to configure the weapon order in any way possible we're checking for empty values. Maybe a server admin wants to use only 4 weapons in his weapon order, in that case 5 of the 9 cvars are set to "". When a player hits an order number that has not been set, it returns -1 and the caller knows that the client has hit an invalid value. Handling such an invalid value will be discussed in a moment.

You'll notice that it's possible to call our cvars like normal variables. They are actually structs with a number of properties. Check the vmCvar_t definition in q_shared.h to see the properties it has: handle, modificationCount, value, integer and string. Especially value, integer and string are important because they allow you to retrieve the value of the cvar in various data formats (respectively float, int and char/string). Because our method returns an int, we want to access the integer properties of our cvars.

We're going to write another helper function. One to progress a player on to the next weapon. When a player makes a kill, we want him to move on to the next weapon in the weapon order. This could be as simple as calling client->currentWeapon++, but we really have to take the possibility into account that the server admin left gaps in his weapon ordering or entered incorrect values. For instance, imagine the following settings:

g_weaponOrder1 "5"
g_weaponOrder2 "8"
g_weaponOrder3 ""
g_weaponOrder4 "3"
g_weaponOrder5 "6"
g_weaponOrder6 "0"
g_weaponOrder7 "9"
g_weaponOrder8 ""
g_weaponOrder9 ""

In this situation we're running into a problem once the player should progress from g_weaponOrder2 to g_weaponOrder3. The same is true for g_weaponOdrer6 because there is no weapon defined as weapon 0. Our GetWeapon() function is going to help us with this, but not entirely. We'll need to add some input checking here. What I've done is define a function ProgressWeapon() in g_combat.c. This function looks like this:

void ProgressWeapon ( gentity_t *ent, qboolean reset ) {
  int weapon;
  //remove currently held weapon/ammo
  ent->client->ps.ammo[GetWeapon(ent->client->currentWeapon)] = 0;
  ent->client->ps.stats[STAT_WEAPONS] = 0;

  //get next weapon for client
  if (reset == qfalse)
    ent->client->currentWeapon++;	
  else
    ent->client->currentWeapon = 1;

  weapon = GetWeapon(ent->client->currentWeapon);

  while (weapon < 1 || weapon > WP_NUM_WEAPONS || weapon == WP_GRAPPLING_HOOK){
    ent->client->currentWeapon++;
    weapon = GetWeapon(ent->client->currentWeapon);
  }

  //give weapon and activate it
  ent->client->ps.stats[STAT_WEAPONS] = (1 << weapon);
  ent->client->ps.ammo[weapon] = -1;
  ent->client->ps.weapon = weapon;
  ent->client->ps.weaponstate = WEAPON_READY;
}

What this does should be pretty obvious. It starts off with removing any currently held ammo and weapon. You'll see it uses the GetWeapon() function we defined to get the currently held weapon based on the client's currentWeapon value. Then we are going to increment the currentWeapon value to make the player progress to the next weapon. However, the function accepts a reset argument, which is a qboolean. Variables of type qboolean can be treated the same as standard boolean variables in other languages (bool does not exist as a datatype in ANSI C). qboolean variables are set to a value of either qtrue or qfalse. We will be using this argument to determine whether we want to reset the player to the first weapon or not. This comes in handy when the player is spawns after being fragged or when the player first enters the game. In the code you can see that when the reset argument is passed as qtrue, the client's currentWeapon is set back to 1.

So now that the currentWeapon value is incremented or reset, we get the new weapon number using the GetWeapon() function. After that, the code goes into a while loop that tests the new weapon value against a few constraints. These are:

If any of these checks fail, we got an invalid value and we progress the player on to the next defined weapon in the weapon order, get the new weapon value and check it again in the while loop. This continues until we hit a new valid value. Finally, the weapon is added to the player's weapon inventory, we give the player ammo, we set the active weapon to the weapon we just gave the player (this is the ent->client->ps.weapon = weapon; line and we set the weaponstate to WEAPON_READY. You may be wondering why we are giving -1 ammo to the player. There is a simple reason for that. When a player has -1 ammo for a weapon, Quake 3 assumes that the player has unlimited ammo for that weapon. This means we've basically already implemented unlimited ammo for our weapons.

NEXT: Spawning the player>>