Wall Piercing Railgun
Author: mnuck
Difficulty: Easy/Hard (short, but thick conceptually)
Wall Piercing Railgun
Once upon a midnight dreary, while I pondered, weak and weary, Over many a quaint and curious volume of forgotten lore, While I nodded, nearly napping, suddenly there came a tapping, As of someone gently rapping, rapping at my chamber door. "'Tis some visitor," I muttered, "tapping at my chamber door; Only this, and nothing more." -E.A.Poe "The Raven"
I think of this verse when I shut the door on someone. Only a few seconds ago, they were the badass with the railgun, and I was the weenie running for my life. Now, with only a few (virtual) inches of steel between me and him, his railgun is naught but useless, incapable of tapping- much less gently rapping- at my chamber door. In fact, he could set up some sort of rail-gatling gun over there, and I’m perfectly safe over here with this wall between me and him.
Until now.
I’ve always wondered why a slug moving at an appreciable fraction of the speed of light should be stopped cold by the thinnest sheet of quakewall. This mod takes care of that little problem, allowing the railgun to fire through “thin” walls, with damage inflicted growing less and less with each wall traversal.
First I’ll give the whole mod at once for you cut-and-paste “programmers”, then I’ll cover the changes from the original code, piece by piece, for those of you looking for a tutorial.
Replace the fire_rail() function in g_weapon.c with the following:
void fire_rail (edict_t *self,
vec3_t start,
vec3_t aimdir,
int damage,
int kick)
{
vec3_t from;
vec3_t end;
trace_t tr;
edict_t *ignore;
int mask;
qboolean water;
int wallcount;
int i;
vec3_t lenvec;
float len;
VectorNormalize( aimdir );
for( i=0 ; i < 3 ; i++ )
{
if( aimdir[i] == 0 )
lenvec[i] = 15000;
else
lenvec[i] = (4096 - fabs( start[i] ) )
/ fabs( aimdir[i] );
}
if( lenvec[0] < lenvec [1] )
len = lenvec[0];
else
len = lenvec[1];
if( lenvec[2] < len )
len = lenvec[2];
VectorMA (start, len, aimdir, end);
VectorCopy (start, from);
ignore = self;
water = false;
wallcount = 0;
mask = MASK_SHOT|CONTENTS_SLIME|CONTENTS_LAVA;
while (ignore)
{
tr = gi.trace (from, NULL, NULL, end, ignore, mask);
if (tr.contents & (CONTENTS_SLIME|CONTENTS_LAVA))
{
mask &= ~(CONTENTS_SLIME|CONTENTS_LAVA);
water = true;
}
else
{
if ((tr.ent != self) && (tr.ent->takedamage))
T_Damage (tr.ent, self, self, aimdir, tr.endpos,
tr.plane.normal, damage, kick, 0, MOD_RAILGUN);
if ((tr.ent->svflags & SVF_MONSTER) || (tr.ent->client))
ignore = tr.ent;
else
{
if( wallcount > 5 )
ignore = NULL;
else
{
wallcount++;
VectorMA( tr.endpos, 10, aimdir, from );
damage = damage * 0.75;
continue;
}
}
}
VectorCopy (tr.endpos, from);
}
// send gun puff / flash
gi.WriteByte (svc_temp_entity);
gi.WriteByte (TE_RAILTRAIL);
gi.WritePosition (start);
gi.WritePosition (tr.endpos);
gi.multicast (self->s.origin, MULTICAST_PHS);
if (water)
{
gi.WriteByte (svc_temp_entity);
gi.WriteByte (TE_RAILTRAIL);
gi.WritePosition (start);
gi.WritePosition (tr.endpos);
gi.multicast (tr.endpos, MULTICAST_PHS);
}
if (self->client)
PlayerNoise(self, tr.endpos, PNOISE_IMPACT);
}
You’ve now got a railgun that can fire through fairly thick walls, losing 25% damage inflicted for each 10 QuakeUnits spent “inside” a wall. Now I’ll go over each change from the standard code.
int wallcount;
int i;
vec3_t lenvec;
float len;
All these variables will be used at one point or another in the code. Since wallcount and i are serial (i.e. by the time I start using one I’m completely done with the other) I could make them one variable, but a good optimizing compiler will do that for me, leaving me free to write readable code.
VectorNormalize( aimdir );
for( i=0 ; i < 3 ; i++ )
{
if( aimdir[i] == 0 )
lenvec[i] = 15000;
else
lenvec[i] = (4096 - fabs( start[i] ) )
/ fabs( aimdir[i] );
}
Mmm, trig. I hate it. I think it’s the kind of math to turns people off to math. Of the branches of mathematics, only Statistics and Probability ranks lower than Trig. Then again, I put Number Theory on top, so maybe my opinion is suspect. Still, trig is useful stuff, especially if we’re trying to calculate the length of a line segment in 3-space from our current location to a given surface along a given direction, trig is the way to go.
NOTE:
If you have the math to follow this, then you have the math to follow this. If you don’t, you don’t, and I’m not going to attempt to give it to you. I will fail, and you will be frusterated. That’s part of the reason I don’t like trig. If you have the math and can think “similar triangles ought to work here”, then you can derive what I did yourself, given the below info. If not, then accept my code as powerful mojo that finds then lengths to the various bounding planes.
First, some info about the Quake2 universe.
Given:
Distance is measured in QuakeUnits. What’s a QuakeUnit? Who cares? It’s just an arbitrary unit of measure.
Given:
The Quake2 universe is a cube of edge length 8192 QuakeUnits, centered about the origin. It can therefore be described as the volume contained by six planes, one each at x=4096, x=-4096, y=4096, y=-4096, z=4096, and z=-4096.
Now some info about our situation.
Given:
We have a direction vector of 3 dimensions called aimdir that may or may not be normalized (of length 1). This vector describes the direction of the line segment we’re trying to calculate the length of.
Given:
We have a position vector of 3 dimensions called start. Each component is a float contained by [-4096,4096]. This vector describes the starting point of our shot.
If you can derive it yourself from that, good for you! If not, no shame, I don’t like trig either.
Did I mention I don’t like trig?
Anyway, we now have three lengths, one to each bounding plane we encounter. You may have realized that there exist directions that intersect only two planes, and there are six distinct directions that only intersect one plane. We catch those cases at the outset and set the cooresponding lengths to an arbitrary high number, high enough to simulate infinity.
We want the least of the three lengths, that being the maximum shot allowable in this direction without leaving the universe. Hence:
if( lenvec[0] < lenvec [1] )
len = lenvec[0];
else
len = lenvec[1];
if( lenvec[2] < len )
len = lenvec[2];
In pseudocode, you would say “len = min( lenvec[0], lenvec[1], lenvec[2] );”, but unfortunately we don’t have a min() function that takes three inputs. Besides, doing it this way give us a slight advantage. Notice how the x-axis and y-axis checks are contained in the same if/else block and the z-axis check is done alone? How many times do you suppose the z-axis length is the one we want? Only when we’re shooting straight up-ish or straight down-ish, right? Not very often then. That last if will usually be false then, right? Well if something is “usually” one way or another, the CPU can use speculative execution. “What’s speculative execution?” you ask. It’s beyond the scope of this tutorial is what it is (kinda like trig. Did I mention I h…never mind). Go look it up.
VectorMA (start, len, aimdir, end);
This is one of the key lines to this mod. In the original code it looked like this:
VectorMA (start, 8192, aimdir, end);
So what changed? Why are we calculating the length of our shot when the great one (JC, aka John Carmack) saw fit to just blast away? Johnny boy’s shots don’t go through walls, that’s why. The inner loop of this function is the call to gi.trace(), which has the interesting property that if it traces to the edge of the universe and beyond, it will wrap around and come back at you from behind. This isn’t a problem with the original railgun because the shot is going to hit something that will stop it before the wraparound effect becomes visible. Since our shots go through obstacles, we have to be a bit more responsible in what we trace. This becomes more important when we think about how the Blue Spiral of Death effect is communicated to the engine.
gi.WriteByte (svc_temp_entity);
gi.WriteByte (TE_RAILTRAIL);
gi.WritePosition (start);
gi.WritePosition (tr.endpos);
gi.multicast (self->s.origin, MULTICAST_PHS);
So we’re saying “Hey game engine, draw a temporary entity of type TE_RAILTRAIL from this starting point to this ending point. Thanks man.” In the original railgun tr.endpos was always directly in front of you. With the new railgun and its ability to wraparound, tr.endpos might be anywhere. The game engine doesn’t care which direction the slug went, it’s just gonna draw a BSoD between a start point and an end point. Believe me, it looks kinda funny when you pull the trigger, the Strogg right in front of you goes *splat*, and your BSoD goes off up and to your left somewhere. We obviously then have to keep tr.endpos directly in front of us. No more wraparound shots. Since gi.trace() isn’t smart enough to stop when the universe ends, we have to figure out the appropriate stopping point ourselves. So there it is.
All that, and we haven’t even started shooting through walls yet. Hey, if it were easy, anybody could do it. ;-)
One more thing before we start blasting away. Barrels stop railgun shots. We don’t want barrels to stop railgun shots. We’ll solve that later, but barrels also take damage (by “barrel” I mean any sort of object descendant from DooM barrels that can take damage and will probably explode). To assure they take damage, we move a bit of code up inside the main else() inside the while() loop.
if ((tr.ent != self) && (tr.ent->takedamage))
T_Damage (tr.ent, self, self, aimdir, tr.endpos,
tr.plane.normal, damage, kick, 0, MOD_RAILGUN);
Now (finally) we’re ready to tunnel through some walls.
if( wallcount > 5 )
ignore = NULL;
else
{
wallcount++;
VectorMA( tr.endpos, 10, aimdir, from );
damage = damage * 0.75;
continue;
}
The original code looked like:
ignore = NULL;
And that’s how you tunnel through a wall. Do a VectorMA( tr.endpos, 10, aimdir, from ) to step through, wrap it in an if/else() if you want to limit the amount of wall you can traverse, and reduce damage as you go. That’s it. That’s all. Tunneling through walls is easy. Dealing with all the side effects is hard.
Happy hunting, and good luck aiming at something you can’t see.
Tutorial by mnuck.
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 their great help and support with hosting. Best viewed with Netscape 4 |
http://web.archive.org/web/19990504200050/http://www.planetquake.com/qdevels/quake2/wp_rail.html