
GreyBear's Quake 2 Tutorials
Chapter
2: Medic! - Part I
Requirements
Contents
Introduction
Welcome back! In this, the
second tutorial of the series, we're going to tackle a larger modification that
covers several aspects of Quake 2 DLL programming. We're going to implement a
player class in Quake 2. Our example class will be a Medic. Our medic will have
the ability to heal other players by touching them. We'll give our Medic a
higher Maximum Health point level. When he touches another player whose health
is below his maximum, our Medic will heal that player back to full health, or
until the Medic's health points drop to 100, whichever occurs first. We'll call
the Player's class his M.O.S.. For those of you who have a military
background, you know that this stands for Military Occupational Specialty. Your
MOS is your job in military service. Since our Quake player is definitely a
military type, the description fits rather nicely.
Since this tutorial covers
a lot of ground, I've broken it into two parts. This first part will cover
allowing the user to choose his MOS, and creating the variable to hold and
remember it. We'll also cover how to write to the console, and show you a twist
on getting commands from the user. As a bonus, we'll learn how to play sounds
in Quake.
GreyBear's
Quake 2 programming philosophy
My philosophy about making
code changes is this:
Make your changes in
your own code files whenever possible. That means for our mods, we'll be
adding NEW files to the Quake 2 DLL, not merely inserting our mods into the
original files. Why? Well, two reasons. One, it's much easier to find your mods
if you keep them in your own files, named by you with descriptive names that
you can remember. Two, it maintains the integrity of the original code. As
programmers we should respect the work of others. Rather than just hack it up,
we should add to it gracefully, and clearly mark our additions as our own.
When we can't follow
the above, as in modifying global include files, or modifying global structs,
we will segregate our mods in the code, and clearly mark them as our additions.
I also highly recommend you use a consistent marker, like your initials. That
way, you can search the code for your initials and easily find every mod you
make in id code. Again, it makes for easy maintenance.
In the body of the tutorial text,
code modifications made by us will have a + sign at the end of the line in a comment field to clue you
in that the line was added to the original surrounding code.
Modifying
the client_t struct for player class MOS
In the first tutorial we
modified the item_t struct to create a new flag to determine whether the weapon
we were using was sabotaged. We're going to do something almost exactly the
same with the client->resp struct. The client->resp struct is a part of
the edict_t struct. Specifically, it's the part of the edict_t struct that gets
saved across deaths in deathmatch. The edict_t struct is what describes players
and monsters in Quake 2. You can tell at a glance whether an edict entity is a
real player or not. The way you tell is by determining whether the client
struct is filled with valid info. If it is, the entity is a player. If it's
null, the entity is a monster. So, placing our flag in the client struct
insures that it'll only apply to real players manipulated by humans.
We're going to use another
int entry, just like last time. However this time, we really will use the power
that defining our flag as an int will give us. One of the objects of defining
player classes is to have more than one, right? So, with and int, we have the
capability of defining thousands of classes if we need to.
To make the modification
to the client_t struct, open up g_local.h and fine the definition for
the client_persistant_t struct. Add the code that's marked as our
modification:
vec3_t cmd_angles; // angles sent over in the last command //+ BD - 1/3 - Our players MOS (Military Occupational Specialty) //+ BD - 1/3 _ Definitions: //+ BD - 1/3 - 1. Grunt - A regular player //+ BD - 1/3 - 2. Medic - Can heal others int mos; //+ BD - 1/15 The flag that determines our player's MOS } client_respawn_t;
Save
the file. As you can see, the new definition is inserted at the end of the
client_respawn_t struct. This struct ends up being accessed as
ent->client->resp. Ent is an edict_t, client is a client_t and resp is a
client_respawn_t struct.
The explanation: Basically the same as tutorial
1's flag.
Displaying
instructions for player class choices
We're not going to get
very far if we can't tell the player how to choose his MOS when he enters the
game are we? So, we need to learn how to write messages to the console. We'll
also learn that this poses some interesting problems that we'll need to solve
in order to make our instructions readable.
You have to give Id
credit. They did a masterful job of writing a game library that is logical, and
easy to use. There are a series of pre-built functions for the DLL programmer
that control the game interface. Surprisingly, they are all prefixed by gi
(game interface). All of the game interface commands that deal with printing
text on the console or game screen are based upon the ubiquitous printf
function that all C programmers come to know (and some to love...). This makes
for quick familiarity and increased ease of use.
So, what do we need to do
that requires writing to the console? Well, here's a chronological list:
Sounds
easy enough. Before we take a stab at it though, let's briefly examine the
tools at our disposal. Note: For a complete description of all of the gi
functions in Quake 2, hop on over to
We can
throw out gi.dprintf and gi.bprintf right away. We don't need debugging info,
and we only want to send output to a single player. That leaves gi.cprintf and
gi.centerprintf. I chose gi.centerprintf because it displays preformatted, and
looks nicer for our purposes.
OK, now that we know what
function we want to use, let's look at the prototype. We'll need to know how to
call the function properly. Here it is:
gi.printf(edict_t *ent, char *fmt...);
Pretty
easy, eh? Basically we need to know who we want to send the message to (edict_t
*ent), and the message, which can be formatted in any way allowed by the normal
printf command.
OK, lets' try it. First,
we want to have our banner display every time a player enters deathmatch, but
only if he hasn't already chosen an MOS. That means if the player gets killed
in a match, or a level change occurs, for a player with a chosen MOS, the
message should NOT display. The place to add our message, then, looks to be
ClientBeginDeathMatch(), located in the p_client.c file. To begin with,
let's place a simple message, to test the viability of our thinking. Add the
following code:
gi.multicast (ent->s.origin, MULTICAST_PVS); gi.bprintf (PRINT_HIGH, "%s entered the game\n", ent->client->pers.netname); //+ BD - 1/5 - Display a message to the player when entering deathmatch gi.centerprintf(ent,"Please choose and MOS\n\nG - Grunt\nM - Medic\n"); //+BD - 1/5 - Display a menu for the player to choose from // make sure all view stuff is valid ClientEndServerFrame (ent);
Save
the file and compile the DLL. Now, after copying the DLL into your
quake2\baseq2 directory, run the game in deathmatch mode. See the banner? Yep,
nicely placed right on the screen as you pop into the spawn point. But, wait...
It disappears! That's the problem I alluded to earlier. The game expires these
screen messages after a few seconds, and erases them. Now, this really makes
perfect sense, since you wouldn't want this stuff scrolling down screen in
front of your world view. But how are we going to make sure that the message stays
up until the player chooses an MOS? Hmm.. We place it in the client's think
function. This think function plans the next move for the player, and
sets up certain environmental events for the next frame or two. That's the
natural place to keep the message in front of our player until he decides. OK,
now that we know what to do, let's go back and change the code in
ClientBeginDeathMatch() so it's really useful, and then we'll more or less
duplicate that functionality in ClientThink(), which is also located in p_client.c.
First, change the code you just added above to:
gi.multicast (ent->s.origin, MULTICAST_PVS); gi.bprintf (PRINT_HIGH, "%s entered the game\n", ent->client->pers.netname); CheckMOS(ent); //+ - BD - 1/5 - Check to see if the user already has an MOS. If not message him // make sure all view stuff is valid ClientEndServerFrame (ent);
To
remain true to my philosophy of keeping as much custom code separate as
possible, we've moved the code to a separate file, which we'll delve into soon.
For now, just be assured that the CheckMOS() function will do the work of
checking the mos flag and displaying the message if needed.
Next, we need to add the
exact same new code line into the Client_Think() function. This function is
located in the same p_client.c file, a little below the
ClientBeginDeathMatch() code. Add the line so it sits this way:
level.exitintermission = true; return; } CheckMOS(ent); //+ - BD - 1/5 - Check to see if the user already has an MOS. If not message him pm_passent = ent; // set up for pmove memset (&pm, 0, sizeof(pm));
Save
the file.
Next, we need to write the
code to actually display the message we want to show the player, and set the mos
flag when he decides. Create a new file, which we'll call b_playermos.c.
In it, place the following code:
//+ - BD - 1/7 THIS ENTIRE CODE BLOCK IS NEW! //BD - 1/3 - b_playermos.c - Brad Davis(GreyBear) //This code file consists of functions that relate to classes or 'MOS' (Military Occupational Specialty) //of players in deathmatch mode //Include this for variable declarations we need #include "g_local.h" // CheckMOS - 1/5 - BD This checks to see if an MOS has been chosen. If not, a message is printed. // This function is called in the Client_Think function to keep displaying it every frame. void CheckMOS(edict_t *ent) { if(ent->client->resp.mos < 1) //Do we already have an MOS? { //Noper...Message time gi.centerprintf(ent, "Please choose your MOS\n\nG - Grunt\nM - Medic\n"); } } //+ - BD - END NEW CODE BLOCK
Save
the file as b_playermos.c. Add it to your makefile or build list as
usual.
Next, as before, we need
to create a new header to allow other code files to know about our new
function. We'll call this file (surprise, surprise...) b_playermos.h. In
it, place the following code:
//+ - BD - 1/7 THIS ENTIRE CODE BLOCK IS NEW! //BD - 1/3 - b_playermos.h - Brad Davis(GreyBear) //This file forward declares our function prototypes for inclusion into Q2 source //Functions we use void CheckMOS(edict_t *ent); //+ - BD - END NEW CODE BLOCK
Save
the file as b_playermos.h. As before, since the header file is
referenced in the b_playermos.c file, it should be included in the build
automatically by the compiler. However, check your compiler docs. YMMV.
Now, we need to return to
the p_client.c file and insert the declaration of our function by
including the b_playermos.h file. Like so:
#include "m_player.h" #include "b_playermos.h" //+ - BD - 1/7 Include function declaration for our functions so they're visible
The
explanation: Whew!
That's a lot! OK, after learning a bit about some of the more common game
interface functions, we placed function calls at the strategic places in the
quake2 code to allow us to display a menu to the player when he enters
deathmatch. By placing it in the ClientBeginDeathMatch() function, we ensure
that it gets displayed as soon as the player enters the game. By placing the
same function call in Client_Think(), we make sure it stays there until
the player makes a choice. Finally, by using the same function in both places,
and taking advantage of the built in formatting of gi.centerprintf, we
get a nicely formatted message that doesn't flicker on the screen.
Creating
new commands for choosing player MOS
Now we'll return for a
moment to familiar territory. We're going to create new console commands to set
the player's MOS, just like we created a command to sabotage a weapon. As
before, the modification gets made within g_cmds.c. Look for the
Client_Command() function, and add the code:
else if (Q_stricmp (cmd, "wave") == 0) Cmd_Wave_f (ent); //+ BD - 1/5 - Added to handle our player class else if (Q_stricmp (cmd, "grunt") == 0) //+ BD - 1/5 - Asking to be a regular player Cmd_MOS_f (ent, cmd); //+ BD - 1/5 - OK, call our new function with the right arguments else if (Q_stricmp (cmd, "medic") == 0) //+ BD - 1/5 - Asking to be a medic Cmd_MOS_f(ent, cmd); //+ BD - 1/5 - Ditto! else if (Q_stricmp (cmd, "gameversion") == 0)
Save
the file. Note the new function Cmd_MOS_f(). We'll write that soon, so don't
worry about it for the moment.
The explanation: Again, this is the same thing we
did in tutorial 1. We're inserting code to look for a cmd string that contains
the arguments we added above, specifically "grunt" and
"medic". Obviously, as you add player classes, you merely add new if
else tests to find the commands you want to represent additional player
classes.
We need to do one
additional thing to make our lives a bit easier. We should bind the keys we
want to use for our new commands. I chose to use the M key for Medic, and the G
key for grunt. There's one small problem with this approach. The G key is
already bound to use grenades. For me this isn't a problem, because I
always choose grenades from the Inventory screen when I want to use them. But
it might be a pain for you. In fact, this is the biggest limitation in Quake 2
I've found yet. The lack of enough viable free keys to bind to custom commands.
You can't use shifted keys, so there's a slight shortage of keys that are
usable. At any rate, here's how I bound the keys. Remember to use correct
syntax for your new command: \
bind g "cmd grunt" bind m "cmd medic"
Once
again, pay attention to the actual string. It must be "cmd
grunt", not just "grunt".
Adding
new code to set the player's MOS
We're almost to the end of
the trail for now. All we need is to actually create the code for the function Cmd_MOS_f().
As you probably guessed, this new code goes in our custom code file, b_playermos.c.
Open it up, and add the following new code:
//+ - BD - 1/5 - THE ENTIRE CODE BLOCK IS NEW! // Cmd_MOS - 1/5 - BD This function sets the ent->client->resp.mos value void Cmd_MOS_f(edict_t *ent, char *cmd) { //If the player already has an MOS, tell him what it is and return if(ent->client->resp.mos > 0) { //Let the player know he can't change MOS, and tell him what he is... if(ent->client->resp.mos == 1) { gi.centerprintf(ent,"Sorry Soldier.\nYou can't change from MOS 11B\n"); } else if(ent->client->resp.mos == 2) { gi.centerprintf(ent,"Sorry Soldier.\nYou can't change from MOS 91B\n"); } //Play a sound for the player. gi.sound(ent, CHAN_VOICE, gi.soundindex("items/damage2.wav"), 1, ATTN_NORM, 0); //Bail out now. We don't want to execute what's below return; } //Otherwise, assign an MOS now... gi.cprintf(ent,PRINT_HIGH,"Got: %s\n",cmd); //We MUST use Quake's string functions. The string isn't a normal one! if(Q_stricmp (cmd, "grunt") == 0) { ent->client->resp.mos = 1; gi.centerprintf(ent,"You have chosen MOS 11B - Infantryman.\n\nGood Luck!\n"); } else if(Q_stricmp (cmd, "medic") == 0) { ent->client->resp.mos = 2; gi.centerprintf(ent,"You have chosen MOS 91B - Combat Medic.\n\nGood Luck!\n"); } else { //For completeness. We should NEVER get here. ent->client->resp.mos = 0; gi.centerprintf(ent,"Invalid MOS selection!\n"); } //Play a sound just to be cool... gi.sound(ent, CHAN_VOICE, gi.soundindex("player/male/jump1.wav"), 1, ATTN_NORM, 0); } //+ - BD - END NEW CODE BLOCK
Well,
that was a mouthful! Before you save the file, add the forward declaration of
our functions by adding the following include line:
#include "m_player.h" #include "b_playermos.h" //+ - BD - 1/7 Make our functions visible here
OK,
now save the file. Next, add a function prototype to the b_playermos.h
file. It should look like this:
void Cmd_MOS_f(edict_t *ent, char *cmd); //+ - BD - 1/5 - Declaration of the new function
Save
the header file.
The explanation: This function sets the mos
flag, checking it in the process. Note that it takes two arguments, the player
structure, and the command string that was given at the console by that player.
The code checks our mos flag. If the flag is greater than zero, we know
the user already has an MOS. In that case, we tell him so, tell him what MOS
he's chosen, and exit, playing a sound. If the flag is zero, it examines
the command string to determine what choice the player made, and sets the mos
flag appropriately, and displaying a message to confirm the user's choice. Upon
exit, a sound is played to give the user audible feedback as well.
One other thing to note,
since it'll be something you're bound to run into again in your Q2
programming. Note that the strings were processed for matching using the Q2
custom function Q_stricmp(). Strings passed from the console in Quake 2
are NOT regular C type strings. Therefore, if you try to use
regular matching strategies, your code will fail, and it will do so without an
error. So remember, and be aware!
Playing
sounds for effect in Quake 2
The sound is played using
yet another game interface function, gi.sound. This is another simple
yet ingenious function provided for our use by Id. Let's take a look at the
pseudoprototype for this function:
gi.sound(entity, channel, sound_index, volume, carry, delay)
The
arguments are:
So, in a
nutshell, that is how you play a sound in Quake 2!
The
result
You should now be able to
compile and install your modified DLL. When entering a deathmatch game, you
should see the banner asking you to choose your MOS. It'll stay visible until
you choose a valid MOS. After you've chosen, it'll tell you your MOS, and cue
you by playing a sound. If you try to change later, you'll get a polite error
message.
To be
continued...
Well, now you can see why
this tutorial is split in two. We're only half way home. We've enabled the
display, and created a way to capture the user's input and store the result in
our class flag. However we still need to implement the new abilities of our
player based on his MOS. That will be the subject of part 2 of the tutorial.
Extra Credit: To keep your mind occupied until
the conclusion, and to help you start expanding on these tutorials on your own,
try modifying the code in Cmd_MOS_f() to allow players to change MOS in
the course of the game, giving them appropriate feedback notifying them that
they've changed MOS.
Contacting
the Author
Questions? Problems? Write
me, and I'll try to answer your question or help you with debugging. Send your
queries to me here.
Tutorial written by GreyBear