TUTORIAL 35 - Homing Rockets
by Chris Hilton
Continuing on the weapon power-up theme, and taking another look at
the MaxCarnage mini-mod, we're
looking at rockets that home on their target.
Rockets are already a powerful weapon... the splash damage gives the
target a "kick" that makes it easier to place the second rocket, getting
the frag. This is balanced in the tutorial by slowing the rockets down
and giving them a tunnel vision view of their target.
There are also some important issues for playing this modification
over a network versus a 56K modem. Quake 3 uses a "fire and
forget" system that allows the client to predict where an object
will be if its path isn't interrupted. This smooths gameplay and greatly
reduces the amount of data sent across a network connection, and is
equivalent to long think times for entities.
Implementing homing missiles will hammer this: a short think time means the
rocket entity will frequently update its direction vector while tracking. 56K
modem users will be greatly disadvantaged by this, so think carefully about
whether you want to implement this in a mod for general release.
Chris Hilton wrote the code and donated the mod source to Code3Arena,
and HypoThermia wrote these words. I've also added a
small bug fix to the code that isn't in the source released, you're
unlikely to notice it in play though.
1. About homing rockets
The concept of a homing rocket is a straight forward one, but I'll
cover the details here so you can see why Chris has written the code the
way he has. The most important thing is restricting the visibility of
targets to the rocket. This is done by only using targets within a
certain range of the rocket, and within a given cone of view based on
the current direction of travel.
The rockets also aim at the midbody, rather than the feet or one corner of
the model. A bit of common sense really, but attention to detail like this makes
for a better mod.
2. Implementing the code
All the modifications take place in g_missile.c, we're just adding
a new think function for the rocket, and updating the rocket creation code so
it uses the new think function.
First off we've got the new "e;think" function. It needs a little bit
of explanation, and should be placed before the fire_rocket() function.
/*
================
CCH: rocket_think
Fly like an eagle...
--"Fly Like an Eagle", Steve Miller Band
================
*/
#define ROCKET_SPEED 600
void rocket_think( gentity_t *ent ) {
gentity_t *target, *tent;
float targetlength, tentlength;
int i;
vec3_t tentdir, targetdir, forward, midbody;
trace_t tr;
target = NULL;
targetlength = LIGHTNING_RANGE;
// Best way to get forward vector for this rocket?
VectorCopy(ent->s.pos.trDelta, forward);
VectorNormalize(forward);
for (i = 0; i < level.maxclients; i++) {
// Here we use tent to point to potential targets
tent = &g_entities[i];
if (!tent->inuse) continue;
if (tent == ent->parent) continue;
if ( OnSameTeam( tent, ent->parent ) ) continue;
// Aim for the body, not the feet
midbody[0] = tent->r.currentOrigin[0] +
(tent->r.mins[0] + tent->r.maxs[0]) * 0.5;
midbody[1] = tent->r.currentOrigin[1] +
(tent->r.mins[1] + tent->r.maxs[1]) * 0.5;
midbody[2] = tent->r.currentOrigin[2] +
(tent->r.mins[2] + tent->r.maxs[2]) * 0.5;
VectorSubtract(midbody, ent->r.currentOrigin, tentdir);
tentlength = VectorLength(tentdir);
if ( tentlength > targetlength ) continue;
// Quick normalization of tentdir since
// we already have the length
tentdir[0] /= tentlength;
tentdir[1] /= tentlength;
tentdir[2] /= tentlength;
if ( DotProduct(forward, tentdir) < 0.9 ) continue;
trap_Trace( &tr, ent->r.currentOrigin, NULL, NULL,
tent->r.currentOrigin, ENTITYNUM_NONE, MASK_SHOT );
if ( tent != &g_entities[tr.entityNum] ) continue;
target = tent;
targetlength = tentlength;
VectorCopy(tentdir, targetdir);
}
ent->nextthink += 50;
if (!target) return;
VectorMA(forward, 0.05, targetdir, targetdir);
VectorNormalize(targetdir);
VectorScale(targetdir, ROCKET_SPEED, ent->s.pos.trDelta);
}
Stepping through the code, we first set in targetlength how far ahead of
the rocket we'll look for targets, and also grab the direction the rocket is currently
flying in.
Then, looping through all the player entities on the map, we apply a several
conditions for rejecting the target. These conditions are:
- That the entity is on the map (tent->inuse),
- not the person firing the rocket (tent == ent->parent),
- not on the same team as the person firing the rocket,
- not too far away (tentlength > targetlength),
- inside the visible cone (DotProduct(forward, tentdir) < 0.9), and
- not obscured by part of the map (trap_Trace()).
The small bug-fix is in the way tentlength is used to
normalize the value of tentdir[], and allows the code to find the
closest target player properly.
The visible cone test uses a little bit of vector math to obtain the
cosine of the angle between the two vectors. The value of 0.9 accepts
all vectors that differ by up to 25 degrees. Increasing this value
closer to 1.0 will narrow the cone, reducing it towards zero will open
the cone up.
Care needs to be taken with size of the cone: there's no code to
remove the rocket when its been on the map for a while, it needs to hit
something to be removed. You need to use sensible values that allow the
rocket to "miss" and so be removed from the map.
Finally, when the closest target has been found, the direction to the
target is mixed with the current direction using VectorMA() so
the rocket starts steering towards its victim. Decreasing the value of
0.05 will give the rocket a larger turning circle and make it less
responsive. Increase the value too much and the player won't have a
chance to get out the way!
All thats left is to link this new think function into the rocket and
set the new slower speed at the time of launch. These last two changes go into
fire_rocket().
bolt = G_Spawn();
bolt->classname = "rocket";
bolt->nextthink = level.time + 1; // CCH
bolt->think = rocket_think; // CCH
bolt->s.eType = ET_MISSILE;
bolt->r.svFlags = SVF_USE_CURRENT_ORIGIN;
bolt->s.pos.trType = TR_LINEAR;
bolt->s.pos.trTime = level.time - MISSILE_PRESTEP_TIME;
VectorCopy( start, bolt->s.pos.trBase );
VectorScale( dir, ROCKET_SPEED, bolt->s.pos.trDelta ); // CCH
SnapVector( bolt->s.pos.trDelta );
VectorCopy (start, bolt->r.currentOrigin);
3. Ideas for change
Just to round off the tutorial, I thought I'd give a few ideas on the changes you could
make to the this code.
The most obvious is making sure that the missile doesn't stay on the
map for too long: eventually calling G_ExplodeMissile() to clean
up.
Giving the player a homing missile detector that beeps when a lock on
is gained would balance things up a bit. The more advanced might even
want to add this as a powerup like the medikit or teleporter.
|