Sniper's Paradise!
Penetrating Weapons
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).
All logos and trademarks are properties of their respective owners.
Unreal™ is a registered trademark of Epic Games Inc.
Privacy Policy
Website by Softly
Powered by RUSH