
GreyBear's Quake 2 Tutorials
Chapter
2: Medic! - Part II
Requirements
Contents
Introduction
Hello again! OK, if you've
gotten this far, you should now have a mod that displays a menu when you start
deathmatch, and allows you to choose an MOS. hen you choose and MOS, you should
get a sound for feedback. Also, you should get a message and a sound if you try
to change MOS's mid game.
In part II, we're going to
put that menu and the choice you make to good use, creating a player medic that
heals other players by touching them. We'll also explore the secrets of adding
new icons and functionality to the player status bar in order to be able to
view the amount of healing ability our medic has.
Before we continue, I'd like
to take a moment to express my gratitude to two colleagues who made significant
contributions to this tutorial. Rohan and Tar. Thanks for your
insights, guys...
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.
Housekeeping
from Part I
You probably should
re-examine Part I quickly if you haven't looked at it recently. I made some minor
changes to it in order to make our tasks here in part II a little easier. I
also have one more change in part I code that you need to make now, before you
continue. This change will make your code more readable, and will help you keep
straight on MOS variable values in the code. We're going to add #declare
statements to the b_playermos.h code file to allow us to use names
instead of numbers to represent MOS's in the game code. So, add the following
to the end of the b_playermos.h code:
//+ BD - 1/15 This file forward declares our function prototypes for inclusion into Q2 source //+ BD - 1/15 Defines to make our code easier to read #define GRUNT 1 //+ BD #define MEDIC 2 //+ BD
Save
the file. Now, go through the b_playermos.c file, replacing the number
in the MOS tests with the appropriate defines (either GRUNT or MEDIC).
Now, for the rest of our code, we'll use these declares to make our code easier
to understand.
Creating
a heal_health variable
Now that we have the
ability to choose our Medic MOS, we need a way for him to collect and then
distribute healing health points to other players. The way to do this is to
create a variable that holds an indicator of how many healing health points the
payer possesses. In order to allow our Medic to carry these points across
levels like regular health points, we should create our variable within the
persistant struct inside the edict->client struct that represents the
player. Add the following code to the client_persistant_t struct in g_local.h
file:
int max_health; int heal_health; //+BD - 1/15 - The amount of health points to heal others with int selected_item;
Save
the file. Now we have the variable to hold our healing health points.
Writing
code to save heal_health points
Now, let's add a test to
determine how our Medic picks up Health items. If a health item is encountered,
and he himself is healthy, those health points get added to his heal_health
variable. We'll write a new function in the b_playermos.c file, after
doing the test in g_items.c. First, the altered code in g_items.c.
Add to the Pickup_Health() function like so:
qboolean Pickup_Health (edict_t *ent, edict_t *other) { if (!(ent->style & HEALTH_IGNORE_MAX)) { //+BD - 1/15 Added braces to contain both sub if's //+ BD - 1/15 If we're in deathmatch AND we're a medic, AND we're healthy... if( deathmatch->value && other->client->resp.mos == MEDIC && other->health >= 100) { Set_Heal_Health(ent,other); //+ BD 1/15 ...then grab the health and add to healing health instead of regular health return true; //+ BD - 1/15 We took it, so return true... } //+BD - 1/15 Original code begins with the if below. End NEW CODE if (other->health >= other->max_health) return false; } //+ BD - 1/15 added brace other->health += ent->count;
Save
the file. Now, open up b_playermos.c. Once again, the actual working
code has been placed in our own file to keep it separate. Next, we're going to
write the function Set_Heal_Health(). This function will determine what
type of health item was picked up, and what it's health value is. It'll also
determine the sound to play, and add the health points to our heal_health
variable for later use. Here's the code. Add it to the end of the file:
//+BD - 1/15 - If we're a medic, health above 100 goes to healing //+BD - 1/15 THIS ENTIRE CODE BLOCK IS NEW! void Set_Heal_Health(edict_t *ent, edict_t *other) { if (ent->count == 2) //BD ent->count is the number of health points ent->item->pickup_sound = "items/s_health.wav"; //BD and the sound associated w/ it. else if (ent->count == 10) ent->item->pickup_sound = "items/n_health.wav"; else if (ent->count == 25) ent->item->pickup_sound = "items/l_health.wav"; else // (ent->count == 100) ent->item->pickup_sound = "items/m_health.wav"; other->client->pers.heal_health += ent->count; //BD - 1/15 - Up the heal_health by the size of the health pkg //BD We'll be returning HERE later to add a status bar display for user feedback if (!(ent->spawnflags & DROPPED_ITEM)) SetRespawn (ent, 30); }
Finally,
we need to add the forward declaration of our new function to the header file,
so it's visible to the other code files that may execute it. Add it to playermos.h
like so:
//Functions we use void CheckMOS(edict_t *ent); void Cmd_MOS_f(edict_t *ent, char *cmd); void Set_Heal_Health(edict_t *ent, edict_t *other); //+BD 1/15 - Our new player function
Save
the file. So far, so good...
Modifying
the ClientThink()function
Now we're going to have a
little discourse on Quake 2 game logic, and why things work the way they do. In
Quake 2, entity collision, or touching, happens in a manner opposite of
what you might think would be normal at first. If your player character touches
something else, that player takes no special note, other than to catalog the
touch in an array. Then, when it's time to move in the game, the code loops
through this array and performs the action appropriate for each touch. What's
backwards is that the player isn't the one to decide what to do. The other
object determines what happens. Upon examination, this is pretty smart. With
this method, each object only needs to remember one function for touching
occurrences, namely its own.
This makes life a little
bit difficult for us, however, for two reasons, one general, and one specific:
So, what
do we do? Well, one answer is to alter the function that triggers actions when
two entities collide, and after doing our own testing, supply the correct
action. We do this in the p_client.c code file, specifically in ClientThink().
This function is the place where almost all essential player actions occur. The
touch checks occur right after the player move occurs. Here's the altered code:
if (!other->touch) { //+ BD -1/16 Here's where we heal the other player if needed if(other->client && ent->client->resp.mos == MEDIC) //+BD - 1/16 If the other ent is player, and we're a medic... { Player_Heal(ent,other); //+BD - 1/16 Check and heal if conditions met } continue; }
Save
the file. Yet again, we're going to segment the actual working code in our own
code file, b_playermos.c.
The next step is to create
the code that actually contains the healing logic, so let's do a little
conceptual thinking. Before we heal the player, several things have to be true.
Remember from above tough, we've already tested two conditions to get to our new
code execution, namely that we touched another player, and we are a medic. So
now, we need to list the remaining conditions our new code needs to test before
healing occurs:
OK, if the
above conditions are met, then we can heal the other player. Then we simply
calculate how many points the other player needs, and take either the needed
points, or all the healing points possessed by our medic, whichever is greater.
OK, here's the code:
//+BD - 1/16 -THIS ENTIRE CODE BLOCK IS NEW! //+BD 1/16 - Player_Heal() - Check and heal if appropriate void Player_Heal(edict_t *ent, edict_t *other) { //BD local vars int heal_needed; //How much healing our patient needs int heal_left; //How much our medic has to give //+BD First, figure out how far below the max health our patient is //If we're at max health, or dead (at the other extreme) exit quietly... if( (heal_needed = other->max_health - other->health) <=0 || ent->health <=0) return; //If not, lets' figure out our current supply situation if( (heal_left = ent->client->pers.heal_health) <=0) { //No healing points. Exit quietly... return; } //OK, now we figure out whether we have enough points to completely heal the pt. if(heal_left >= heal_needed) { //Enough to completely heal the patient other->health += heal_needed; //Add back the points we need to be healed heal_left -= heal_needed; //...and subtract them from the medic's points gi.centerprintf(ent,"%s completely healed!\n",other->client->pers.netname); gi.centerprintf(other,"%s has healed your battle wounds!\n",ent->client->pers.netname); //Play a sound for the player. gi.sound(ent, CHAN_VOICE, gi.soundindex("items/m_health.wav"), 1, ATTN_NORM, 0); } else { //Not enough. Use what we have. other->health += heal_left; //Take all the points remaining heal_left = 0; gi.centerprintf(ent,"%s partially healed.\n",other->client->pers.netname); gi.centerprintf(other,"%s has treated your battle wounds\nas best as possible.\n",ent->client->pers.netname); //Play a sound for the player. gi.sound(ent, CHAN_VOICE, gi.soundindex("items/n_health.wav"), 1, ATTN_NORM, 0); } //Now, set the variables for the medic based on what we actually used... ent->client->pers.heal_health = heal_left;}
OK,
now save the file.
The Explanation: Hey, you guys are getting good by
this time, judging from the feedback and questions I get, so I figure you've
followed what's come through up to now. However that last bit was all new code,
and it deserves explaining.
First, note the functions
arguments. We're passing two edict structs, ent and other. Ent is our medic.
Other is the player to heal. Next, we declare some local variables. These
variables never get seen outside this function. Their sole purpose is to
provide us a place to store values we calculate, and make it easier to refer to
variables buried in structures. Saves on typing. Next, we perform a check to
make sure that the other player isn't on either end of the health spectrum.
That is, the other player isn't fully healthy, and he's not dead. If either
applies, we return, because we can't help him. Next, we look to see if our
medic has any healing points to spare. If not, again we go away. If we get past
this point, we roll up our sleeves and get to work. We now need to determine
whether we can fully or partially heal the player with the healing points our
medic has, and take the appropriate action. We then print a message to both
players, and play a sound for feedback purposes. Finally, we set the new value
of the medic's heal points to either the remainder left over, or zero.
Next, add a function
prototype to the b_playermos.h file. It should look like this:
void Player_Heal(edict_t *ent, edict_t *other); //+ - BD - 1/16 - Declaration of the new function
Save
the header file. We're almost home...
Displaying
healing health points on the status bar
The last thing we need to
do is to give the medic a visual gauge to tell him how many healing points he
possesses. This way, he knows when to go hunting for MedKits. This subject
could take a tutorial all by itself to explain fully (and might if I get around
to it), but for now I'll give you the quick 'n' dirty explanation of how to
display stuff on the statusbar.
First, you need to
understand that there are three parts to creating the statusbar.
Pardon my
spooging, but I can't help saying it again. Id really writes COOL code.
The statusbar code concept is so easy that once you grasp the structure of the
macro language and determine where to set the values, it's a snap to modify the
statusbar almost any way you wish.
OK, first we need to add a
declaration for our heal_health variable in the STAT_* list within q_shared.h.
This serves as an array index to allow the statusbar code to get at the actual
numeric value of our heal_health points. Here's the code: !-Code block-->
#define STAT_FLASHES 15 // cleared each frame, 1 = health, 2 = armor #define STAT_HEAL 16 //+ BD 1/20 - Added for healing point display above health #define MAX_STATS 32
Note
that the maximum number of STAT variables is 32... That means we've got room to
spare for further additions down the road. Next, we need to insert a new macro
into the code within g_spawn.c. This is tricky to find, since it's not
commented at all.... Go to line 590, and scroll down from there. You'll see a
list of grouped lines enclosed in quotes. This is actually two macro sets, one
for a single player statusbar, and another for a deathmatch statusbar. Look for
the code below and make the modifications indicated:
char *dm_statusbar = "yb -24 " // health "xv 0 " "hnum " "xv 50 " "pic 0 " //+ BD 1/20 Healing Points "if 16 " //+BD 1/20 The index of our STAT value defined in q_shared.h " xv 75 " //+BD 1/20 The X value from left of screen for the numeric display //" yb -48" //+BD 1/20 Y value from bottom of statusbar for numeric display. " num 3 16" //+BD 1/20 Display a number - 3 digits use the value of STAT index #16 " xv 125 " //+BD 1/20 X value from left screen of icon //" yb -48" //+BD 1/20 Y value from bottom of statusbar for icon " pic 0 " //+BD 1/20 index of picture array to show as icon "endif " //+BD 1/20 Close the if statement
We
want to change only the deathmatch statusbar (hence we change below *dm_statusbar).
The comments in our added code are pretty straightforward. We're adding a new
macro to tell the statusbar code where and how to display our heal_health
value, along with a health icon.
Finally, we need to make
an addition to the p_hud.c file. This file is where the code lives to
draw the statusbar or heads up display (hud, get it?). First, add the
declaration for our functions by including the header file:
//+ BD 1/20 Add include for declare statements #include "b_playermos.h"
Then,
insert code to make our heal_health value display on the statusbar:
ent->client->ps.stats[STAT_HEALTH] = ent->health; //+ BD 1/20 Add Healing points to display. They get displayed directly above //+ BD the health points if greater than zero... if(ent->client->resp.mos == MEDIC) //+BD 1/20 If we're a medic... { //+BD 1/20 ent->client->ps.stats[STAT_HEAL] = ent->client->pers.heal_health; //+BD 1/20 associate our value with the STAT_HEAL index pointer } //+BD 1/20 // // ammo
The
result
We're done! Compile and
run your new DLL as usual. You now have a fully functional medic player class.
With a little elbow grease and a good idea, it shouldn't' be hard to extend
this tutorial to cover multiple classes.
Extra Credit: Implement a max_heal variable
that caps the number of healing points a medic may accumulate.
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