You may have noticed that the monsters in Quake don't do much.
They don't jump, sing or dance. They don't even duck or evade. But they could; QuakeC has the power to make them do all of this. Well, maybe not sing.
In this lesson we will be learning about animation frames and sequences. You might not do any special scenes for your monsters, so it might not seem relevant. But every Quake creature thinks during his animation, therefore it's good to know.
By the end of this tutorial, we will have an enforcer who ducks under projectiles.
Quake monsters think one time per frame of animation in the game. That is, they think 10 times a second, or every 0.1 seconds. Their thoughts and motions are grouped into sequences that look like this:
void() enf_stand1 =[ $stand1, enf_stand2 ] {ai_stand();}; void() enf_stand2 =[ $stand2, enf_stand3 ] {ai_stand();}; void() enf_stand3 =[ $stand3, enf_stand4 ] {ai_stand();}; void() enf_stand4 =[ $stand4, enf_stand5 ] {ai_stand();}; void() enf_stand5 =[ $stand5, enf_stand6 ] {ai_stand();}; void() enf_stand6 =[ $stand6, enf_stand7 ] {ai_stand();}; void() enf_stand7 =[ $stand7, enf_stand1 ] {ai_stand();};Okay, now don't get all nervous on me. This may look ugly, but it is simple to understand. It's just a type of QuakeC shorthand. There are seven numbered subroutines here. Let's pull one apart and see what it means without the shorthand:
void() enf_stand1 =
{
self.frame = $stand1;
self.think = enf_stand2;
self.nextthink = time + 0.1;
ai_stand();
};
That's the deal. It's just a sequence of events that loops over and over (note the "enf_stand1" in the last line of code). It's his camping behavior.
If we wanted to screw around with it, we could change it to this:
void() enf_stand1 =[ $stand1, enf_stand2 ] {}; void() enf_stand2 =[ $stand1, enf_stand3 ] {}; void() enf_stand3 =[ $stand1, enf_stand1 ] {};Notice I changed all his animation frames to $stand1, which means his body won't move or vibrate at all. I also removed the ai_stand(), which means he won't look for enemies; in fact, here he is not really thinking about anything at all.
void() enf_camp1 =[ $stand1, enf_camp2 ] {ai_stand(); self.angles_y = self.angles_y - 15; }; void() enf_camp2 =[ $stand2, enf_camp3 ] {ai_stand(); self.angles_y = self.angles_y - 15; }; void() enf_camp3 =[ $stand3, enf_camp4 ] {ai_stand();}; void() enf_camp4 =[ $stand4, enf_camp5 ] {ai_stand();}; void() enf_camp5 =[ $stand5, enf_camp6 ] {ai_stand(); self.angles_y = self.angles_y + 15; }; void() enf_camp6 =[ $stand6, enf_camp6 ] {ai_stand(); self.angles_y = self.angles_y + 15; };Cool, now we have a "camp" sequence for the enforcer. It's just about the same as his stand routines above, only it's more realistic: he'll now pivot one way, then the other, as if he were looking around the room.
void() enf_duck1 =[ $paind4, enf_duck2 ] { ai_face(); self.solid = SOLID_NOT; }; void() enf_duck2 =[ $paind5, enf_duck3 ] {}; void() enf_duck3 =[ $paind6, enf_duck4 ] {}; void() enf_duck4 =[ $paind7, enf_duck5 ] {}; void() enf_duck5 =[ $paind7, enf_duck6 ] {}; void() enf_duck6 =[ $paind7, enf_duck7 ] {}; void() enf_duck7 =[ $paind7, enf_duck8 ] {}; void() enf_duck8 =[ $paind7, enf_duck9 ] {}; void() enf_duck9 =[ $paind7, enf_duck10 ] {}; void() enf_duck10 =[ $paind7, enf_run1 ] { self.solid = SOLID_SLIDEBOX; };There we have it. There's not much to know. We use the animation frames from the pain script (which are saved in the .mdl file, by the way), to make it look like he's ducking.
We have the ducking now; we just need to tell him when to do it. The variable "attack_finished" is set for the length of an attack whenever a player fires. Therefore, if the current game time is less than the "attack_finished" of a monsters's enemy, that means that player is attacking, right?
Open up AI.QC and scroll down to the last routine, ai_run(), the part that looks like this:
// look for other coop players if (coop && self.search_time < time) { if (FindTarget ()) return; } enemy_infront = infront(self.enemy); enemy_range = range(self.enemy); enemy_yaw = vectoyaw(self.enemy.origin - self.origin);Then after this line add this:
if (self.classname == "monster_enforcer" && time < self.enemy.attack_finished && random() < 0.25) { enf_duck1(); return; }Uh-oh, that long line looks scary. But it isn't really. Here's what it says in English:
"If the current monster is an enforcer and his enemy is attacking and he feels like it, then he should duck."
First, we check the classname of the monster that is thinking, since all monsters use ai_run(). If he's an enforcer, we look to see if his enemy's "attack_finished" variable is active.
But then we don't want him to duck under every attack. Therefore we check a random number, which will allow him to duck only 25 percent of the time.
If all these situations are true, we call the duck routine and then "return," or leave the current subroutine.
If I talk any more, I'll make it seem more complicated than it is (it is very simple), so I'll shut up. Just compile it and check out a map like E2M1. Your rockets will go right over that ducking enforcer.