news | about | forums | links | mods | tools | tutorials | contact

Penetrating Weapons - author: [mr.self-destruct]

#### Theory

In Unreal Tournament weapons that immediately hit their target use a trace to locate the target. The trace begins at the player who fired the weapon, travels along their line of sight until it intersects an object (be it wall or player). If we simplify the problem by treating it in 2D, the diagram below illustrates what happens when a normal weapon shoot at a wall:

You'll have to excuse the dodgy diagrams in this tutorial, my drawing skills are somewhat lacking :P

Now to make a weapon appear to fire through a wall we need to begin a second trace starting at a distance x from the where the first trace ended. x is the penetration factor, how thick a wall the weapon will appear to shoot through. When the wall is thinner than x the 2nd trace will continue along until it intersects another object:

When the wall is thicker than x the 2nd trace will hit the inside of the wall and stop:

In the real world it's slightly more complicated that than. A player is soft, so a powerful gun will shoot through them easily, a wall is hard and so the weapon will penetrate through it less. The problem lies in the fact that player's are quite thick, so the penetration factor must be quite high to go through them, with the result that it'll go through thicker walls than we want it to. The solution to the problem is to have two penetration factors, one for pawns and one for anything else. We then check what the first trace hit and then use the relevant penetration factor.

#### Code

"Great you've explained it all and yes the method is quite simple, but how to implement it?" I hear you say. Easy! All you need to do is override the TraceFire() function to implement the second trace. Before I give you the code, remember that it doesn't cut and paste into a new weapon too well. In part this is because it calls ProcessTraceHit() twice, and this is where things such as shell cases get spawned.

// Weapon fire that goes through wall // Penetration through walls var() float Thick; // Penetration through pawns var() float PawnThick;
function TraceFire( float Accuracy )
{
local vector HitLocation, HitNormal, StartTrace, EndTrace, X,Y,Z;
local actor Other;
local Pawn PawnOwner;
local float Penetration;

PawnOwner = Pawn(Owner);

Owner.MakeNoise(PawnOwner.SoundDampening);
GetAxes(PawnOwner.ViewRotation,X,Y,Z);
// First trace, just like ordinary weapon
StartTrace = Owner.Location + CalcDrawOffset() + FireOffset.X * X + FireOffset.Y * Y + FireOffset.Z * Z;
AdjustedAim = PawnOwner.AdjustAim(1000000, StartTrace, 2*AimError, False, False);
EndTrace = StartTrace + Accuracy * (FRand() - 0.5 )* Y * 1000 + Accuracy * (FRand() - 0.5 ) * Z * 1000;
X = vector(AdjustedAim);
EndTrace += (10000 * X);
Other = PawnOwner.TraceShot(HitLocation, HitNormal, EndTrace, StartTrace);
// Deal with first target
ProcessTraceHit(Other, HitLocation, HitNormal, X,Y,Z);

// Second trace, start location takes into account previous target
if (Other.IsA('Pawn'))
Penetration = PawnThick;
else
Penetration = Thick;
StartTrace = HitLocation + HitNormal + (Penetration * X);
EndTrace = StartTrace + Accuracy * (FRand() - 0.5 )* Y * 1000 + Accuracy * (FRand() - 0.5 ) * Z * 1000;
EndTrace += (10000 * X);
Other = PawnOwner.TraceShot(HitLocation, HitNormal, EndTrace, StartTrace);
// Deal with second target
ProcessTraceHit(Other, HitLocation, HitNormal, X,Y,Z);
}

defaultproperties
{
// Walls are hard, so gun penetrates less
Thick=20.0
// Pawns on the other hand are soft and tend to be thicker than
// walls you want to penetrate
// 17 is UT's default player collision radius, so make the gun
// just penetrate a pawn
PawnThick=36.0
}

What?! Only nine extra lines strapped on the end of the standard TraceFire() function, I said it was easy to implement ;)

#### Taking it Further

Having outlined the basic principles behind this, it's possible to take it further and devise a technique that will cope with shooting through multiple targets.
The principle behind this is much the same, but it requires several loops and a counter to keep track of how many times we've traced ahead. I'm not going to go into an in-depth discussion of how the technique works, instead I'll just present you with the code (which is quite well commented):

// Maximum number of times to trace ahead var int MaxPenetration; // How far to trace ahead var float PenetrationDepth; // First shot?
var bool bFirst;

// What did we just hit?
function int ReportMaterial(actor Other)
{
if (Other != None)
{
if (Other == Level)
return 1; // Level
else if (Other.bIsPawn)
return 2; // Pawn
else if (Other.IsA('Decoration'))
return 3; // Decoration
}
else
return 4; // Air
}

// Multiple penetration TraceFire()
function TraceFire (float Accuracy)
{
local vector HitLocation, HitNormal, StartTrace, EndTrace, X,Y,Z, EntryLocation;
local actor Other, LastHit, BOther;
local pawn PawnOwner;
local int Penetration;
local bool bAfterFull;

// Prepare variables
PawnOwner = Pawn(Owner);
Penetration = MaxPenetration;
Owner.MakeNoise(PawnOwner.SoundDampening);
GetAxes(PawnOwner.ViewRotation,X,Y,Z);
StartTrace = Owner.Location + CalcDrawOffset() + FireOffset.X * X + FireOffset.Y * Y + FireOffset.Z * Z;
AdjustedAim = PawnOwner.AdjustAim(1000000, StartTrace, 2*AimError, False, False);
EndTrace = StartTrace + Accuracy * (FRand() - 0.5 )* Y * 1000 + Accuracy * (FRand() - 0.5 ) * Z * 1000;
X = vector(AdjustedAim);
EndTrace += (10000 * X);
bFirst = True;
while (Penetration > 0)
{
// Initial trace
if (bFirst)
{
Other = PawnOwner.TraceShot(HitLocation, HitNormal, EndTrace, StartTrace);
bFirst = False;
bAfterFull = True;
Penetration--;
EntryLocation = HitLocation + HitNormal;
ProcessTraceHit(Other, HitLocation, HitNormal, X,Y,Z);
}
// Full length trace
else
{
StartTrace = EndTrace;
// NB: For more realism, randomly adjust the aim after shooting through something
EndTrace += (10000 * X);
Other = PawnOwner.TraceShot(HitLocation, HitNormal, EndTrace, StartTrace);
Penetration--;
bAfterFull = True;
EntryLocation = HitLocation + HitNormal;
if ((LastHit == Level && bAfterFull) || Other != LastHit)
ProcessTraceHit(Other, HitLocation, HitNormal, X,Y,Z);
}
// Trace ahead through whatever we've hit
while (Penetration > 0 && Other != None)
{
LastHit = Other;
// After a full length trace we trace from where we hit
if (bAfterFull)
StartTrace = HitLocation + HitNormal;
else
StartTrace = EndTrace;
EndTrace = StartTrace + (PenetrationDepth * X);
Other = PawnOwner.TraceShot(HitLocation, HitNormal, EndTrace, StartTrace);
Penetration--;
if (LastHit != Other)
break;
bAfterFull = False;
}
// Back trace for exit decal
BOther = PawnOwner.TraceShot(HitLocation, HitNormal, EntryLocation, EndTrace);
if (BOther == Level)
{
Spawn(class'UT_WallHit',,, HitLocation + HitNormal, Rotator(HitNormal));
}
else if ((BOther != self) && (BOther != Owner) && (BOther != None))
{
if (!BOther.bIsPawn && !BOther.IsA('Carcass'))
spawn(class'UT_SpriteSmokePuff',,, HitLocation + HitNormal * 9);
else
BOther.PlaySound(Sound'ChunkHit',, 4.0,, 100);
}
// NB: VSize((HitLocation + HitNormal) - EntryLocation) = thickness of what we just
shot through }
}

defaultproperties
{
MaxPenetration=4
PenetrationDepth=10.0
}

The above code even generates entry & exit decals :)

Do note however that a trace is a quite expensive operation to perform and too many going on at once can slow things down. The combination of a small PenetrationDepth & high MaxPenetration and/or using this technique with a weapon with a very high rate of fire could produce a notable drop in performance (but most likely only in the case of a multi-player game with many players using that weapon at once).

suggestions? questions? comments? please send them all to ray@beyondunreal.com

copyright 2001 ray davis. this site and all content belongs to me unless otherwise noted, with the exception of some tutorials. the authors retain the rights to their material, and are hosted here with their express permission. everything else is mine though, so don't steal it. bastards.
privacy