Modifying Quake 3 - part 3 - Missile behavior
< Continued from Part 2: Gamer inputs
Changing the behavior of the new missile
Now once we are done with accepting input, we need to change the behavior of the missile based on this value. As discussed before, to achieve this we will have create a new think function that will have the necessary code to exhibit different behavior.
In the fire_rocket
method in g_missile.c
under the game project you will find lines stating:
bolt->nextthink = level.time + 15000;
bolt->think = G_ExplodeMissile; //Game time here is in milliseconds.
G_ExplodeMissile
is a think function for the rocket. This means that after 15 seconds
the rocket will explode. 15 seconds
is the next think
time here.
We will point it to a different think function missile_think
. Also a homing missile needs to move a bit slower initially. This is shown below:
gentity_t *fire_rocket (gentity_t *self, vec3_t start, vec3_t dir) {
gentity_t *bolt;
............
bolt->classname = "rocket";
//0 -> off
//1 -> Constant speed
//2 -> Variable speed
//3 -> Fireworks with varible speed
if(self->client->pers.homingMissle == 1)
{
bolt->think = missile_think;
bolt->nextthink = level.time + 1;
}
else if(self->client->pers.homingMissle == 2)
{
bolt->think = missile_think_variable_speed;
bolt->nextthink = level.time + 1;
}
else if(self->client->pers.homingMissle == 3
|| self->client->pers.homingMissle == 4
|| self->client->pers.homingMissle == 5)
{
bolt->think = missile_think_variable_speed_fireworks;
bolt->nextthink = level.time + 1;
}
else
{
bolt->nextthink = level.time + 15000;
bolt->think = G_ExplodeMissile;
}
............
............
bolt->s.pos.trType = TR_LINEAR;
// move a bit on the very first frame
bolt->s.pos.trTime = level.time - MISSILE_PRESTEP_TIME;
VectorCopy( start, bolt->s.pos.trBase );
if (self->client->pers.homingMissle == 1)
{
// Initial speed of homing missile has to be less
VectorScale( dir, 500, bolt->s.pos.trDelta );
}
else
{
VectorScale( dir, 900, bolt->s.pos.trDelta );
}
// save net bandwidth
SnapVector( bolt->s.pos.trDelta );
VectorCopy (start, bolt->r.currentOrigin);
return bolt;
}
The speed is initially set to 500
. In this code, the speed
increases once target is found. You may try tweaking it.
Now that we have pointed to another think function, we must define it. Before defining the function, let me explain the behavior that the think function will exhibit in the game environment.
The homing missile will select a nearest target, steer towards it and blast into it.
When the missile is fired and travelling straight, it should scan all the entities in its visibile distance and visual cone. The visual cone limits the visibility of missile and allows it to become more realistic. Otherwise it could just take a 180 degree turn and hit some target behind.
It should only select other player entities and should not select the firing-player and his team members (in case of team match).
The target should be directly visible. Since this is a virtual world, the visual cone math does not solve this problem. We need to address it bit differently.
The code for this think function is also placed in g_missile.c
just under the line #define MISSILE_PRESTEP_TIME 50
. It is shown below.
#define MISSILE_PRESTEP_TIME 50
gentity_t *findNearestTargetInRadius (gentity_t *ent, float rad, vec3_t *pTargetDirectionVector,
vec3_t missileNormalizedForwardDirection, double fVisionCone)
{
gentity_t *pEntity = NULL;
gentity_t *pTarget = NULL;
vec3_t pEntityMidBodyVector;
vec_t fEntityMidBodyVectorLengh=0;
vec_t fTargetDistance=0;
vec3_t temp_vector;
trace_t trace;
// Check for all the entities and find atleast one in radius
for (pEntity = g_entities; pEntity < &g_entities[level.num_entities]; pEntity++) {
if (!pEntity->inuse) // If not on map, continue
continue;
// Hit the player in the body and not in feet
// Add the mins and maxs. Then divide by two.
// Add the current origin to it.
// That would yeild the middle of the body (bounding box atleast)
VectorAdd(pEntity->r.mins,pEntity->r.maxs,pEntityMidBodyVector);
VectorScale(pEntityMidBodyVector,0.5,pEntityMidBodyVector);
VectorAdd(pEntity->r.currentOrigin,pEntityMidBodyVector,pEntityMidBodyVector);
// Now subtract to get a proper direction.
VectorSubtract(pEntityMidBodyVector,ent->r.currentOrigin,pEntityMidBodyVector);
// We calculated the distance vector above. So this is the distance to entity.
fEntityMidBodyVectorLengh = VectorLength(pEntityMidBodyVector);
// If the distance is greater than the given radius, leave it.
if (fEntityMidBodyVectorLengh > rad)
continue;
// Check the entity for different conditions.
if(pEntity!=NULL)
{
if (!pEntity->client)
continue;
if (pEntity == ent->parent)
continue;
if (pEntity->health <= 0) continue;
if (pEntity->client->sess.sessionTeam == TEAM_SPECTATOR)
continue;
if ( OnSameTeam( pEntity, ent->parent ) )
continue;
// Normalize the direction vector
VectorCopy(pEntityMidBodyVector,temp_vector);
VectorNormalize(temp_vector);
// Lesser the value of vision cone, wider it will be.
// Value 1 will narrow down the cone.
if ( fVisionCone > 0.01
&&
DotProduct(missileNormalizedForwardDirection, temp_vector) < fVisionCone ) continue; // Check if visible
trap_Trace (&trace, ent->s.pos.trBase, NULL, NULL,
pEntity->s.pos.trBase, ent->s.number, MASK_SHOT );
if ( trace.contents & CONTENTS_SOLID ) // If not visible, then continue
continue;
// Passed all the tests, it is a possible target.
// Check if there is closer target.
// Added 100 so that missile does not change targets
// frequently in case of players close to each other
if ((pTarget == NULL) || (fEntityMidBodyVectorLengh < fTargetDistance+100))
{
pTarget=pEntity;
// Copy the direction vector for the think function.
// We dont need to recalculate it there.
VectorCopy(pEntityMidBodyVector, (*pTargetDirectionVector));
fTargetDistance = fEntityMidBodyVectorLengh;
}
}
}
return pTarget;
}
void missile_think( gentity_t *ent ) {
gentity_t *pTarget = NULL;
vec3_t targetdir,forward;
int speed=900;
VectorCopy(ent->s.pos.trDelta, forward);
VectorNormalize(forward);
pTarget = findNearestTargetInRadius(ent,5000,&targetdir, forward,0.7);
if(pTarget!=NULL)
{
// Steer the missile to the target.
// Lower the value in place of 0.05, larger the turning circle.
VectorMA(forward, 0.05, targetdir, targetdir);
VectorNormalize(targetdir);
VectorScale(targetdir, speed, ent->s.pos.trDelta);
}
// These two lines are used to smoothen the missile trajectory.
// Removing these will exhibit jerky movement.
VectorCopy( ent->r.currentOrigin ,ent->s.pos.trBase );
ent->s.pos.trTime = level.time;
ent->nextthink = level.time + 1;
}
Different modes of homing missiles
The comments placed in the code above are self-explanatory. In the attached code, you will find multiple missile_think
custom functions and in total five different homing modes. Here’s a brief on those:
Homing Missiles with constant speed:
These are the ones shown above. Once launched the missile steers towards the target at same speed.
Homing Missiles with variable speed:
I like these the most and these are designed for higher precision to hit the target. If the target is far enough, the missile will increase its speed to come close to it. Once it is quite closer, it takes a relative measurement of the angle between itself and target. If angle is too less, missile will again increase its speed and hit the target before it moves out of sight. It is capable of shooting a target in mid-air almost every time.
Homing Missiles with variable speed and fireworks:
Well this idea came from the epic and one time everybody’s favorite tele-serial Ramayan, where, in the wars one arrow fired would spawn multiple arrows. Similarly this function facilitates spawning of non-homing rockets when the target is closer. Absolutely no way to escape. Not much interesting to play with. But you’ll get to see some great fireworks.
Homing Missiles with variable speed, fireworks and wide angle:
The visual cone is much spread out. Rest is same as above
Homing Missiles with variable speed, fireworks and all view:
Same as above. The visual cone is completely open. Expect the missile to take hair-pin turns when using this.
Conclusions
I have set some values in code for the homing missile, such that it works with high precision and the target cannot escape. In game play, that will be a really tough scenario and will not be interesting. I leave it upto you to change the code so that the fun part in gaming is not lost.
Learning Quake 3 can be real fun. Some day it can be used to teach you game development, simulation, 3D geometry and vector mathematics. And besides playing games, its engine can be quite useful for creating 3D UI applications, researching on building architectures,etc.
Source code
- Id Software Download Quake3 base code from this website.
- Source files, with changes only
- Compiled Mods