
GreyBear's Quake 2 Tutorials
Chapter
3: Spawning Magic
Requirements
Contents
Self Serving Editorial
Hello again! As many of
you know, I've been pretty busy as the lead programmer for Navy Seals
2 (NS2). It's been a while since I've done a tutorial because of the turn
'n' burn coding we've been doing, but I finally caught a break. The good
news is that all this coding has provided the fodder for several new and unique
tutorials. While, I'm editorializing, I'd like to make a statement about the
state of Q2 tutorials on the web. If you're just here for the code, breeze on
by this part, otherwise, pull up a stump:
SOAPBOX
I've noticed the tendency on some sites to keep a running score of how many
tutorials get posted. I think it's great that folks are sharing their hard won
knowledge, but the quality of some of these tutorials makes me think
that they've been thrown together to satisfy someone's tutorial body count
rather than to serve any useful function. I've been criticized for not
producing a steady stream of tutorials of late. What I haven't been
criticized for is lack of content in my tutorials. I'd rather deliver a little
quality than a lot of quantity. You can get schlock anywhere. If it's got my
name on it, I promise it'll be the best I can deliver. QED.
/SOAPBOX
Introduction
Whew! OK with the SSE out
of the way, what we're going to tackle in this edition is something that I
think is unique. As I worked on NS2, I realized that playing the standard Q2
maps under NS2 is a bit difficult, as there aren't any NS2 weapons in them.
Also, NS2 removes all the standard Q2 weapons from the game, hence, you
get dropped into the game with your trusty Mk23 pistol, and nothing else. The
Mk23 is a nice weapon, but it makes for slow, painful deathmatches. What I
needed was a way to replace the standard Q2 weapons with my own NS2 weapons.
Sure, we could (and have) create our own NS2 maps, but for testing (and for
playing our fav Q2 maps) it's nice to have a quick map that's already in place.
Problem is, the maps already have spawn points listed for Quake 2 weapons, and
we don't want to have to add our own, and then try to recompile those huge
maps. The answer? The DLL, of course! We grab the Q2 spawn name, and using a
lookup table, replace it with our own!
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 in a comment field along with my initials, to clue
you in that the line was added to the original surrounding code.
Whither
Spawning?
The first thing we need to
know is where the actual spawning of functions occurs. In order to sneak our
own weapons into others' maps, we have to catch the spawning name as it's read
into the DLL to be spawned as an entity, and then substitute it with our own
spawning name. The nice thing about this approach is that it only has to be
done once. The code keeps internal track of what got spawned where, and
throughout the life of the map, the same item get spawned at that place ad
infinitum.
NOTE: This tutorial assumes you're going
to be using this idea in a mod of your own, where you have removed some or all
of the standard Q2 weapons with ones of your own. So, if you're playing along
at home and haven't done so yet, go to g_items.c and remove a few
weapons so you'll get the results we're after.
So, where do we find the
right code? In g_spawn.c, of course! Take a look at the code and find
the ED_CallSpawn() function. What we want to do is add a call to a
function we'll create to check each item as it comes up to be spawned. If the
game can't find a spawn function for it because we've removed it, we'll replace
it in our new function. Here's the original spawn code along with the
modification to call our function:
// check item spawn functions for (i=0,item=itemlist ; iclassname) continue; if (!strcmp(item->classname, ent->classname)) { // found it SpawnItem (ent, item); return; } //+BD 6/16/98 - If the item didn't match, it's probably needing to be replaced //+BD NEW CODE else { //6/16/98 - Check the item. If it's a Q2 item we don't want in NS2, //replace it with an NS2 item... CheckItem(ent,item); } //+BD END NEW CODE }
You've
been around long enough that I don't need to tell you to save the file, right?
OK. One more thing: To avoid problems with forward declaration, we need to
declare the form of our CheckItem() function for the compiler before we
use it. Since we're only going to be adding one function to the DLL today, I've
elected to just add a #define to the top of the g_spawn.c file
rather than write an include file. Here is what is should look like:
//+BD 6/16/98 - Our defines for the item check function void CheckItem(edict_t *ent, gitem_t *item);
The
Explanation: You
may remember from the Sabotage
tutorial that all non player entities in the game are kept in the itemlist[]
array. What's happening here is that we're looping through itemlist[]
looking at the classname of each item. If it matches the entity that is
indicated to exist within the current map, we spawn it and move on. Originally,
if no match was found, the DLL would print a warning on the console at map load
time. What we added was an extra check to see if the currently indicated entity
has a replacement item that we want spawned instead. That's the purpose of the CheckItem()
function, which we're about to write.
Weapons
Check!
Now for the meat of the
thing. Writing our CheckItem() function. Create a new file called b_check.c,
and drop the following code in:
// + BD CheckItem() ENTIRE CODE BLOCK IS NEW! // Checks item about to be spawned. If a Q2 item we don't want in the game, // we replace it with an appropriate NS2 item. void CheckItem(edict_t *ent, gitem_t *item) { //An 2D array of items to look for and replace with... //item[i][0] = the Q2 item to look for //item[i][1] = the NS2 item to actually spawn char *sp_item[4][2]= { {"weapon_machinegun","weapon_MP5"}, {"weapon_supershotgun","weapon_shotgun"}, {"weapon_grenadelauncher","weapon_M203"}, {"weapon_rocketlauncher","weapon_M203"}, {"weapon_railgun","weapon_M88"} }; int i; for(i = 0;i < 4; i++) { //If it's a null entry, bypass it if(!sp_item[i][0]) continue; //Do the passed ent and our list match? if(Q_stricmp(ent->classname,sp_item[i][0])==0) { //Yep. Replace the Q2 entity with our own. ent->classname = item->classname = sp_item[i][1]; SpawnItem(ent,item); //We found it, so don't waste time looking for more. //gi.bprintf(PRINT_HIGH,"Found %s\nReplaced with %s\n",ent->classname,test); return; } } }
Explanation: What we've done is create a mini
lookup table of standard Q2 weapons, and the NS2 weapons to replace them with.
Note that unless you add weapons to the itemlist[] array that have the
same names as our NS2 weapons, you'll have to alter the array entries to match
your own weapons. The array can be sized to fit your needs also. One thing of
note; I could make this code a bit more efficient by creating the lookup array
in g_local.h. That would mean the table would be created only once, at
run time, and would save a few CPU cycles. However, since this array will never
grow much beyond 10 weapons (the number of standard weapons in Q2), the speed
hit is negligible, and it makes the tutorial much easier to understand. Still,
it's worth a bit of extra credit if you figure out how to make the change.
Once we have built the
table, we merely compare the passed classname value with the table entries by
looping through the table itself. If we find a match, we substitute our weapon
classname for the original Q2 name, and Spawn it. If you want to see the
results as they happen, uncomment the gi.printf() line to get console
output of the weapons found and replaced by our code.
Double Extra Credit: Alter the code to allow for random
spawning replacement.
The
result
Geez, I know what was
supposed to be hard, but that's it! It's a wonder nobody thought of this before
(maybe they have, but I haven't seen a tutorial on this subject anywhere...).
Now pretty much any map can be playable with your own mod!
We talked about weapons
replacement in this tutorial, but as you have probably already noticed, an item
is and item. You can extend your lookup table to include any item spawned in
the Q2 universe, as long as it exists in the itemlist[]. This makes it
even cooler, since you can take a popular map created for Q2, and alter all the
items in it to suit your own modification.
ENJOY!
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