We have a good lesson today, class. I mean, damned good. If the previous tutorials were high school, this is college. Without the beer, of course.
Today's lesson has made some programmers, and broken others. It can take 50 pages worth of code. It can drive you to drink. It is navigation.
There has never been a monster that roams the level and picks up player items. This is just not done. It must frighten the average coder. Today we'll learn how to do it in three subroutines.
The problem is, monsters are not allowed to touch the items. At all.
Step 1. We take a quick look at id Software's roaming routine, then improve it:
/* ============= ai_walk The monster is walking it's beat ============= */ void(float dist) ai_walk = { local vector mtemp; movedist = dist; if (self.classname == "monster_dragon") { movetogoal (dist); return; } // check for noticing a player if (FindTarget ()) return; movetogoal (dist); };Hmm, that's a bit fat. Meaning, seven of those lines aren't even needed (QuakeC is full of fat). The FindTarget() looks for enemies, so we need that.
/* ============= ai_walk The monster is looking for an collecting items. ============= */ void(float dist) ai_walk = { // check for noticing a player if (FindTarget ()) return; bot_search_for_items(); bot_grab_items(); movetogoal(dist); };I hope that is self-explanatory. If not, well, the first section, between the double lines, is a remark. Next comes the name of the routine. The part that says "float dist" is called a parameter. This is a variable that is passed to this routine from another part of the program. For example, when we call this subroutine, it will look like this:
ai_walk(20);Of course, we must always check for a player, here with FindTarget().
Step 2. The movetogoal() function will navigate towards the entity stored as the monster's goalentity. Thus, we need to find items. We will simply check a circle of area around him for entities.
Insert this subroutine before the previous one:
// ------------------------------------------------ void() bot_search_for_items = // ------------------------------------------------ { local entity item; if (time > self.search_time) self.goalentity = world; if (self.goalentity) return; item = findradius(self.origin, 1500); while(item) { if ( (item.flags & FL_ITEM) && visible(item) && item.model != string_null) { self.goalentity = item; self.search_time = time + 30; } item = item.chain; } };The best way for us to understand what all this means is to write it out in English. Here goes:
If I have searched for my goal for long enough, I'll give up on that item. But if I already have a goal, I'll just leave the subroutine.
I will look at entities within 1500 units around me. While there are items to look at, I'll keep looking.
If I find one that is an item and is visible to me and that is not invisible right now, then I will choose that as my goal. I will look for it for the next thirty seconds.
If there are more entities, I will keep looking at them.
Okay, good. That makes sense. So now he has a goal entity and will try movetogoal() for the next 30 seconds. But remember, in QuakeC the monsters are not allowed to touch things. If you open ITEMS.QC and look at the health_touch() routine, you will see this bad, bad line:
if (other.classname != "player") return;Boooo! Hissss! This means that if the entity which is touching the health is not named a player, then he's not allowed to enter the subroutine. I guess id didn't want us to make mobots. So what are we going to do, dammit?
Step 3. In life they say if you want something, you have to take it. And that's just what we're gonna do.
Paste this little routine above the previous two:
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ void() bot_grab_items = // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ { if (self.goalentity == world) return; if (vlen(self.origin - self.goalentity.origin) <= 70) { self.goalentity.solid = SOLID_NOT; self.goalentity.model = string_null; self.goalentity.nextthink = time + 20; self.goalentity.think = SUB_regen; if (self.goalentity.healamount) sound (self, CHAN_ITEM, "items/health1.wav", 1, ATTN_NORM); else if (self.goalentity.weapon) sound (self, CHAN_ITEM, "weapons/pkup.wav", 1, ATTN_NORM); else sound(self, CHAN_ITEM, "items/armor1.wav", 1, ATTN_NORM); self.goalentity = world; } };Beautiful, just beautiful. This solves our item problem the old-fashioned way: by cheating.
Uh, okay, unless I'm mistaken, that's it. But I will recap the ideas we've executed. First, while our monster walks, he looks for any entity that is flagged as an item. Second, he uses movetogoal() to move toward that item. In addition, he uses movetogoal() to move randomly if he has no goal entity.
Third, if he comes close to his goal, he turns it invisible and makes a pick-up sound, thus pretending to pick it up. Finally, he resets his goal entity to world and moves on to other parts of the map. The previous goal item will reset in about 30 seconds.
I hope you understand all that simple stuff, because if you don't, I'm gonna come over there and smack you.