
Quake DeveLS - Star Wars Blaster
Author: LM_Jormungard
Difficulty: Medium
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.
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.
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.
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.
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.
|
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 |