Armor Piercing Rails

From ioquake3 wiki
Jump to: navigation, search

Tutorial 5 - Armor Piercing Rails

By [sumfuka]

Do you get frustrated when the enemy you've got aimed up for a rail in the head suddenly ducks behind a pillar ?? Let's teach the chicken-shit a lesson.

If you like, go and have a read about Vectors. (Like it or not, a good understanding of vector mathematics is essential to quake coding).

How do rails work?

Let's find the weapon_railgun_fire function at line 334 in g_weapon.c :

/*
=================
weapon_railgun_fire
=================
*/
#define MAX_RAIL_HITS   4
void weapon_railgun_fire (gentity_t *ent) {
        vec3_t          end;
        trace_t         trace;
        gentity_t       *tent;
        gentity_t       *traceEnt;
        int                     damage;
        int                     radiusDamage;
        int                     i;
        int                     hits;
        int                     unlinked;
        gentity_t       *unlinkedEntities[MAX_RAIL_HITS];

        damage = 100 * s_quadFactor;
        radiusDamage = 30 * s_quadFactor;

        VectorMA (muzzle, 8192, forward, end);

        // trace only against the solids, so the railgun will go through people
        unlinked = 0;
        hits = 0;
        do {
                trap_Trace (&trace, muzzle, NULL, NULL, end, ent->s.number, MASK_SHOT );
                if ( trace.entityNum >= ENTITYNUM_MAX_NORMAL ) {
                        break;
                }
                traceEnt = &g_entities[ trace.entityNum ];
                if ( traceEnt->takedamage ) {
                        if( LogAccuracyHit( traceEnt, ent ) ) {
                                hits++;
                        }
                        G_Damage (traceEnt, ent, ent, forward, trace.endpos, damage, 0,
                        MOD_RAILGUN);
                }
                if ( trace.contents & CONTENTS_SOLID ) {
                        break;          // we hit something solid enough to stop the beam
                }
                // unlink this entity, so the next trace will go past it
                trap_UnlinkEntity( traceEnt );
                unlinkedEntities[unlinked] = traceEnt;
                unlinked++;
        } while ( unlinked < MAX_RAIL_HITS );

        // link back in any entities we unlinked
        for ( i = 0 ; i < unlinked ; i++ ) {
                trap_LinkEntity( unlinkedEntities[i] );
        }

        // the final trace endpos will be the terminal point of the rail trail

        // snap the endpos to integers to save net bandwidth, but nudged towards the line
        SnapVectorTowards( trace.endpos, muzzle );

        // send railgun beam effect
        tent = G_TempEntity( trace.endpos, EV_RAILTRAIL );

        // set player number for custom colors on the railtrail
        tent->s.clientNum = ent->s.clientNum;

        VectorCopy( muzzle, tent->s.origin2 );
        // move origin a bit to come closer to the drawn gun muzzle
        VectorMA( tent->s.origin2, 4, right, tent->s.origin2 );
        VectorMA( tent->s.origin2, -1, up, tent->s.origin2 );

        // no explosion at end if SURF_NOIMPACT, but still make the trail
        if ( trace.surfaceFlags & SURF_NOIMPACT ) {
                tent->s.eventParm = 255;        // don't make the explosion at the end
        } else {
                tent->s.eventParm = DirToByte( trace.plane.normal );
        }
        tent->s.clientNum = ent->s.clientNum;

        // give the shooter a reward sound if they have made two railgun hits in a row
        if ( hits == 0 ) {
                // complete miss
                ent->client->accurateCount = 0;
        } else {
                // check for "impressive" reward sound
                ent->client->accurateCount += hits;
                if ( ent->client->accurateCount >= 2 ) {
                        ent->client->accurateCount -= 2;
                        ent->client->ps.persistant[PERS_REWARD_COUNT]++;
                        ent->client->ps.persistant[PERS_REWARD] = REWARD_IMPRESSIVE;
                        ent->client->ps.persistant[PERS_IMPRESSIVE_COUNT]++;
                        // add the sprite over the player's head
                        ent->client->ps.eFlags &= ~(EF_AWARD_IMPRESSIVE |
                        EF_AWARD_EXCELLENT | EF_AWARD_GAUNTLET );
                        ent->client->ps.eFlags |= EF_AWARD_IMPRESSIVE;
                        ent->client->rewardTime = level.time + REWARD_SPRITE_TIME;
                }
                ent->client->ps.persistant[PERS_ACCURACY_HITS]++;
        }

}

Firstly, assume that 'muzzle' is a location vector just in front of the firer, and 'forward' is a direction vector pointing in the direction the client is facing.

Ok, there's a loop here that basically says do { trace the slug until you hit something; if the slug hits a wall, exit the loop; repeat; }. The do { } simply repeats everything in the curly brackets until the code break's out. Inside this loop, the first line calls the function trap_Trace(blah blah blah) - this traces a line through space from the origin (muzzle) in the direction of the rail (forward) for a maximum distance of 8192 units. If we hit something, trace returns information about what we hit.

If the slug hits nothing, the do loop simply exits. If the slug hits something damageable ( if (traceEnt->takedamage), e.g. a player or a button ) then the target is damaged (with G_Damage). If the slug hits a wall or the 'edge of the world' ( if ( trace.entityNum >= ENTITYNUM_MAX_NORMAL ) ) then the loop exits. We want to change this !!

Fire through walls

First we need to a new vector variable (directly below the other variables, near the top of the function). After line 344, add :

void weapon_railgun_fire (gentity_t *ent) {
       vec3_t          end;
       trace_t         trace;
       gentity_t       *tent;
       gentity_t       *traceEnt;
       int                     damage;
       int                     radiusDamage;
       int                     i;
       int                     hits;
       int                     unlinked;
       gentity_t       *unlinkedEntities[MAX_RAIL_HITS];
       vec3_t          tracefrom;      // SUM

A few lines down we can see a 'VectorMA' function call. This creates an 'end' vector that is 8192 units 'forward' of 'muzzle' (the startpoint for the rail). We need to make a copy of 'muzzle' in 'tracefrom', so add a line so that your code looks like this :

       damage = 100 * s_quadFactor;
       radiusDamage = 30 * s_quadFactor;
       VectorMA (muzzle, 8192, forward, end);
       VectorCopy (muzzle, tracefrom);

Next, let's change the trap_Trace function call so that the railgun is traced from 'tracefrom' (instead of muzzle)... there's a good reason for this, read on.

       // trace only against the solids, so the railgun will go through people
       unlinked = 0;
       hits = 0;
       do {
                trap_Trace (&trace, tracefrom, NULL, NULL, end, ent->s.number, MASK_SHOT );

Next, we want to change the behaviour of the if ( trace.entityNum >= ENTITYNUM_MAX_NORMAL ) { ... } block. This if statement detects if our rail slug runs into a 'solid' (e.g. a wall or the sky). What do we want to change ? Well, instead of simply 'breaking' out of the do loop (and marking the endpoint for our slug), we want the slug to keep going through walls. Of course, we still want the slug to stop when we hit the sky. The code for this is :

               if ( trace.entityNum >= ENTITYNUM_MAX_NORMAL ) {
                       // SUM break if we hit the sky
                       if (trace.surfaceFlags & SURF_SKY)
                               break;
                       // Hypo: break if we traversed length of vector tracefrom
                       if (trace.fraction == 1.0)
                               break;
                       // otherwise continue tracing thru walls
                       VectorMA (trace.endpos,1,forward,tracefrom);
                       continue;
               }
               traceEnt = &g_entities[ trace.entityNum ];

In other words, if we hit the sky (check the surfaceFlags), stop. If we've travelled the full length of the vector then we also need to stop (there's no guarantee we'll ever hit the sky.) Otherwise we've hit a solid wall. We set our new 'tracefrom' position 1 unit forward of the impact point (which effectively tunnels through the wall). The loop then repeats - continue; takes us back to the top of the 'do' loop.

Can everyone see it?

Thanks to [WarZone] for this section : as it is, the railgun trail is possibly not seen by people far away on the level. The little addition below makes sure that the rail trail entity is 'broadcast' to everyone (not just those in the vicinity of the firer). This makes sense, it would be stupid to have a railgun fire through a wall and frag someone if the victim couldn't see the rail trail !

       // no explosion at end if SURF_NOIMPACT, but still make the trail
       if ( trace.surfaceFlags & SURF_NOIMPACT ) {
               tent->s.eventParm = 255;        // don't make the explosion at the end
       } else {
               tent->s.eventParm = DirToByte( trace.plane.normal );
       }
       tent->s.clientNum = ent->s.clientNum;
       //send the effect to everyone since it tunnels through walls
       tent->r.svFlags |= SVF_BROADCAST;

I've asked Mark (WarZone) to write an article for us, explaining how 'broadcasting' works and the deeper subject of what goes on in the quake network code. Look out for some more great stuff from him!

Ok, the rest of the function remains the same, and our railgun should fire through walls.

Fire up q3tourney6 and see if you can give Xearo a surprise with your new toy on Nightmare!. "He is got t'go DOWN, man".


« Tutorial 4 - Permanent Haste | Tutorial 5 - Armor Piercing Rails | Tutorial 6 - Bouncing Rockets »