[Tutorial] Making Ogres lob grenades into your cover
Posted: Wed Aug 27, 2014 3:25 am
Do you think the original Ogre isn't challenging enough? In this tutorial we'll make him shoot grenades at your cover, remember the Half-Life HECU soldiers? This code is strongly inspired on the soldier's AI.
Step 1.
First, you'll open defs.qc and put these lines at the very bottom:
Attention!! This tutorial requires sqrt() function, I'll post a QuakeC sqrt() later, but if you want to run this on DarkPlaces, make sure you have dpextensions.qc, and put "dpextensions.qc" without quotes after "defs.qc", on your progs.src.
These lines will define the fields used by the Ogre, a dot float and two dot vectors. Any definition beginning with a dot (.) is called a field, which is a float, vector, entity assigned to another entity, like self.ammo_shells -- gets the quantity of shells of the player.
grenade_time will define how often the ogre will check for shooting grenades at your cover, as well as how often he actually shoots grenades at you.
last_enemy_origin will be the point where the ogre has last seen you.
grenade_velocity will be the proper grenade's velocity for reaching your cover.
Step 2.
Save and close defs.qc, and open ai.qc. Before ai_run(), paste this code (Warning! ignore this step if you're using DarkPlaces!):
This cute little function is a QC version of sqrt(), for those who don't want to use DarkPlaces. It's slow, but has little margin of error.
Now to explain it:
First we declared 3 local floats. A local float is a float only used on a specific function. Then we have:
This merely checks if 'n' is negative (less than zero), where 'n' is any number called to that function, like sqrt(81) will return 9. If 'n' is negative, it will print a developer message (only seen if developer is set to 1) and return -1, because a negative square root is impossible.
Then later we have:
This checks if 'n' is exactly 0 or 1, if so, will return itself.
Now we have the main part of the function:
First, we declare a minimum divisor (2), and the result, is 'n' divided by that number. Then, a while() loop begins. If we can reach our square root in less than 100 tries, then we're done. Otherwise, we mayn't reach our exact square root in 100 tries, but we'll quit to avoid runaway loop errors.
Inside that while(), the divisor will calculate the average value between the previos divisor and result, and then we divide 'n' by that new divisor, and finally we add 1 to 'i', which is the number of tries. And the cycle repeats until we get the square root.
Step 3.
Ignore step 2 if you're using DarkPlaces. Just before ai_run() (and after the QC sqrt()) paste this:
Explanations are all commented within the code...
Step 4.
Just before ai_run(), put this line:
This will declare here the ogre's lob animation, which we will create later in ogre.qc.
Now, scroll down to ai_run(), until you see this:
Here the monster will check if he can see his enemy, and if so, search for him for 5 seconds. I want you to replace this:
With this:
This single line we added, will make the monster update self.last_enemy_origin every time he can see his enemy.
Step 5.
Now before these lines:
Add this:
In English, this if() statement says:
"If I can't see my enemy, and I'm an Ogre OR and Ogre Marksman, and the difficulty level is more than Easy (skill 0), then I will check if I can lob a grenade into his sorry ass, and if I can successfully lob one, I will check for it again in 6 seconds, call my shoot animation, and do nothing after this."
Step 6.
Save and close ai.qc, and now open ogre.qc. After OgreFireGrenade(), I want you to paste this:
This is merely a copy of OgreFireGrenade(), we called it OgreFireGrenade2(), notice this line:
This will make the grenade's velocity to be that CheckToss() returned, to reach your cover.
Step 7.
Scroll down a little more, and you'll find this:
After that whole code, paste this:
ai_facetoss() is a new function, which will make the ogre face the direction the grenade is launched, using vectoyaw(self.origin + self.grenade_velocity * 64);
vectoyaw() converts a vector into yaw angles only, that means angles_y. And we detect the direction the grenade will go by adding self.grenade_velocity to self.origin, and we multiply by 64 just in case it gets too low.
ChangeYaw() is a built-in function, which turns a monster towards self.ideal_yaw limited by self.yaw_speed.
Later we created ogre_gren1(), followed by the next animation frames. We call ai_facetoss() on all of them, and OgreFireGrenade2() (our new grenade launch function), on the fourth frame, which is exactly when he is shooting a grenade. Each animation frames lasts for a tenth of a second (0.1), unless if you set a nextthink higher or lower than 0.1 on each animation function. So, this animation lasts for 0.7 seconds, as it has 7 frames (shoot2 is repeated here, it actually has 6).
After that, you can save, compile, and run.
Step 1.
First, you'll open defs.qc and put these lines at the very bottom:
Code: Select all
.float grenade_time;
.vector last_enemy_origin, grenade_velocity;
These lines will define the fields used by the Ogre, a dot float and two dot vectors. Any definition beginning with a dot (.) is called a field, which is a float, vector, entity assigned to another entity, like self.ammo_shells -- gets the quantity of shells of the player.
grenade_time will define how often the ogre will check for shooting grenades at your cover, as well as how often he actually shoots grenades at you.
last_enemy_origin will be the point where the ogre has last seen you.
grenade_velocity will be the proper grenade's velocity for reaching your cover.
Step 2.
Save and close defs.qc, and open ai.qc. Before ai_run(), paste this code (Warning! ignore this step if you're using DarkPlaces!):
Code: Select all
float(float n) sqrt =
{
local float divisor, result, i;
if (n < 0)
{
dprint ("Negative sqrt is impossible.\n");
return -1;
}
if (n == 0 || n == 1)
return n;
divisor = 2;
result = n / divisor;
while (divisor != result && i < 100)
{
divisor = (divisor + result) / 2;
result = n / divisor;
i = i + 1;
}
return result;
};
Now to explain it:
First we declared 3 local floats. A local float is a float only used on a specific function. Then we have:
Code: Select all
if (n < 0)
{
dprint ("Negative sqrt is impossible.\n");
return -1;
}
Then later we have:
Code: Select all
if (n == 0 || n == 1)
return n;
Now we have the main part of the function:
Code: Select all
divisor = 2;
result = n / divisor;
while (divisor != result && i < 100)
{
divisor = (divisor + result) / 2;
result = n / divisor;
i = i + 1;
}
return result;
Inside that while(), the divisor will calculate the average value between the previos divisor and result, and then we divide 'n' by that new divisor, and finally we add 1 to 'i', which is the number of tries. And the cycle repeats until we get the square root.
Step 3.
Ignore step 2 if you're using DarkPlaces. Just before ai_run() (and after the QC sqrt()) paste this:
Code: Select all
float(vector org, float radius) MonsterInRange =
{
local entity head;
local float yes;
head = findradius(org, radius); // finds entities wihthin a specified radius
while (head) // while 'head' is not world...
{
if (head.health > 0) // it must be alive
if (head.flags & FL_MONSTER) // must be a monster
if (head != self.enemy) // must not be the ogre's enemy (he may get mad at another monster...
{
traceline (org, head.origin + head.view_ofs, TRUE, self); // see if he can 'see' his enemy from his grenade launcher instead of his eyes
if (trace_fraction == 1.0) // trace_fraction of 1 means he can see it
yes = TRUE; // set yes to TRUE
}
head = head.chain; // go for next entity in the chain
}
return yes; // returns 'yes' value, TRUE or FALSE
};
float() crandom; // crandom() is declared on weapons.qc, which is compiled after ai.qc, so we have to redeclare it here
vector(entity e, vector spot1, vector spot2) CheckToss =
{
local vector midpoint, apex, vel;
local float grav, dist1, dist2, time1, time2, timeinv;
grav = cvar("sv_gravity"); // get sv_gravity value and store it on a float
if (spot2_z - spot1_z > 500)
return '0 0 0'; // if the enemy is too high above the ogre, do nothing, as the grenade can't reach there, and return '0 0 0'
makevectors (e.angles); // gets forward, right and up vectors for assigned entitty (e)
// these two lines will spread a little bit to left-right and forward-back the position where the grenade is launched. crandom() returns between -1 to 1,
// and here we multiply it by 8 and 16
spot2 = spot2 + v_right * (crandom()*8 + crandom()*16);
spot2 = spot2 + v_forward * (crandom()*8 + crandom()*16);
midpoint = spot1 + (spot2 - spot1) * 0.5; // detects midpoint of grenade trajectory with a simple calculation
traceline (midpoint, midpoint + '0 0 500', TRUE, e); // traces a line 500 units above the midpoint, ignoring monsters and other entities, and the ogre
// himself
midpoint = trace_endpos; // set midpoint to the end position of the traceline() (may be a ceiling)
midpoint_z = midpoint_z - 15; // subtract midpoint down to 15 units
if (midpoint_z < spot1_z || midpoint_z < spot2_z)
return '0 0 0'; // if the midpoint height (z means up-down value) is lower than 'spot1' (the ogre's origin) OR lower than 'spot2'
// (the ogre's enemy origin), then don't launch a grenade
dist1 = midpoint_z - spot1_z; // get distance between midpoint and ogre
dist2 = midpoint_z - spot2_z; // get distance between midpoint and the enemy (usually the player)
time1 = sqrt(dist1 / (0.5 * grav)); // calculate time it takes for grenade to reach the ogre (in seconds), use sqrt of dist1, divided by sv_gravity
// multiplied by half
time2 = sqrt(dist2 / (0.5 * grav)); // same as above, but this gets time to reach the enemy
if (time1 < 0.1)
return '0 0 0'; // if it takes less than a tenth of a second from midpoint to the ogre, don't shoot
timeinv = 1 / (time1 + time2); // inverts time1 + time2 (not negative)
vel = (spot2 - spot1) * timeinv; // calculates velocity based on distance from the enemy to the ogre, multiplied by the inverted time
vel_z = grav * time1; // calculate upwards velocity of the grenade, which is sv_gravity multiplied by the time it takes to reach from the ogre
// to the midpoint
apex = spot1 + vel * time1; // now, we calculate the apex of the shot, adding the ogre's origin to the grenade's velocity and multiplying by the time
// it takes to reach the midpoint from the ogre
apex_z = midpoint_z; // the apex's height is the same as the midpoint height, so the grenade will hit the ceiling as less as possible
traceline (spot1, apex, FALSE, e); // trace a line between the ogre and the apex, and don't ignore monsters
if (trace_fraction != 1.0)
return '0 0 0'; // if there's something blocking the grenade's path, don't shoot!
traceline (spot2, apex, TRUE, e); // trace a line between the ogre's enemy and the apex, but ignore monsters
if (trace_fraction != 1.0)
return '0 0 0'; // if there's a wall blocking the enemy's path, don't shoot!
return vel; // return the grenade's velocity
};
float(vector src) CheckThrowGrenade =
{
local vector vec, dist2d, toss;
if (time < self.grenade_time)
return FALSE; // if ogre has previously checked or lobbed a grenade, do nothing and return FALSE
if (!(self.enemy.flags & FL_ONGROUND) && self.last_enemy_origin_z > self.absmax_z)
return FALSE; // if the enemy is not on the ground, and the point where we last seen our enemy is above the top of the ogre's bounding box,
// do nothing
if (random() <= 0.5) // find feet, 50% chance
{ // magically know where they are
vec = self.enemy.origin; // this will throw a grenade at the enemy's feet
vec_z = self.enemy.absmin_z;
}
else
vec = self.last_enemy_origin; // toss it to where you last saw them
if (MonsterInRange(vec, 256)) // if there's another monster nearby, don't shoot!
{
self.grenade_time = time + 1; // check again in 1 second
return FALSE;
}
dist2d = vec - src; // calculate distance...
dist2d_z = 0; // ...without regarding height
if (vlen(dist2d) <= 256) // if length is less than or equal to 256 units...
{
self.grenade_time = time + 1; // again delay 1 second, do nothing after
return FALSE;
}
if (vlen(dist2d) > 1000) // if 2d distance is over 1000 units...
{
self.grenade_time = time + 1; // same as above, too far away
return FALSE;
}
toss = CheckToss(self, src, vec); // call CheckToss()... for the ogre, the ogre's origin (src), and the enemy's origin (vec)
if (toss != '0 0 0') // if 'toss' doesn't return '0 0 0'...
{
self.grenade_velocity = toss; // assing self.grenade_velocity to returned 'toss' vector
self.grenade_time = time + 0.3; // check again in the next third of a second
return TRUE; // return TRUE, this menas he can shoot a grenade at your cover!
}
self.grenade_time = time + 1; // if all above fails, delay 1 second, return FALSE, meaning it can't shoot :(
return FALSE;
};
Step 4.
Just before ai_run(), put this line:
Code: Select all
void() ogre_gren1;
Now, scroll down to ai_run(), until you see this:
Code: Select all
enemy_vis = visible(self.enemy);
if (enemy_vis)
self.search_time = time + 5;
Code: Select all
if (enemy_vis)
self.search_time = time + 5;
Code: Select all
if (enemy_vis)
{
self.last_enemy_origin = self.enemy.origin;
self.search_time = time + 5;
}
Step 5.
Now before these lines:
Code: Select all
if (CheckAnyAttack ())
return; // beginning an attack
Code: Select all
if (!enemy_vis && (self.classname == "monster_ogre" || self.classname == "monster_ogre_marksman") && skill > 0)
{
if (CheckThrowGrenade(self.origin))
{
self.grenade_time = time + 6;
ogre_gren1 ();
return;
}
}
"If I can't see my enemy, and I'm an Ogre OR and Ogre Marksman, and the difficulty level is more than Easy (skill 0), then I will check if I can lob a grenade into his sorry ass, and if I can successfully lob one, I will check for it again in 6 seconds, call my shoot animation, and do nothing after this."
Step 6.
Save and close ai.qc, and now open ogre.qc. After OgreFireGrenade(), I want you to paste this:
Code: Select all
void() OgreFireGrenade2 =
{
local entity missile;
self.effects = self.effects | EF_MUZZLEFLASH;
sound (self, CHAN_WEAPON, "weapons/grenade.wav", 1, ATTN_NORM);
missile = spawn ();
missile.owner = self;
missile.movetype = MOVETYPE_BOUNCE;
missile.solid = SOLID_BBOX;
// set missile speed
makevectors (self.angles);
missile.velocity = self.grenade_velocity;
missile.avelocity = '300 300 300';
missile.angles = vectoangles(missile.velocity);
missile.touch = GrenadeTouch;
// set missile duration
missile.nextthink = time + 2.5;
missile.think = GrenadeExplode;
setmodel (missile, "progs/grenade.mdl");
setsize (missile, '0 0 0', '0 0 0');
setorigin (missile, self.origin);
};
Code: Select all
missile.velocity = self.grenade_velocity;
Step 7.
Scroll down a little more, and you'll find this:
Code: Select all
void() ogre_nail1 =[ $shoot1, ogre_nail2 ] {ai_face();};
void() ogre_nail2 =[ $shoot2, ogre_nail3 ] {ai_face();};
void() ogre_nail3 =[ $shoot2, ogre_nail4 ] {ai_face();};
void() ogre_nail4 =[ $shoot3, ogre_nail5 ] {ai_face();OgreFireGrenade();};
void() ogre_nail5 =[ $shoot4, ogre_nail6 ] {ai_face();};
void() ogre_nail6 =[ $shoot5, ogre_nail7 ] {ai_face();};
void() ogre_nail7 =[ $shoot6, ogre_run1 ] {ai_face();};
Code: Select all
void() ai_facetoss =
{
self.ideal_yaw = vectoyaw(self.origin + self.grenade_velocity * 64);
ChangeYaw ();
};
void() ogre_gren1 =[ $shoot1, ogre_gren2 ] {ai_facetoss();};
void() ogre_gren2 =[ $shoot2, ogre_gren3 ] {ai_facetoss();};
void() ogre_gren3 =[ $shoot2, ogre_gren4 ] {ai_facetoss();};
void() ogre_gren4 =[ $shoot3, ogre_gren5 ] {ai_facetoss();OgreFireGrenade2();};
void() ogre_gren5 =[ $shoot4, ogre_gren6 ] {ai_facetoss();};
void() ogre_gren6 =[ $shoot5, ogre_gren7 ] {ai_facetoss();};
void() ogre_gren7 =[ $shoot6, ogre_run1 ] {ai_facetoss();};
vectoyaw() converts a vector into yaw angles only, that means angles_y. And we detect the direction the grenade will go by adding self.grenade_velocity to self.origin, and we multiply by 64 just in case it gets too low.
ChangeYaw() is a built-in function, which turns a monster towards self.ideal_yaw limited by self.yaw_speed.
Later we created ogre_gren1(), followed by the next animation frames. We call ai_facetoss() on all of them, and OgreFireGrenade2() (our new grenade launch function), on the fourth frame, which is exactly when he is shooting a grenade. Each animation frames lasts for a tenth of a second (0.1), unless if you set a nextthink higher or lower than 0.1 on each animation function. So, this animation lasts for 0.7 seconds, as it has 7 frames (shoot2 is repeated here, it actually has 6).
After that, you can save, compile, and run.