Chapter 3: Spawning Magic
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:
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.
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.
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 ; i
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.
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] = the Q2 item to look for
//item[i] = the NS2 item to actually spawn
for(i = 0;i < 4; i++)
//If it's a null entry, bypass it
//Do the passed ent and our list match?
//Yep. Replace the Q2 entity with our own.
ent->classname = item->classname = sp_item[i];
//We found it, so don't waste time looking for more.
//gi.bprintf(PRINT_HIGH,"Found %s\nReplaced with %s\n",ent->classname,test);
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.
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.
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