
Quake DeveLS - Homing missile
Author: Chris Hilton
Difficulty: Medium
Let's modify our rockets
to become homing missiles. To pay for this behavior, we're going to take an
additional 5 cells from the player. Also, we'd like for the player to be able
to turn the homing missile behavior on and off.
I'll start with being able
to turn the missile behavior on and off. Each player should be able to toggle
this behavior independently and the state should be maintained across levels.
This means we'll add a homing_state variable to the 'client persistant data',
which every player has and is maintained across level changes.
client_persistant_t is defined at line 719 of g_local.h, and I've added
a homing_state variable below. +'s indicate new lines added, -'s indicate
lines removed.
int max_slugs; gitem_t *weapon;++ // CCH: new persistant data+ qboolean homing_state; // are homing missiles activated+ } client_persistant_t;
Now that
we've defined the variable, we should initialize it like a good programmer.
InitClientPersistant() is defined at line 236 of p_client.c, to
which we add:
client->pers.max_grenades = 50; client->pers.max_cells = 200; client->pers.max_slugs = 50;++ // CCH: initialize homing_state to off+ client->pers.homing_state = 0; }
Okay, now
let's provide the player a way to toggle this variable on and off. Console
command functions are located in g_cmds.c, to which we add our new
function for toggling homing_state, as below. Place this function after Cmd_wave_f(),
and before the ClientCommand()
+/*+=================+Cmd_Homing_f+CCH: whole new function for adjusting homing missile state+=================+*/+void Cmd_Homing_f (edict_t *ent)+{+ int i;++ i = atoi (gi.argv(1));++ switch (i)+ {+ case 0:+ gi.cprintf (ent, PRINT_HIGH, "Homing missiles off\n");+ ent->client->pers.homing_state = 0;+ break;+ case 1:+ default:+ gi.cprintf (ent, PRINT_HIGH, "HOMING MISSILES ON\n");+ ent->client->pers.homing_state = 1;+ break;+ }+}
When this
function is called, it gets the first argument to the command by calling
gi.argv(1) and converting it to an integer with atoi(). The homing_state
variable for this player is set appropriately and a message is printed to the
player letting them know the homing missile state. Now, we need to add a way to
call this function by editing the ClientCommand() function, also located
in g_cmds.c, like so:
Cmd_PutAway_f (ent); else if (Q_stricmp (cmd, "wave") == 0) Cmd_Wave_f (ent);++ // CCH: new 'homing' command+ else if (Q_stricmp (cmd, "homing") == 0)+ Cmd_Homing_f (ent);+ else if (Q_stricmp (cmd, "gameversion") == 0) { gi.cprintf (ent, PRINT_HIGH, "%s : %s\n", GAMEVERSION, __DATE__);
At this
point, you should be able to compile your DLL and issue the 'homing' console
command and see the homing state messages. I've read that you should be able to
type the commands 'homing 0' and 'homing 1' directly at the console, but I've
always had to type 'cmd homing 0' and 'cmd homing 1'. Any info on what's up
with this would be appreciated.
Finally, we're ready for
the fun stuff. Here, we'll be mucking with g_weapon.c. First, we'll add
what our homing missile will be 'think'ing every .1 seconds. Place this new
function just before the fire_rocket() function
+// CCH: New think function for homing missiles+void homing_think (edict_t *ent)+{+ edict_t *target = NULL;+ edict_t *blip = NULL;+ vec3_t targetdir, blipdir;+ vec_t speed;++ while ((blip = findradius(blip, ent->s.origin, 1000)) != NULL)+ {+ if (!(blip->svflags & SVF_MONSTER) && !blip->client)+ continue;+ if (blip == ent->owner)+ continue;+ if (!blip->takedamage)+ continue;+ if (blip->health <= 0)+ continue;+ if (!visible(ent, blip))+ continue;+ if (!infront(ent, blip))+ continue;+ VectorSubtract(blip->s.origin, ent->s.origin, blipdir);+ blipdir[2] += 16;+ if ((target == NULL) || (VectorLength(blipdir) < VectorLength(targetdir)))+ {+ target = blip;+ VectorCopy(blipdir, targetdir);+ }+ }+ + if (target != NULL)+ {+ // target acquired, nudge our direction toward it+ VectorNormalize(targetdir);+ VectorScale(targetdir, 0.2, targetdir);+ VectorAdd(targetdir, ent->movedir, targetdir);+ VectorNormalize(targetdir);+ VectorCopy(targetdir, ent->movedir);+ vectoangles(targetdir, ent->s.angles);+ speed = VectorLength(ent->velocity);+ VectorScale(targetdir, speed, ent->velocity);+ }++ ent->nextthink = level.time + .1;+}
Whew.
Let's break that down a bit. The while loop is used to locate a target. We're
using findradius to step through every entity (blip) within 1000 units. Then we
have a number of exclusion factors. For instance, if the blip is not a monster
or player (only players have client defined), we're not interested in it. Also,
we check that the blip is not our owner, can take damage, has health to lose,
is visible, and is in front of us. If all this is true, we calculate blipdir, a
vector that points from the origin of our rocket to the origin of the blip (its
feet). Then we add 16 to the blipdir's Z value so that blipdir points at some
location above the feet. If a target hasn't been set yet, this blip becomes our
target. If a target has already been set, we compare the distances and make
this blip the target only if it is closer.
After the while loop, if
we've found a target, we want to adjust our direction (movedir) toward the
target. We normalize the targetdir, which changes its length to 1 regardless of
its previous length, then scale it down to a length of .2. We then add this
small targetdir correction to our current movement direction (which has a
length of 1) and store it back in targetdir. We want our movedir to always have
a length of 1, so targetdir is normalized again and copied into this rocket's
movedir. We then set the rocket's angles appropriate to the new direction with
vectoangles. Also, we obtain the rocket's speed and adjust the rocket's
velocity to have the same speed, but in the new movement direction.
Finally, we set the rocket
to call its think function again in one tenth of z second.
Now that we have the think
function down, we just have to set up the rocket to use it by modifying the
fire_rocket() function in g_weapon.c like so (-'s indicate original lines that
were removed):
rocket->s.modelindex = gi.modelindex ("models/objects/rocket/tris.md2"); rocket->owner = self; rocket->touch = rocket_touch;- rocket->nextthink = level.time + 8000/speed;- rocket->think = G_FreeEdict;++ // CCH: see if this is a player and if they have homing on+ if (self->client && self->client->pers.homing_state)+ {+ // CCH: if they have 5 cells, start homing, otherwise normal rocket think+ if (self->client->pers.inventory[ITEM_INDEX(FindItem("Cells"))] >= 5)+ {+ self->client->pers.inventory[ITEM_INDEX(FindItem("Cells"))] -= 5;+ rocket->nextthink = level.time + .1;+ rocket->think = homing_think;+ } else {+ gi.cprintf(self, PRINT_HIGH, "No cells for homing missile.\n");+ rocket->nextthink = level.time + 8000/speed;+ rocket->think = G_FreeEdict;+ }+ } else {+ rocket->nextthink = level.time + 8000/speed;+ rocket->think = G_FreeEdict;+ }+ rocket->dmg = damage; rocket->radius_dmg = radius_damage; rocket->dmg_radius = damage_radius;
If they
are a player and have homing_state turned on, we see if they have the required
5 cells. If so, we remove 5 cells from the player's inventory and set the
homing_think function to be called. Otherwise, we let the player know he/she
doesn't have enough cells for a homing missile. When not firing a homing
missile, we fire a regular rocket instead.
That's all there is to it.
A possible exercise for the reader: Modify the blip 'height adjustment' to take
into account the size of the blip.
Hope you've found this
tutorial useful. Full source and patch files available at
http://www.jump.net/~dctank.
Tutorial by Chris Hilton
|
This
site, and all content and graphics displayed on it, |