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
Tweaks
Now 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 It
The 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 Fix
There 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.
|