You could joke that the Tutor Bot and water don't mix. You could quip that the Tutor Bot shouldn't be allowed in the deep end of the pool. You could laughingly say that the Tutor Bot will never win a gold medal in swimming.
Yeah, you could make all these jokes, but you'd be a cold-hearted bully that got his jollies at the expense of others. Remember, I meant the Tutor Bot code to be as simple and short as possible. Naturally, I planned to improve his skills all along.
So now is the time to improve his swimming. This will not be easy. We will not perfect his water skills. There are plenty of methods you as a bot author could implement to achieve the best swimming possible. But as with most lessons, we will learn a simple solution here and now.
It is important to note that there are two different goals to reach for while working on bot swimming: collecting items underwater, and getting the hell out of the water. With any luck, will will achieve both of these today. To some extent anyway. Basically, our bot will try looking for items until he runs out of air, then he'll head for the surface.
The file tutor.qc uses the subroutine check_for_water() to see if the bot is submerged in water. This is where it all starts. The beginning of that code looks like so:
// ------------------------------------------------ void() check_for_water = // ------------------------------------------------ { local float p; // bots don't see water like players do, so we check for it makevectors(self.angles); p = pointcontents(self.origin + v_forward*16); if (p != CONTENT_WATER && p != CONTENT_SLIME && p != CONTENT_LAVA) return;This part checks a point 16 units in front of the bot for contents other than just air. If it does not detect anything, it leaves the subroutine. We want to change that to the following:
// ------------------------------------------------ void() check_for_water = // ------------------------------------------------ { local float p; // bots don't see water like players do, so we check for it makevectors(self.angles); p = pointcontents(self.origin + v_forward*16); if (p != CONTENT_WATER && p != CONTENT_SLIME && p != CONTENT_LAVA) { self.air_finished = time + 10; return; }Alright, now if this code does not detect water, it makes sure the bot has ten seconds of air in his cute little lungs. (Oppositely, if it does see water, that air will begin to expire.) Then it leaves the subroutine.
if (p == CONTENT_WATER && time > self.pain_finished) { T_Damage (self, world, world, 5); self.pain_finished = time + 2; sound (self, CHAN_VOICE, "player/gasp2.wav", 1, ATTN_NORM); }This part damages the bot if he is underwater. We want to change this to reflect the air stored in his lungs. Change it to look like this:
if (p == CONTENT_WATER && time > self.pain_finished && time > self.air_finished) { T_Damage (self, world, world, 5); self.pain_finished = time + 2; sound (self, CHAN_VOICE, "player/gasp2.wav", 1, ATTN_NORM); }We changed the first line, so now he will only "choke" when his air is expired. Cool. Pretty simple, huh. Now we will examine the end of that routine, which looks like this:
self.flags = self.flags - (self.flags & FL_ONGROUND); // he'll try to swim upward here self.velocity = self.velocity + (v_forward * 200); self.velocity_z = self.velocity_z + 200; if (random() < 0.4) self.velocity_x = self.velocity_x + 100; else if (random() > 0.8) self.velocity_y = self.velocity_y + 100; };This is butt-simple code that basically pushes the bot upward, then left or right. Here we shall make are significant alterations. Change the above code to the below code:
if (p == CONTENT_SLIME || p == CONTENT_LAVA || time > self.air_finished) { bot_swim1(); return; } };I bet you were expecting our changes to be longer than the original code, huh? Well, in this instance what we don't do is just as important as what we do. Here, if slime or lava is detected, the bot will swim upwards (we assume that all bots want to keep on living) and leave the subroutine. If the bot is out of air, he will also try to get out of the water, leaving the subroutine as well.
void() bot_swim1 =[ $rockrun1, bot_swim2 ] {swim_up();}; void() bot_swim2 =[ $rockrun2, bot_swim3 ] {swim_up();}; void() bot_swim3 =[ $rockrun3, bot_swim4 ] {swim_up();}; void() bot_swim4 =[ $rockrun4, bot_swim5 ] {swim_up();}; void() bot_swim5 =[ $rockrun5, bot_swim6 ] {swim_up();}; void() bot_swim6 =[ $rockrun6, bot_swim7 ] {swim_up();}; void() bot_swim7 =[ $rockrun1, bot_swim8 ] {swim_up();}; void() bot_swim8 =[ $rockrun2, bot_swim9 ] {swim_up();}; void() bot_swim9 =[ $rockrun3, bot_swim10 ] {swim_up();}; void() bot_swim10 =[ $rockrun4, bot_swim11 ] {swim_up();}; void() bot_swim11 =[ $rockrun5, bot_swim12 ] {swim_forward();}; void() bot_swim12 =[ $rockrun6, bot_swim13 ] {swim_forward();}; void() bot_swim13 =[ $rockrun1, bot_swim14 ] {swim_forward();}; void() bot_swim14 =[ $rockrun2, bot_swim15 ] {swim_forward();}; void() bot_swim15 =[ $rockrun3, bot_swim16 ] {swim_forward();}; void() bot_swim16 =[ $rockrun4, bot_swim17 ] {swim_forward();}; void() bot_swim17 =[ $rockrun5, bot_swim18 ] {swim_forward();}; void() bot_swim18 =[ $rockrun6, bot_swim19 ] {swim_forward();}; void() bot_swim19 =[ $rockrun1, bot_swim20 ] {swim_forward();}; void() bot_swim20 =[ $rockrun2, bot_walk1 ] {swim_forward();};Hmm, interesting. This is a script that lasts two seconds. As you can see from the end of each line, the bot will call the routine swim_up() for the first ten frames. He will call swim_forward() for the last ten frames. Thus, he is moving up, then forward, like an upside-down L shape.
// ----------------------------------- void() swim_up = // ----------------------------------- { self.flags = self.flags - (self.flags & FL_ONGROUND); self.velocity_z = 300; }; // ----------------------------------- void() swim_forward = // ----------------------------------- { self.flags = self.flags - (self.flags & FL_ONGROUND); makevectors(self.angles); self.velocity = v_forward * 300; };These routines do exactly what they say, so that is simple stuff. One last note: near the beginning of the tutor.qc file, you must declare the routine bot_swim1() with a line like this:
void() bot_swim1;And, if I'm not mistaken, that's a done deal. Just keep in mind there are two kinds of swimming AI routines: 1) the perfect swimming AI routine; 2) all the others. And to my knowledge, the perfect swimming AI routine has never been written. So that leaves us all in, um, the same boat, so to speak. What I'm trying to say is that there are a hundred and one ways to improve this improvement.