Penetrating Weapons

From Oldunreal-Wiki
Jump to navigation Jump to search

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).