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