A few commentaries to the code. The first part is about changes in class PlayerPawn.
function ClientUpdatePosition()
{
local SavedMove CurrentMove;
local int realbRun, realbDuck;
- local bool bRealJump,bRealCrouch;
+ local bool bRealJump;
realbRun = bRun;
realbDuck = bDuck;
bRealJump = bPressedJump;
- bRealCrouch = bIsReducedCrouch;
CurrentMove = SavedMoves;
while ( CurrentMove != None )
{
if ( CurrentMove.TimeStamp <= CurrentTimeStamp )
{
SavedMoves = CurrentMove.NextMove;
CurrentMove.NextMove = FreeMoves;
FreeMoves = CurrentMove;
FreeMoves.Clear();
CurrentMove = SavedMoves;
}
else
{
CrouchCheckTime = -1;
- SetCrouch(CurrentMove.bIsReducedCrouch);
MoveAutonomous(CurrentMove.Delta, CurrentMove.bRun, CurrentMove.bDuck, CurrentMove.bPressedJump, CurrentMove.DodgeMove, CurrentMove.Acceleration, rot(0,0,0));
CurrentMove = CurrentMove.NextMove;
}
}
bDuck = realbDuck;
bRun = realbRun;
bPressedJump = bRealJump;
CrouchCheckTime = -1;
- SetCrouch(bRealCrouch);
bUpdatePosition = false;
//log("Client adjusted "$self$" stamp "$CurrentTimeStamp$" location "$Location$" dodge "$DodgeDir);
}
I don't see reasons to call SetCrouch from ClientUpdatePosition; moreover, when I logged the value of CurrentMove.bIsReducedCrouch, it was always equal to false.
event UpdateEyeHeight(float DeltaTime)
{
local float smooth, bound;
+ if (bIsReducedCrouch && IsInState('PlayerWalking'))
+ BaseEyeHeight = default.BaseEyeHeight + CollisionHeight - default.CollisionHeight;
+
smooth = FMin(1.0, 10.0 * DeltaTime/Level.TimeDilation);
// smooth up/down stairs
If( (IsInState('PlayerSwimming') || Physics==PHYS_Walking) && !bJustLanded )
{
EyeHeight = (EyeHeight - Location.Z + OldLocation.Z) * (1 - smooth) + ( ShakeVert + BaseEyeHeight) * smooth;
bound = -0.5 * CollisionHeight;
if (EyeHeight < bound)
EyeHeight = bound;
else
{
- bound = CollisionHeight + FMin(FMax(0.0,(OldLocation.Z - Location.Z)), MaxStepHeight);
+ bound = FMin(FMax(0.0,(OldLocation.Z - Location.Z)), MaxStepHeight);
+ if (bIsReducedCrouch)
+ bound += default.CollisionHeight;
+ else
+ bound += CollisionHeight;
if ( EyeHeight > bound )
EyeHeight = bound;
This change is necessary for smooth movement of player's camera when the player begins to crouch.
simulated function bool AdjustHitLocation(out vector HitLocation, vector TraceDir)
{
local float adjZ, maxZ;
TraceDir = Normal(TraceDir);
HitLocation = HitLocation + 0.5 * CollisionRadius * TraceDir;
- if ( BaseEyeHeight == Default.BaseEyeHeight )
+ if (BaseEyeHeight == default.BaseEyeHeight || bIsCrouching && bIsReducedCrouch)
return true;
When the player is crouching and the collision height is reduced, there is no need in cutting the top part of the collision cylinder.
event PlayerCalcView(out actor ViewActor, out vector CameraLocation, out rotator CameraRotation )
{
local Pawn PTarget;
if ( ViewTarget != None )
{
ViewActor = ViewTarget;
CameraLocation = ViewTarget.Location;
CameraRotation = ViewTarget.Rotation;
PTarget = Pawn(ViewTarget);
if ( PTarget != None )
{
if ( Level.NetMode == NM_Client )
{
if ( PTarget.bIsPlayer )
PTarget.ViewRotation = TargetViewRotation;
PTarget.EyeHeight = TargetEyeHeight;
if ( PTarget.Weapon != None )
PTarget.Weapon.PlayerViewOffset = TargetWeaponViewOffset;
}
if ( PTarget.bIsPlayer )
CameraRotation = PTarget.ViewRotation;
if ( !bBehindView )
CameraLocation.Z += PTarget.EyeHeight;
+ else
+ CameraLocation.Z += PTarget.PrePivot.Z - PTarget.default.PrePivot.Z;
}
if ( bBehindView )
CalcBehindView(CameraLocation, CameraRotation, 180);
return;
}
ViewActor = Self;
CameraLocation = Location;
if ( bBehindView ) //up and behind
+ {
+ CameraLocation.Z += PrePivot.Z - default.PrePivot.Z;
CalcBehindView(CameraLocation, CameraRotation, 150);
+ }
else
This change prevents the camera from jumping up and down when crouching and standing up while using 3rd person view.
In state PlayerWalking:
function AnimEnd()
{
local name MyAnimGroup;
bAnimTransition = false;
if ( Physics == PHYS_Spider )
{
if ( VSize(Velocity)<10 )
PlayDuck();
else PlayCrawling();
}
else if (Physics == PHYS_Walking)
{
....
}
+ else if (bIsReducedCrouch)
+ PlayDuck();
else
PlayInAir();
}
PlayInAir animation doesn't look appropriate when the player is crouching in air.
In state PlayerWalking:
function ProcessMove(float DeltaTime, vector NewAccel, eDodgeDir DodgeMove, rotator DeltaRot)
{
local vector OldAccel;
OldAccel = Acceleration;
Acceleration = NewAccel;
bIsTurning = ( Abs(DeltaRot.Yaw/DeltaTime) > 5000 );
if ( (DodgeMove == DODGE_Active) && (Physics == PHYS_Falling) )
DodgeDir = DODGE_Active;
else if ( (DodgeMove != DODGE_None) && (DodgeMove < DODGE_Active) )
Dodge(DodgeMove);
if ( bPressedJump )
DoJump();
if ( (Physics == PHYS_Walking) && (GetAnimGroup(AnimSequence) != 'Dodge') )
{
if ( !bIsCrouching )
{
- if ( bDuck != 0 && TryToDuck(true) )
+ if ( bDuck != 0 && TryToDuck(true) && IsInState('PlayerWalking') ) // Note: TryToDuck may change the state (e.g. to PlayerSwimming)
{
bIsCrouching = true;
PlayDuck();
}
}
else if ( bDuck == 0 && TryToDuck(false) )
{
OldAccel = vect(0,0,0);
bIsCrouching = false;
}
TryToDuck may change player's state to PlayerSwimming (because new player location may reside a water zone), then changing the value of bIsCrouching to true produces weird effects.
In state PlayerWalking:
function BeginState()
{
WalkBob = vect(0,0,0);
DodgeDir = DODGE_None;
- bIsCrouching = false;
+ bIsCrouching = bIsReducedCrouch;
bIsTurning = false;
bPressedJump = false;
if (Physics != PHYS_Falling)
SetPhysics(PHYS_Walking);
if ( !IsAnimating() )
- PlayWaiting();
+ {
+ if (bIsCrouching)
+ PlayDuck();
+ else
+ PlayWaiting();
+ }
}
A crouching player may enter a water zone and leave it while holding the crouch key. A correct and smooth transition from PlayerSwimming to PlayerWalking needs setting the value of bIsCrouching to the value that would correspond to bIsReducedCrouch which reflects whether the height of player's collision cylinder is reduced.
In state PlayerWalking:
function EndState()
{
WalkBob = vect(0,0,0);
bIsCrouching = false;
- SetCrouch(false);
}
Restoring the original collision height and location when leaving the PlayerWalking state may cause undesirable effects, because the state can be changed due to location change implied by real crouching. Uncrouching may imply changing the location back to its previous value which in turn may imply the need in changing the state back to PlayerWalking. This way we can end up in having a series of state changes like PlayerWalking -> PlayerSwimming -> PlayerWalking -> PlayerSwimming -> PlayerWalking -> ...
In state FeigningDeath:
function Rise()
{
if ( !bRising )
{
Enable('AnimEnd');
- BaseEyeHeight = Default.BaseEyeHeight;
bRising = true;
- PlayRising();
+ if (TryToDuck(false))
+ {
+ BaseEyeHeight = default.BaseEyeHeight;
+ PlayRising();
+ }
+ else
+ {
+ PlayDuck();
+ BaseEyeHeight = default.BaseEyeHeight + CollisionHeight - default.CollisionHeight;
+ }
}
}
If the player cannot stand up due to insufficient space, rising should be performed as a transition from feigning death to crouching.
In state FeigningDeath:
+ simulated function bool ShouldHaveReducedHeight()
+ {
+ return !bRising;
+ }
This function is supposed to override the corresponding global function (see below). True result indicates that the collision cylinder should have a reduced height, false result indicates that an attempt to restore the original CollisionHeight should be done (CollisionHeight is changed as a part of standing up; standing up is possible only if there is enough space).
In state FeigningDeath:
function BeginState()
{
local rotator NewRot;
if ( carriedDecoration != None )
DropDecoration();
NewRot = Rotation;
NewRot.Pitch = 0;
SetRotation(NewRot);
BaseEyeHeight = -0.5 * CollisionHeight;
bIsCrouching = false;
bPressedJump = false;
bRising = false;
Disable('AnimEnd');
PlayFeignDeath();
PlayerReplicationInfo.bFeigningDeath = true;
+ TryToDuck(true);
}
If crouching implies that CollisionHeight should be reduced, it would be logical to reduce CollisionHeight when the player is feigning death too. Since players cannot move when feigning death, I do not suggest to choose a smaller value of CollisionHeight than the one used for crouching. Maintaining the same CollisionHeight as would be used for crouching does not allow to block the player in a way so that he would not be able to quit feigning death and either stand up or begin to crouch at least.
In state PlayerSwimming:
event UpdateEyeHeight(float DeltaTime)
{
local float smooth, bound;
// smooth up/down stairs
if ( !bJustLanded )
{
smooth = FMin(1.0, 10.0 * DeltaTime/Level.TimeDilation);
EyeHeight = (EyeHeight - Location.Z + OldLocation.Z) * (1 - smooth) + ( ShakeVert + BaseEyeHeight) * smooth;
bound = -0.5 * CollisionHeight;
if (EyeHeight < bound)
EyeHeight = bound;
else
{
bound = CollisionHeight + FClamp((OldLocation.Z - Location.Z), 0.0, MaxStepHeight);
+ if (bIsReducedCrouch)
+ bound += default.CollisionHeight - CollisionHeight;
if ( EyeHeight > bound )
EyeHeight = bound;
This change is necessary for smooth movement of player's camera when the player begins to crouch.
In state PlayerSwimming:
function BeginState()
{
Disable('Timer');
if (!IsAnimating())
TweenToWaiting(0.3);
+ else if (bIsReducedCrouch)
+ TweenToSwimming(0.2);
//log("player swimming");
}
This change is needed for smooth animation transition.
In function SetCrouch:
bIsReducedCrouch = bCrouching;
+ class'RealCrouchController'.static.GetRealCrouchController(self);
This function creates new RC controller if the player has no such a controller yet. The RC controller serves two purposes. Firstly, it checks whether the player's collision cylinder definitely must have a reduced CollisionHeight or full CollisionHeight and if the current value does not match the required one, then the controller changes CollisionHeight correspondingly. This check is done on every tick. Since PlayerPawn.PlayerTick is widely overridden in various states and may be overridden in subclasses, using the Tick event of a separate actor appears to be more simple and reliable solution.
Secondly, RC controller implements speculative move operations, it lets us determine whether the player can be moved from "here" to "there". Basically, it imitates the player's collision cylinder.
In function SetCrouch:
if (bCrouching)
{
// Update collision size, prepivot height and location.
- OldHeight = CollisionHeight;
- SetCollisionSize(CollisionRadius,CollisionHeight*CrouchHeightPct);
- OldHeight = OldHeight-CollisionHeight;
- PrePivot.Z+=OldHeight;
- EyeHeight+=OldHeight;
- Move(vect(0,0,-1)*OldHeight);
+ NewCollisionHeight = default.CollisionHeight * CrouchHeightPct;
+ MoveDistance = CollisionHeight - NewCollisionHeight;
+ if (MoveDistance <= 0)
+ return;
+
+ VerticalOffset = default.CollisionHeight - NewCollisionHeight;
+ SetCollisionSize(CollisionRadius, NewCollisionHeight);
+ PrePivot.Z = default.PrePivot.Z + VerticalOffset;
+ EyeHeight += MoveDistance;
+ Move(vect(0, 0, -1) * MoveDistance);
}
default.CollisionHeight is a reliable basis, while CollisionHeight may have an outdated value assigned through server-to-client replication. It's worth noting though that using default.CollisionHeight limits the freedom of changing CollisionHeight by third party mods.
In function SetCrouch:
else
{
// Reset collision size, prepivot height and location.
- OldHeight = CollisionHeight;
- NewHeight = CollisionHeight/CrouchHeightPct;
- OldHeight = NewHeight-OldHeight;
- PrePivot.Z-=OldHeight;
- OldHeight*=0.5f;
- EyeHeight-=OldHeight;
- Move(vect(0,0,2)*OldHeight);
- SetLocation(Location);
- SetCollisionSize(CollisionRadius,NewHeight);
+ NewCollisionHeight = default.CollisionHeight;
+ MoveDistance = NewCollisionHeight - CollisionHeight;
+
+ // Use the cached result when indirectly calling from TryToDuck or a newly calculated result otherwise
+ if (MoveDistance <= 0 || !class'RealCrouchController'.static.CanStandUp(self, NewCollisionHeight))
+ return;
+
+ PrePivot.Z = default.PrePivot.Z;
+ EyeHeight -= MoveDistance;
+
+ bOldBlockActors = bBlockActors;
+ bOldBlockPlayers = bBlockPlayers;
+ bOldCollideWorld = bCollideWorld;
+
+ SetCollision(bCollideActors, false, false);
+ bCollideWorld = false;
+ SetCollisionSize(CollisionRadius, NewCollisionHeight);
+ Move(vect(0, 0, 1) * MoveDistance);
+ SetLocation(Location);
+ if (CarriedDecoration != none)
+ {
+ CarriedDecoration.SetPhysics(PHYS_None);
+ CarriedDecoration.SetBase(self);
+ }
+ SetCollision(bCollideActors, bOldBlockActors, bOldBlockPlayers);
+ bCollideWorld = bOldCollideWorld;
}
Generally, SetCrouch is supposed to be an internal function in the new implementation, mods should not call it directly. In case if someone calls it nevertheless, SetCrouch(false) performs an extra check if the player can stand up (such a check is supposed to be done by TryToDuck).
Once the player is known to be able to stand up, the player is moved to the new location. During the move operation the player cannot be blocked by level geometry or other actors and cannot encroach on other actors (the cause of telefragging). SetLocation(Location) ensures that the player touches all actors that might be touched when moving the top bound of the player in a usual way. The call to Move moves the player and the CarriedDecoration (if any); it's possible to use only SetLocation, but then CarriedDecoration has to be moved separately in order to resolve issue 6.1. I don't see an easy resolution for issue 6 in general, having only the currently available set of C++ functions.
Changing CollisionHeight and Location could be done in a much more clean way if we had a function that could perform these two operations as a single transaction:
// Effects: the function moves the actor by (ZOffset * vect(0, 0, 1)) and changes its CollisionHeight
// to (CollisionHeight + ZOffset) - hence the collision top is moved by (ZOffset * vect(0, 0, 2)).
// Other actors can be touched as if Move(ZOffset * vect(0, 0, 2)) was called instead, but the bottom
// of the collision cylinder does not change its relative position to the level and the actor cannot be
// blocked by the level geometry or other actors during the move operation. A possible overlapping with
// other actors does not cause calls to EncroachingOn or EncroachedBy. Moving the actor out of world is
// allowed.
// Precondition: ZOffset shall not be less than -CollisionRadius
//
native final function bool MoveCollisionTop(float ZOffset);
simulated final function bool TryToDuck(bool bCrouching)
{
- local vector Dummy,Offset,Start;
if (!Level.bSupportsRealCrouching || bIsReducedCrouch == bCrouching)
return true;
if (bCrouching)
{
SetCrouch(true);
return true;
}
// Make sure there is space to stand up
- Start = Location;
- Start.Z = Location.Z+CollisionHeight-0.01f;
- Offset.Z = ((CollisionHeight/CrouchHeightPct)-CollisionHeight);
- if( Trace(Dummy,Dummy,Start+Offset,Start,true,vect(1.f,1.f,0.f)*CollisionRadius)!=None )
- return false; // Wasnt enough space to uncrouch.
+ if (!class'RealCrouchController'.static.CanStandUp(self, default.CollisionHeight))
+ return false; // Not enough space to uncrouch
SetCrouch(false);
return true;
}
The method based on the Trace function may give false-positive and false-negative results. CanStandUp uses a more reliable method based on a speculative move operation (see below).
simulated function bool ShouldHaveReducedHeight()
{
return bIsCrouching || bIsReducedCrouch && bDuck != 0;
}
The result of this function is used by RealCrouchController.Tick to determine if the player should be forced to crouch or try to uncrouch (see below).
+simulated function bool IsHeadShot(vector HitLocation, vector TraceDir)
+{
+ return !bIsReducedCrouch && super.IsHeadShot(HitLocation, TraceDir);
+}
Crouching is known as a trick that prevents headshots. Although it's more likely a dev's oversight than a planned behavior, I think that people have widely used this trick for years, and disallowing it in case of real crouching wouldn't be appreciated by gamers. Tweaking AdjustHitLocation as shown above would allow headshots for crouching players, and this definition of IsHeadShot is supposed to negate such an effect, so that old trick would still work.
Now about new class RealCrouchController.
static function RealCrouchController GetRealCrouchController(PlayerPawn Player)
{
local RealCrouchController CrouchController;
foreach Player.ChildActors(class'RealCrouchController', CrouchController)
break;
if (CrouchController == none || CrouchController.bDeleteMe)
return Player.Spawn(class'RealCrouchController', Player);
return CrouchController;
}
Finding the existing controller can be optimized by adding a new property in class PlayerPawn:
var RealCrouhController RealCrouchController;
I used ChildActors for testing purposes, because adding new variables in PlayerPawn by means of UnrealEd doesn't work well.
In function CanStandUp:
if (Player.Level.TimeSeconds == CrouchController.TimeStamp)
return !CrouchController.bIsBlocked;
if (Player.CollisionHeight >= TargetCollisionHeight)
return true;
if (!Player.bCollideActors && !Player.bCollideWorld)
return true;
A speculative move operation is relatively slow, so it makes sense to avoid unnecessary calculations, when the result can be evaluated using more simple methods. Firstly, CollisionHeight most likely won't be changed many times during the same tick, so we can cache the result of CanStandUp and use it within the same tick later. Secondly, if all collision between the player and other objects is disabled (unusual case), the move operation would succeed anyway.
CrouchController.SetCollision(false, false, false);
CrouchController.SetCollisionSize(Player.CollisionRadius, Player.CollisionHeight);
CrouchController.bCollideWorld = false;
CrouchController.Move(Player.Location - CrouchController.Location);
if (Player.bBlockActors)
CrouchController.SetCollision(true, false, false);
CrouchController.bCollideWorld = Player.bCollideWorld;
CrouchController.bIsBlocked = false;
CrouchController.BumpedActor = none;
DstLocation = Player.Location + vect(0, 0, 2) * (TargetCollisionHeight - Player.CollisionHeight);
// Checking if the player would hit non-Brush actors, Brush actors, or the level geometry
CrouchController.Move(DstLocation - CrouchController.Location);
if (CrouchController.bIsBlocked)
return CrouchController.CanStandUp_Cache(false);
while (
CrouchController.Location != DstLocation &&
CrouchController.BumpedActor != none &&
CrouchController.BumpedActor.bCollideActors &&
!CrouchController.BumpedActor.bBlockPlayers &&
i < 16)
{
CrouchController.BumpedActor.SetCollision(false, CrouchController.BumpedActor.bBlockActors, false);
CrouchController.Move(DstLocation - CrouchController.Location);
CrouchController.BumpedActor.SetCollision(true, CrouchController.BumpedActor.bBlockActors, false);
if (CrouchController.bIsBlocked)
return CrouchController.CanStandUp_Cache(false);
CrouchController.BumpedActor = none;
++i;
}
return CrouchController.CanStandUp_Cache(CrouchController.Location == DstLocation);
}
All this complicated stuff is supposed to calculate what entities would block the player if the player tried to stand up. Blocking PlayerPawns by other actors is implemented in a special way, so simulating its movement with an aux actor is a rather tricky thing. Such tricks would be unnecessary if we had a function like this:
// Effects: the function determines what would happen if Move(Delta) was called instead:
// - the return value is the same as would be returned by Move(Delta);
// - if FromLocation is not specified, the initial location is assumed to be the current Location;
// otherwise, the initial location before the move operation is assumed to be FromLocation;
// - ResultingLocation is equal to the value that the actor's Location would have after calling Move(Delta);
// - if during the move operation the actor would be blocked by the level geometry or other actor,
// then BlockedByActor is set to Level or that blocking actor correspondingly;
// otherwise, BlockedByActor is set to none.
// Note: the function has no side effects; in particular, it does not invoke events Touch/UnTouch, ZoneChange, etc.
//
native final function bool SpeculativeMove(vector Delta, optional vector FromLocation, optional out vector ResultingLocation, optional out Actor BlockedByActor);
This is how CanStandUp could be implemented then:
static final function bool CanStandUp(PlayerPawn Player, float TargetCollisionHeight)
{
local RealCrouchController CrouchController;
local vector TopOffset;
local Actor BlockedBy;
CrouchController = GetRealCrouchController(Player);
if (CrouchController == none) // should be always false
return true;
if (Player.Level.TimeSeconds == CrouchController.TimeStamp)
return !CrouchController.bIsBlocked;
if (Player.CollisionHeight >= TargetCollisionHeight)
return true;
if (!Player.bCollideActors && !Player.bCollideWorld)
return true;
CrouchController.bIsBlocked = false;
TopOffset = vect(0, 0, 2) * (TargetCollisionHeight - Player.CollisionHeight);
return CrouchController.CanStandUp_Cache(SpeculativeMove(TopOffset,,, BlockedBy) && BlockedBy == none);
}
event Tick(float DeltaTime)
{
local PlayerPawn Player;
Player = PlayerPawn(Owner);
if (Player == none || Player.bDeleteMe)
{
Destroy();
return;
}
if (Player.bIsReducedCrouch != Player.ShouldHaveReducedHeight())
Player.TryToDuck(!Player.bIsReducedCrouch);
}
This function periodically checks if the player should be forced to crouch or should try to stand up and adjust the player correspondingly. It's useful mostly in states that differ from PlayerWalking.