Author: LM_Jormungard
A Preliminary : Weapon Speedups
You've probably
already read the tutorial on speeding up your hand blaster -- it involved
changing the start and end points of some of the animation sequences. Here's an
alternative solution that keeps the same start and end frames, but skips some of
the frames in the middle. Ent->client->ps.gunframe is the frame number in the gun's
animation sequence that your "view" gun is currently on. The gun in your
view was pre-made with fixed size animation sequences back to back, so it is
difficult to alter the animation rates of the gun without either editting the
model itself to add/subtract frames from the sequence, or altering the logic in
"Weapon_Generic", the central routine that is called every round for your gun to
determine what frame it should be on.
Each action your
gun can take has an animation "sequence" associated with it. When you are
idle, are activating a new weapon, deactivating an old one, or firing,
"Weapon_Generic" decides what frame you are on. The root of the problem
lies in the fact that the animation sequences are assumed to be back to back, so
the end of one sequence acts as the same marker as the beginning of the
next. We could speed up the firing rates of ALL weapons across the board
by defining the end of of the firing sequence to be one frame sooner while using
a second definitition for the beginning of the sequence that comes after the
firing sequence, so we don't alter the look of this adjacent
sequence.
Star Wars Blaster !
Next, I'd like
to show you some code I whipped up to make your hand blaster look like a "Star
Wars" style laser blaster. While the effect the hand blaster shows is
quite impressive, it looks more like a shooting comet than like a blaster to
me. The new weapon uses short "beam" effects, and looks much more like a
laser blaster to me. We then spawn an
entity, just as we had before, to be our moving projectile. In our case,
we have altered the renderfx to be "RF_BEAM". There are a few key things
to keep in mind with this effect. First of all, the s.skinnum will hold
the color of the BEAM. Some example colors are listed in "g_target.c", the
one place RF_BEAM is used in the original Quake 2 code. Second, our
s.frame will hold the diameter of the beam. Both of these things were done
because it can be easier to reuse existing variables in the code than to make
new ones that require more memory. Since RF_BEAM is rare, it made little
sense for the original programmers to make specific "color" and "diameter"
fields for all entities. Rather, the code reused "skinnum" and "frame"
because in our case, these two variables are unused in code that uses an RF_BEAM
and beams do not have models associated with them.
The Beam Effect
Since we have no
model associated with the beam, only an effect, we set our modelindex to 1, a
predefined value meaning "has no model", as opposed to 0, which is what all
uninitialized entities are set to. This should help developers track down
errors, as no entity should ever be rendered with a 0 modelindex -- it would
mean we forgot to set it.
One last
important detail to keep in mind about "RF_BEAM". How LONG is the
beam? Well, just to be clever, the beam only exists between the location
in space defined by s.origin, and s.old_origin. The variable s.old_origin
is supposed to hold the value of where our "origin" (the x,y,z coordinates of
our object) was last game tick (also known as "frame"), if we are an object in
motion. Since beams are supposed to be stationary, and have no movetype,
s.old_origin would normally be unused. We, on the other hand, take special
advantage of it.
We calculate our
velocity using the direction and speed we are passed in. This
velocity is the distance our entity will travel every turn.
Our s.origin is automatically copied to s.old_origin every round in g_main.c
right before calling the physics code that moves our s.origin. Now, by setting
the s.origin and s.old_origin that much apart to begin with, we ensure that the
two positions will always remain that far apart. Thus, our velocity
determines the length of our beam.
If you wanted
the beam length to be any longer or shorter than the velocity, you would have to
change our movetype to MOVETYPE_NONE, and give our entity a think function where
you code your own movement code to remove the relationship between s.old_origin
and our velocity. Also notice our "clipmask" is MASK_SHOT. These
clipmasks determine how soon we decide we have collided with a valid
target. Some entities only want to collide with walls, while others want
to be notified of collisions with monsters, players, or substances like
water. Feel free to play around with the masks, but I have found that
incorrect masks used in determining collisions has been the leading reason for
objects I code to fall out of levels.
Bounding Boxes
The "mins" and
"maxs" we set for our beam are the coordinates for our bounding box for
collisions. "check_dodge" is some code that checks to see if a monster
wants to dodge our weapon fire. This should only be called for slow moving
weapon projectiles as the monster won't have time to dodge the faster ones
anyway. Lastly, call "gi.trace" to trace a line between two
endpoints. In our case, we trace a line between ourselves and the starting
location where our baster should appear in the first round. We do this to
see if we hit something right away. Why bother? If we didn't, our
laser blast might appear on top of a monster or wall in the first round, but not
check to see if it collides with any objects (or backgrounds) till the second
round, where our laser blast has already moved. This, we might be standing
in an enemy player's face and fire, but our blast might go right through them
because it didn't check for a collision soon enough.
Lastly, we don't
want our laser blast lasting forever if it never hits anything. We want it
to vanish if it travels long enough. To accomplish this, we give it a
nextthink of "G_FreeEdict", then tell it it's nextthink is "level.time +
4". This means the entity will automatically self-destruct in 4
seconds. This is slightly longer than a normal blaster's weapon blast
would last, but it seems to make sense for the laser
weapon.
Difficulty: Medium
void Weapon_Blaster_Fire (edict_t *ent)
{
int damage;
if (deathmatch->value)
damage = 15;
else
damage = 10;
Blaster_Fire (ent, vec3_origin, damage, false, EF_BLASTER);
if (ent->client->ps.gunframe == 5) // First frame
ent->client->ps.gunframe+=2;
else
{
ent->client->ps.gunframe++;
}
}
This is a sample from code I am experimenting with LM CTF. It makes the
animation for the hand blaster last 3 frames rather than 4, and attempts to
minimize the jumpiness of the animation by skipping early frames in the
sequence, simulating faster kickback of the weapon.
void fire_blaster (edict_t *self, vec3_t start, vec3_t dir, int damage, int speed, int effect)
{
edict_t *bolt;
trace_t tr;
VectorNormalize (dir);
// Only change hand blaster effect
if (effect & EF_BLASTER)
{
bolt = G_Spawn();
VectorCopy (start, bolt->s.origin);
vectoangles (dir, bolt->s.angles);
VectorScale (dir, speed, bolt->velocity);
VectorAdd (start, bolt->velocity, bolt->s.old_origin);
bolt->clipmask = MASK_SHOT;
bolt->movetype = MOVETYPE_FLYMISSILE;
bolt->solid = SOLID_BBOX;
bolt->s.renderfx |= RF_BEAM;
bolt->s.modelindex = 1;
bolt->owner = self;
bolt->s.frame = 3;
// set the color
if (self->client && self->client->teamnum == 1) // on red team
bolt->s.skinnum = 0xf2f2f0f0;
else // on blue team
bolt->s.skinnum = 0xf3f3f1f1;
VectorSet (bolt->mins, -8, -8, -8);
VectorSet (bolt->maxs, 8, 8, 8);
bolt->touch = blaster_touch;
bolt->nextthink = level.time + 4;
bolt->think = G_FreeEdict;
bolt->dmg = damage;
gi.linkentity (bolt);
if (self->client)
check_dodge (self, bolt->s.origin, dir, speed);
tr = gi.trace (self->s.origin, NULL, NULL, bolt->s.origin, bolt, MASK_SHOT);
if (tr.fraction < 1.0)
{
VectorMA (bolt->s.origin, -10, dir, bolt->s.origin);
bolt->touch (bolt, tr.ent, NULL, NULL);
}
return;
}
bolt = G_Spawn();
VectorCopy (start, bolt->s.origin);
VectorCopy (start, bolt->s.old_origin);
vectoangles (dir, bolt->s.angles);
VectorScale (dir, speed, bolt->velocity);
bolt->movetype = MOVETYPE_FLYMISSILE;
bolt->clipmask = MASK_SHOT;
bolt->solid = SOLID_BBOX;
bolt->s.effects |= effect;
VectorClear (bolt->mins);
VectorClear (bolt->maxs);
bolt->s.modelindex = gi.modelindex ("models/objects/laser/tris.md2");
bolt->s.sound = gi.soundindex ("misc/lasfly.wav");
bolt->owner = self;
bolt->touch = blaster_touch;
bolt->nextthink = level.time + 2;
bolt->think = G_FreeEdict;
bolt->dmg = damage;
gi.linkentity (bolt);
if (self->client)
check_dodge (self, bolt->s.origin, dir, speed);
tr = gi.trace (self->s.origin, NULL, NULL, bolt->s.origin, bolt, MASK_SHOT);
if (tr.fraction < 1.0)
{
VectorMA (bolt->s.origin, -10, dir, bolt->s.origin);
bolt->touch (bolt, tr.ent, NULL, NULL);
}
}
Messing around with fire_blaster has a few consequences. This code
is in "g_weapon.c" rather than "p_weapon.c", meaning it is generic game code
used by more than just the player's weapons. In our case, this routine is
used both by our hand blaster as well as by our hyperblaster. To make sure
the code doesn't alter the look of our hyperblaster, we check the parameter
"effect" as it is passed in. It just so happens that this field tells us
which effect we were looking for.