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