TUTORIAL 18 - Vortex Grenades
II by TeknoDragon
This is a follow-up to AssKicka's Vortex
Grenades Tutorial, that beautifies the grenade's effects and may
speed up the algorithm in high load situations with a lot of players
tossing grenades. This tutorial assumes that you have just stepped
out of the Vortex
Grenades Tutorial, so if you haven't done that get over there!
I'll also go over a few things that make good development
practice, they are a composite of personal experience and hours in a
few Comp. Sci. classes at WSU.
1. Simple TweaksNow that
you have vortex grenades there are a few issues to consider if you
want to optimize this mod. First you can change the nextthink
of the grenades so that they aren't calling G_Suck 50 times a
second, instead maybe 10 or 20.
Go to the G_Suck function in g_missile.c and scroll
down to this line: self->nextthink = level.time + 20;
You can change the 20 to something larger (say 100 --
that's 10 times per second) to make G_Suck suck up less
process. This doesn't change the rate that players are sucked
towards the grenade because G_Suck doesn't add the grenade's
velocity to yours, it replaces it.
Looking at how vortex grenades move the player (through modifying
target->client->ps.velocity), you can find what else
modifies the player motion and what it does to beautify the
movement. If you use MS Visual Studio to search though the source
files you'll find client->ps.velocity in several
functions, including G_Damage in g_combat.c seen here:
VectorAdd (targ->client->ps.velocity, kvel, targ->client->ps.velocity);
// set the timer so that the other client can't cancel
// out the movement immediately
if ( !targ->client->ps.pm_time ) {
int t;
t = knockback * 2;
if ( t < 50 ) {
t = 50;
}
if ( t > 200 ) {
t = 200;
}
targ->client->ps.pm_time = t;
targ->client->ps.pm_flags |= PMF_TIME_KNOCKBACK;
}
You'll notice that immediately after the kick velocity
is added to the client a special timer is set so that the movement
isn't immediately negated. We'll add something like this into our
G_Suck function to beautify the effect that 2 grenades have
on each other. In the original Vortex Grenades tutorial says that
VectorScale adds the scaled vector to the result, but
if you look at the code of _VectorScale (the unoptomized
version of the VectorScale #define macro -- both in
g_math.c) you'll see that it doesn't! We can change this so
that it does add the velocity, but in affect that means that we're
accelerating towards the grenade! This feels more natural to
a player since acceleration is a natural affect of forces and it
will jerk them around less.
First we need to add a new variable: vec3_t kvel, since
dir is reused at the end of the function we shouldn't mess with it.
We should also make a few #defines so that we can change the
G_Suck effects more easily.
At the beginning of G_Suck change this: vec3_t start,dir,end,kvel;
Then add this before G_Suck: #define GSUCK_TIMING 50 // the think time interval of G_Suck
#define GSUCK_VELOCITY 200 // the amount of kick each second gets
Now you can change GSUCK_TIMING to affect the
times per second that G_Suck pulls a player back. Finally to
correct this little bug and add in the kick timer let's change the
G_Suck code: // scale directional vector by the kick factor and add to the targets velocity
VectorScale(dir,GSUCK_VELOCITY / GSUCK_TIMING, kvel);
// add the kick velocity to the player's velocity
VectorAdd (target->client->ps.velocity,kvel, target->client->ps.velocity);
// set the timer so that the other client can't cancel
// out the movement immediately
if ( !target->client->ps.pm_time ) {
targ->client->ps.pm_time = GSUCK_TIMING - 1;
/* the next G_Suck that works here will
probably be the one that worked before */
targ->client->ps.pm_flags |= PMF_TIME_KNOCKBACK;
}
Now compile and play around with it a bit, you'll
notice a few changes.
2. Fine Tuning ItThe grenades might not seem so
effective, so turn up the GSUCK_VELOCITY. I found that
2000 is just enough so that if there's only one grenade there
you can still get away, but 2 will almost certainly kill you
(remember they really add forces now).
The GSUCK_TIMING value is pretty important to balancing
how good it looks vs. how much process it uses. If
GSUCK_VELOCITY is very large and GSUCK_TIMING is very
small players will be tossed back and forth across the grenade, so
be careful with this extreme.
Next, you might even up the stakes and comment out these lines:
// if (target == self->parent)
// continue; Now you are
affected too and can better tune the grenade's strength!
We see the VectorCopy call at the end of the function, but
we don't see it in the G_Damage function, that's strange. I
suppose you could comment out this last thing and then remove
kvel from the beginning saving you about 12 bytes on
G_Suck's stack, but that's not too bad in the great big
scheme of things (and no, it doesn't unequivocally add up to be a
per grenade penalty... only per G_Suck simultaneous calls,
which don't happen that much unless you have a lot of
grenades out there).
3. The Big FixThere is one big optimization that can
be done. If you poke around in many of the core functions of the
code you'll see stuff from syscalls.c a lot, particularly
trap_Trace. This function serves to calculate impacts. There
is another function that can serve us well:
trap_EntitiesInBox. This function takes two vectors that
define a bounding box and returns a list of entity numbers that are
inside this region! We'll take the function G_KillBox as our
example. void G_KillBox (gentity_t *ent) {
int i, num;
int touch[MAX_GENTITIES];
gentity_t *hit;
vec3_t mins, maxs;
VectorAdd( ent->client->ps.origin, ent->r.mins, mins );
VectorAdd( ent->client->ps.origin, ent->r.maxs, maxs );
num = trap_EntitiesInBox( mins, maxs, touch, MAX_GENTITIES );
for (i=0 ; iclient ) {
continue;
}
// nail it
G_Damage ( hit, ent, ent, NULL, NULL,
100000, DAMAGE_NO_PROTECTION, MOD_TELEFRAG);
}
}
100k damage, Ouch! You see the exact behavior or
trap_EntitiesInBox here, so although there is little to no
documentation on the syscalls you can still use them just like id
does!
However we've got a circle, not a box! So we fudge the radius a
bit and then double check to make sure everyone's inside. First we
need some kind of a list like touch[MAX_GENTITIES] and a
number. Add this line at the beginning of G_Suck: vec3_t start,dir,end,kvel,mins,maxs;
int targNum[MAX_GENTITIES],num;
Next add this define near the others: #define GSUCK_RADIUS 500
Now take out this line: while ((target = findradius(target, self->r.currentOrigin, 500)) != NULL) {
In it's place, put this: mins[0] = -GSUCK_RADIUS * 1.42;
mins[1] = -GSUCK_RADIUS * 1.42;
mins[2] = -GSUCK_RADIUS * 1.42;
maxs[0] = GSUCK_RADIUS * 1.42;
maxs[1] = GSUCK_RADIUS * 1.42;
maxs[2] = GSUCK_RADIUS * 1.42;
VectorAdd( self->r.currentOrigin, mins, mins );
VectorAdd( self->r.currentOrigin, maxs, maxs );
num = trap_EntitiesInBox(mins,maxs,targNum,MAX_GENTITIES);
for(num--; num > 0; num--) { // count from num-1 down to 0
target = &g_entities[targNum[num]];
Finally add these 3 lines: // target must be able to take damage
if (!target->takedamage)
continue;
// target must actually be in GSUCK_RADIUS
if ( Distance(self->r.currentOrigin,targ->r.currentOrigin) > GSUCK_RADIUS )
continue;
We multiply GSUCK_RADIUS by 1.42 because
it describes the far corner, not just the closest edge of the
square. Since distance is = sqrt( height^2 + width^2 ) the
distance between a unit square's corners as well as the ratio of its
width to its diagonal distance is sqrt( 2 ) or about
1.42!
Compile away! If you have less than the minimal requirements for
Quake 3 then you will probably see some performance increase when
there are a lot of people and a lot of vortex grenades.
4. A Nasty Surprize!
One last thing that we can do is do another
trap_EntitiesInBox to see if someone is right over the top of
our grenade and then explode it on them! We'll reuse targNum
and make another define: #define GSUCK_TRIGGER 32
At the very end of the function add these lines: mins[0] = -GSUCK_TRIGGER * 1.42;
mins[1] = -GSUCK_TRIGGER * 1.42;
mins[2] = -GSUCK_TRIGGER * 1.42;
maxs[0] = GSUCK_TRIGGER * 1.42;
maxs[1] = GSUCK_TRIGGER * 1.42;
maxs[2] = GSUCK_TRIGGER * 1.42;
VectorAdd( self->r.currentOrigin, mins, mins );
VectorAdd( self->r.currentOrigin, maxs, maxs );
num = trap_EntitiesInBox(mins,maxs,targNum,MAX_GENTITIES);
for(num--; num > 0; num--) { // count from num-1 down to 0
target = &g_entities[targNum[num]];
// target must be a client
if (!target->client)
continue;
// target must not be the player who fired the vortex grenade
if (target == self->parent) // makes sense here
continue;
// target must be able to take damage
if (!target->takedamage)
continue;
G_ExplodeMissile( self) // EXPLODE goes the weasel!
}
I don't check the radius again here since it's so
small, but you certainly can! However, it involves some heavy
multiplication that's not necessarily optimized.
Now that we've done the same thing twice you could take the code
that seems repeated and put it in another function. This will reduce
average memory usage since you can reduce the time large variables
like targNum are in memory. I'll leave the implementation up to you.
One final thing to consider: MAX_GENTITIES is
1<<10, i.e. 1024, that's 4k on the stack
each time though the function. I changed mine to 64 instead,
which is only 256. The tradeoff here is that if you get more
than 64 entities (including rockets, players, plasma, and
just about everything that moves) within GSUCK_RADIUS some of
them will be tagged for suckage and the younger entities probably
won't AFAIK (entity management is a bit of a mystery to me yet).
5. Testing and Balancing
The absolutely best way to test and balance your mod is to get
together with some Quake 3 fans on a LAN and get everyone's opinions
on your work. These opinions are in fact more valuable than your own
in the development process, because they hopefully represent the
kinds of people that will spend time playing your mod.
You can go back and change the four #defines:
GSUCK_TIMING, GSUCK_VELOCITY, GSUCK_RADIUS, and
GSUCK_TRIGGER to tune up your mod in-between games when
you're testing it. You'll also want to add another #define to
easily change grenade damage. Add this line with your other
#defines: #define GRENADE_DAMAGE 100 // bolt->damage for grenade
#define GRENADE_RADIUS 150 // bolt->splashRadius for grenade
Next be sure to replace the appropriate lines in
fire_grenade: bolt->damage = GRENADE_DAMAGE;
bolt->splashDamage = GRENADE_DAMAGE;
bolt->splashRadius = GRENADE_RADIUS;
This way you can have the file open in MS Dev Studio and tweak a
few values and compile. Most modern compilers including MS Dev
Studio will only recompile the changed files. Next copy the
dll's or qvm's over your lan and start the next game
in a few seconds. |