SoF Game Development

Keith Fuller
Programmer, Raven Software
3/17/00

Basics of SOF Deathmatch Coding


Note:
any time I use a word or phrase you`ll actually find in
the code, I`ll try to put it in Courier font, like this.

Also, something I should mention is that more than 90% of
the deathmatch code is written in C++ and makes
heavy use of the language`s inheritance and polymorphism features.
If you want to really understand why the SOF
multiplayer game code is arranged the way it is and how to
properly add to it or change it, you need a firm grasp of C++.

Intro
Just about everything you need to look at can be found in
the gamecpp files beginning
with “dm”. Each multiplayer gametype (Standard, Arsenal, Assassin,
CTF, Realistic, Control, Conquer) has
its own class and its behavior is coded in its own file. In fact,
and this may sound strange, but it actually
makes quite a bit of sense, singleplayer mode has its own
dm_none.cpp file.

The idea is that every multiplayer class is derived from the gamerules_c
class and every function that a multiplayer
class might need is present in gamerules_c.
If it`s a function that only dm_realist needs, its in gamerules_c.
f it`s a function that only dm_arsenal needs, its in gamerules_c.
When you define a function in gamerules_c,
you give it some default behavior. For instance,
FlagCaptured() is a function that only ever has meaning when
you`re playing CTF. It`s still defined in gamerules_c,
but its default behavior there is to do nothing. Only when
the function is overridden in the dm_ctf class does it do anything.

Why set it up this way? The following example may
help clarify the value of this arrangement.
Take a look at gamerules_c::checkItemSpawn().
When a map is loaded and the game code tries to spawn all of the items in a map,
checkItemSpawn is called before each item is
spawned to see if the current set of rules (in other words, the multiplayer gametype
we`re currently playing) allows that type of item.
dm_arsenal overrides it to prevent any pickups from being spawned. dm_none,
also known as singleplayer, overrides it to allow all pickups to be spawned.

Now it`s a little tricky due to the fact that the global
variable we use to access the current game type,
gamemode_ic* dm, isn`t actually an instance of gamerules_c.
It`s an instance of what`s called an interface class.
The definition of class gamemode_ic lets you know what functions
will be available for all deathmatch modes.
When you begin a new map, the deathmatch system is initialized
and our global gamemode_ic*
dm sets itself up with a new instance of the current game
type by calling gamemode_ic::setDMMode().


IMPORTANT:

After that occurs, any time you call a function with the
global variable dm you`re actually calling the same function
for the current game type. So when you`re in singleplayer
and you see dm->checkItemSpawn() somewhere in the code,
that line is actually calling dm_none::checkItemSpawn().
If you`re playing CTF, however, that same line of code winds
up calling dm_ctf::checkItemSpawn(). Sounds a little screwy,
but I hope it makes sense.

A Quick Sample Mod
What if you wanted to have armor pickups always
show up in realistic mode deathmatch,
regardless of deathmatch flag settings? Well, the function
we mentioned above, dm_real::checkItemSpawn(), is what determines
which pickups are allowed in dm_real. If you look at
this function`s code in gamecpp/dm_real.cpp you`ll notice these lines at the
top of the function:

if ((*pickup)->GetPickupListIndex() == OBJ_CTF_FLAG)
{
G_FreeEdict (ent);
return(0);
}

This chunk of code says, “if the object we just spawned is a CTF flag,
remove it from the game.” The next several lines do something fairly similar
and they will be the target of this mod.

if(dmRule_NO_ARMOR())
{
if ((*pickup)->GetType() == PU_ARMOR)
{
G_FreeEdict(ent);
return(0);
}
}

Here we’re checking one of dm_real’s member functions, dmRule_NO_ARMOR(), to find out
if our current deathmatch flags (dm flags) allow armor in the game. If they don’t,
and the object we just spawned is armor, we remove it from the game. Next,
we perform a similar check to remove any non-bullet weapons and their ammo if that’s
what the current dm flags tell us to do.

if(dmRule_BULLET_WPNS_ONLY())
{
if ( ((*pickup)->GetType() == PU_WEAPON) && (!(*pickup)->IsBulletWpn()) ||
((*pickup)->GetType() == PU_AMMO) && (!(*pickup)->IsBulletAmmo()) )
{
G_FreeEdict(ent);
return(0);
}
}

Later on in the function we replace any regular health pickups
that might be in the map with medkits,
and then finish up with some basic housekeeping stuff that
pertains to realistic mode’s handling of weapon re-spawning.

Anyway, our original goal for this mod was to make sure that
armor pickups always show up in the map.
So we go back to the code we saw earlier:

if(dmRule_NO_ARMOR())
{
if ((*pickup)->GetType() == PU_ARMOR)
{
G_FreeEdict(ent);
return(0);
}
}

As mentioned previously, the call to dmRule_NO_ARMOR()
is what decides whether or not an armor pickup gets removed.
If that rule says it’s not OK, then we proceed to check the
type of item we just spawned.
The item we just spawned is referred to by pickup and
we find out its type by calling pickup::GetType().
If the item does turn out to be armor,
then we call G_FreeEdict() on the pickup object itself
and therefore it gets removed from the game.
What we want to do is not check the dm flags and instead
just leave this checkItemSpawn function if the object
we just spawned is armor.
To do that, we replace the above lines of code with the following:

if ((*pickup)->GetType() == PU_ARMOR)
{
return 1;
}

That check for PU_ARMOR makes sure that the item
we`re dealing with is,
in fact, armor, and then we return 1 to let the calling
code know we want to keep this item.

Compile that and we’re done.

Something important to note here is that
you didn`t have to
change any of the code that calls checkItemSpawn and
you didn`t change any of the item spawning behavior
for any of the other multiplayer game types.

That`s the benefit of the current C++ setup we use in SOF for deathmatch code.