Contents
Overview
Goal
This page will describe, and will hopefully provide solutions to, the xPawn "priority bug" in UT2003 and UT2004.
Volume Priority
The priority variable found in PhysicsVolume determines which one of several overlapping PhysicsVolumes gets to apply its physics properties (Gravity, GroundFriction, ZoneVelocity and so on) on players and other actors in the overlapping area. The higher the value of priority, the higher the Priority. Every PhysicsVolume added, however, even if priority is 0, has a higher priority than the DefaultPhysicsVolume.
The Bug - In Brief
There is a bug in the implementation of the priority scheme in xPawn as it pertains to interactions with PhysicsVolumes. If the player is contained within overlapping volumes and one of the volumes has bWaterVolume set to True, splashing sounds are likely to be played when a Player walks, dodges, or whatever, regardless of which volume has priority.
Cause
How Volume Priority And Splashing Are Handled
PlayerReplicationInfo
The following code was extracted from an export of the PlayerReplicationInfo class, UT2004 build 3186
function UpdatePlayerLocation() { local Volume V, Best; local Pawn P; local Controller C; C = Controller(Owner); if( C != None ) P = C.Pawn; if( P == None ) { PlayerVolume = None; PlayerZone = None; return; } if ( PlayerZone != P.Region.Zone ) PlayerZone = P.Region.Zone; foreach P.TouchingActors( class'Volume', V ) { if( V.LocationName == "") continue; if( (Best != None) && (V.LocationPriority <= Best.LocationPriority) ) continue; if( V.Encompasses(P) ) Best = V; } if ( PlayerVolume != Best ) PlayerVolume = Best; }
xPawn
The following code was extracted from an export of the xPawn class, UT2004 build 3186
simulated function FootStepping(int Side) { local int SurfaceNum, i; local actor A; local material FloorMat; local vector HL,HN,Start,End; SurfaceNum = 0; for ( i=0; i<Touching.Length; i++ ) if ( ((PhysicsVolume(Touching[i]) != None) && PhysicsVolume(Touching[i]).bWaterVolume) || (FluidSurfaceInfo(Touching[i]) != None) ) { if ( FRand() < 0.5 ) PlaySound(sound'PlayerSounds.FootStepWater2', SLOT_Interact, FootstepVolume ); else PlaySound(sound'PlayerSounds.FootStepWater1', SLOT_Interact, FootstepVolume ); return; } if ( bIsCrouched || bIsWalking ) return; if ( (Base!=None) && (!Base.IsA('LevelInfo')) && (Base.SurfaceType!=0) ) { SurfaceNum = Base.SurfaceType; } else { Start = Location - Vect(0,0,1)*CollisionHeight; End = Start - Vect(0,0,16); A = Trace(hl,hn,End,Start,false,,FloorMat); if (FloorMat !=None) SurfaceNum = FloorMat.SurfaceType; } PlaySound(SoundFootsteps[SurfaceNum], SLOT_Interact, FootstepVolume,,400 ); }
PhysicsVolume
The following code was extracted from an export of the PhysicsVolume class, UT2004 build 3186
simulated function PostBeginPlay() { super.PostBeginPlay(); BACKUP_Gravity = Gravity; BACKUP_bPainCausing = bPainCausing; if( VolumeEffect == None && bWaterVolume ) VolumeEffect = new(None) class'EFFECT_WaterVolume'; } // // (...) // event touch(Actor Other) { local Pawn P; local bool bFoundPawn; Super.Touch(Other); if ( Other == None ) return; if ( bNoInventory && (Pickup(Other) != None) && (Other.Owner == None) ) { Other.LifeSpan = 1.5; return; } if ( bMoveProjectiles && (ZoneVelocity != vect(0,0,0)) ) { if ( Other.Physics == PHYS_Projectile ) Other.Velocity += ZoneVelocity; else if ( (Other.Base == None) && Other.IsA('Emitter') && (Other.Physics == PHYS_None) ) { Other.SetPhysics(PHYS_Projectile); Other.Velocity += ZoneVelocity; } } if ( bPainCausing ) { if ( Other.bDestroyInPainVolume ) { Other.Destroy(); return; } if ( Other.bCanBeDamaged && !Other.bStatic ) { CausePainTo(Other); if ( Other == None ) return; if ( PainTimer == None ) PainTimer = Spawn(class'VolumeTimer', self); else if ( Pawn(Other) != None ) { ForEach TouchingActors(class'Pawn', P) if ( (P != Other) && P.bCanBeDamaged ) { bFoundPawn = true; break; } if ( !bFoundPawn ) PainTimer.SetTimer(1.0,true); } } } if ( bWaterVolume && Other.CanSplash() ) PlayEntrySplash(Other); } // // (...) // function PlayEntrySplash(Actor Other) { local float SplashSize; local actor splash; splashSize = FClamp(0.00003 * Other.Mass * (250 - 0.5 * FMax(-600,Other.Velocity.Z)), 0.1, 1.0 ); if( EntrySound != None ) { PlaySound(EntrySound, SLOT_Interact, splashSize); if ( Other.Instigator != None ) MakeNoise(SplashSize); } if( EntryActor != None ) { splash = Spawn(EntryActor); if ( splash != None ) splash.SetDrawScale(splashSize); } } event untouch(Actor Other) { if ( bWaterVolume && Other.CanSplash() ) PlayExitSplash(Other); } function PlayExitSplash(Actor Other) { local float SplashSize; local actor splash; splashSize = FClamp(0.003 * Other.Mass, 0.1, 1.0 ); if( ExitSound != None ) PlaySound(ExitSound, SLOT_Interact, splashSize); if( ExitActor != None ) { splash = Spawn(ExitActor); if ( splash != None ) splash.SetDrawScale(splashSize); } }
The Problem
The most obvious problem is that the FootStepping() function in xPawn takes it upon itself to check for touching PhysicsVolumes, rather than using the PlayerVolume var in PlayerReplicationInfo, in order to determine if a "splash" should be "played". In doing this, it bypasses the priority check made in the UpdatePlayerLocation() function of PlayerReplicationInfo, thus eliminating the priority scheme for the "splashing" functionality.
Solutions
Proposal One: Avoid Overlapping Volumes
Note: Obviously, this is just a work-around...
Just like the title says: Avoid overlapping PhysicsVolumes.
Pros:
- You avoid the issue all together
Cons:
- You potentially sacrifice certain senarios–"dynamic" volumes attached to movers interacting with one another, for instance.
Proposal Two: Modify xPawn
One solution may be to add a new check to the simulated function FootStepping() in xPawn (or in your own new pawn class) that compares touching volumes with bWaterVolume set to True with PlayerVolume in PlayerReplicationInfo. If they are not one in the same, skip the splashing code.
Pros:
- Will likely solve the problem
Cons:
- By extending xPawn, you now essentially are forced into making a mod/mutator in order to impliment your changes. Pawn-type is set in the player class (by default, xPlayer) which is in turn is set by a GameInfo actor like xDeathMatch.