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()

+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,
are ©opyrighted to the Quake DeveLS team. All rights received.
Got a suggestion? Comment? Question? Hate mail? Send it to us!
Oh yeah, this site is best viewed in 16 Bit or higher, with the resolution on 800*600.
Thanks to Planet Quake for there great help and support with hosting.
Best viewed with Netscape 4 or IE 3