TUTORIAL 34 - Cluster grenades
by Chris Hilton
Powering up the weapons that already exist is an excellent way to introduce yourself
to the code. Game balance is always an issue here, but you can have a lot of fun with this
in a LAN party as you frag your friends in devious and gratuitous ways.
The grenade launcher is a difficult weapon to use well. The ordnance is unpowered and
falls under gravity, and it bounces too. This tute powers up the grenade launcher to create a
lethal area to try and run or jump over. Especially deadly in corridors!
This tutorial updates the cluster grenade work by SumFuka at QDevels for Q2, bringing it
in line with the new Q3 code. Chris Hilton wrote the code and donated the mod source to Code3Arena,
and HypoThermia
wrote these words.
The coding modifications are all in g_missile.c, with one addition to g_local.h.
1. The cluster grenade design
When we create entities that exist on the map for a short duration,
such as fired weapon ordnance, there's a framework already in place for
us to use. Such items are called "Bolts" (the name given to crossbow
ammunition). A lot of work has already been done for us to make sure
they move through the map smoothly, either under their own power, or on
a gravitational trajectory.
The bolt system can be easily extended to create new types, or separate out
similar types of ammo (as we're doing here). A cluster grenade looks like any
other grenade, but has the effect of breaking up on impact or after a time delay,
so we need two types of ammo. Fortunately the bolt system allows us to add
new types with ease.
2. The code
We'll start by creating our new type of bolt ammunition, by modifying the ammo fired
by the grenade launcher.
We make this change to fire_grenade() in g_missile.c:
bolt = G_Spawn();
bolt->classname = "cgrenade"; // CCH
bolt->nextthink = level.time + 2500;
By renaming our bolt we've created a new type, thats the minimum you need to do! The bolt
is a gentity_t which is an entity created with G_Spawn(). This data
structure is only used in this form within the server code, so it can get away with using
a pointer to the string name.
Setting the item type to ET_MISSILE ensures that the G_MissileImpact()
function is called. Inside G_MissileImpact() we have to create the
cluster of grenades when something is hit. We need a vector for the direction that
each grenade will go in:
gentity_t *other;
qboolean hitClient = qfalse;
vec3_t dir; // CCH
Then the code that recognizes the type of bolt and creates the grenades. This uses
a function fire_grenade2() we'll provide in a moment:
// splash damage (doesn't apply to person directly hit)
if ( ent->splashDamage ) {
if( G_RadiusDamage( trace->endpos, ent->parent,
ent->splashDamage, ent->splashRadius,
other, ent->splashMethodOfDeath ) ) {
if( !hitClient ) {
g_entities[ent->r.ownerNum].client->
ps.persistant[PERS_ACCURACY_HITS]++;
}
}
}
// CCH: For cluster grenades
if (!strcmp(ent->classname, "cgrenade")) {
VectorSet(dir, 20, 20, 40);
fire_grenade2(ent->parent, ent->r.currentOrigin, dir);
VectorSet(dir, -20, 20, 40);
fire_grenade2(ent->parent, ent->r.currentOrigin, dir);
VectorSet(dir, 20, -20, 40);
fire_grenade2(ent->parent, ent->r.currentOrigin, dir);
VectorSet(dir, -20, -20, 40);
fire_grenade2(ent->parent, ent->r.currentOrigin, dir);
}
trap_LinkEntity( ent );
The vector dir contains the velocity in which each fragment will move: four diagonal
directions and a small kick vertically so they bounce nicely.
Now we need to provide the functionality within the "think" function, called when
the game time reaches the nextthink time. The grenade has "timed out" and broken
into equally lethal fragments.
Adding to G_ExplodeMissile() the same code as we put into G_MissileImpact()
gives the required effect. If you plan to develop or extend cluster grenades then it would
be wise to merge this identical code into a single function. This would give the same effect
with either event, without having to copy/paste code back and forth.
// splash damage
if ( ent->splashDamage ) {
if( G_RadiusDamage( ent->r.currentOrigin, ent->parent,
ent->splashDamage, ent->splashRadius, NULL
, ent->splashMethodOfDeath ) ) {
g_entities[ent->r.ownerNum].client->
ps.persistant[PERS_ACCURACY_HITS]++;
}
}
// CCH: For cluster grenades
if (!strcmp(ent->classname, "cgrenade")) {
VectorSet(dir, 20, 20, 40);
fire_grenade2(ent->parent, ent->r.currentOrigin, dir);
VectorSet(dir, -20, 20, 40);
fire_grenade2(ent->parent, ent->r.currentOrigin, dir);
VectorSet(dir, 20, -20, 40);
fire_grenade2(ent->parent, ent->r.currentOrigin, dir);
VectorSet(dir, -20, -20, 40);
fire_grenade2(ent->parent, ent->r.currentOrigin, dir);
}
trap_LinkEntity( ent );
Just the creation of each grenade in the cluster needed now. This is done using the
new function fire_grenade2(), which is best placed just before fire_grenade().
/*
=================
CCH: fire_grenade2
38: 62. They will also say, `Our Lord, whosoever prepared this for us,
do thou multiply manifold his punishment in the Fire.'
--Holy Quran, translated by Maulvi Sher Ali
=================
*/
gentity_t *fire_grenade2 (gentity_t *self, vec3_t start, vec3_t dir) {
gentity_t *bolt;
VectorNormalize (dir);
bolt = G_Spawn();
bolt->classname = "grenade";
bolt->nextthink = level.time + 2500;
bolt->think = G_ExplodeMissile;
bolt->s.eType = ET_MISSILE;
bolt->r.svFlags = SVF_USE_CURRENT_ORIGIN;
bolt->s.weapon = WP_GRENADE_LAUNCHER;
bolt->s.eFlags = EF_BOUNCE_HALF;
bolt->r.ownerNum = self->s.number;
bolt->parent = self;
bolt->damage = 100;
bolt->splashDamage = 100;
bolt->splashRadius = 150;
bolt->methodOfDeath = MOD_GRENADE;
bolt->splashMethodOfDeath = MOD_GRENADE_SPLASH;
bolt->clipmask = MASK_SHOT;
bolt->s.pos.trType = TR_GRAVITY;
// move a bit on the very first frame
bolt->s.pos.trTime = level.time - MISSILE_PRESTEP_TIME;
VectorCopy( start, bolt->s.pos.trBase );
VectorScale( dir, 700, bolt->s.pos.trDelta );
// save net bandwidth
SnapVector( bolt->s.pos.trDelta );
VectorCopy (start, bolt->r.currentOrigin);
return bolt;
}
This is almost an exact copy/paste of the original fire_grenade(), but restoring the
original classname for the grenade. If you put "cgrenade" in here then you'd be creating new
clusters of grenades every time, crashing the game before filling the level!
You can tweak these values for slightly different behaviour: the amount of damage and
splash damage for example. The 700 in the VectorScale() determines the speed of the
fragments (and hence how far they can travel). Other ideas include changing the number of fragments,
or a more random disrtibution to the fragments.
The last thing we need to do is declare the function in g_local.h at about line 454:
//
// g_missile.c
//
void G_RunMissile( gentity_t *ent );
gentity_t *fire_blaster (gentity_t *self, vec3_t start, vec3_t aimdir);
gentity_t *fire_plasma (gentity_t *self, vec3_t start, vec3_t aimdir);
gentity_t *fire_grenade (gentity_t *self, vec3_t start, vec3_t aimdir);
gentity_t *fire_grenade2 (gentity_t *self, vec3_t start, vec3_t aimdir); // CCH
3. All done!
Now go off and enjoy this souped up weapon! Particularly effective and useful in a CTF
game!
|