├── .gitignore ├── Binaries └── Win64 │ ├── UnrealEditor-PBCharacterMovement.dll │ ├── UnrealEditor-PBCharacterMovement.pdb │ └── UnrealEditor.modules ├── LICENSE ├── PBCharacterMovement.uplugin ├── README.md └── Source └── PBCharacterMovement ├── PBCharacterMovement.Build.cs ├── Private ├── Character │ ├── PBPlayerCharacter.cpp │ └── PBPlayerMovement.cpp └── PBCharacterMovementModule.cpp └── Public ├── Character ├── PBPlayerCharacter.h └── PBPlayerMovement.h ├── PBCharacterMovementModule.h └── Sound └── PBMoveStepSound.h /.gitignore: -------------------------------------------------------------------------------- 1 | Intermediate 2 | *.exp -------------------------------------------------------------------------------- /Binaries/Win64/UnrealEditor-PBCharacterMovement.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectBorealis/PBCharacterMovement/a06250ea7a95f157a8e30a80c9e4bf2264ba7cb0/Binaries/Win64/UnrealEditor-PBCharacterMovement.dll -------------------------------------------------------------------------------- /Binaries/Win64/UnrealEditor-PBCharacterMovement.pdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectBorealis/PBCharacterMovement/a06250ea7a95f157a8e30a80c9e4bf2264ba7cb0/Binaries/Win64/UnrealEditor-PBCharacterMovement.pdb -------------------------------------------------------------------------------- /Binaries/Win64/UnrealEditor.modules: -------------------------------------------------------------------------------- 1 | { 2 | "BuildId": "37670630", 3 | "Modules": 4 | { 5 | "PBCharacterMovement": "UnrealEditor-PBCharacterMovement.dll" 6 | } 7 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2025 Project Borealis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PBCharacterMovement.uplugin: -------------------------------------------------------------------------------- 1 | { 2 | "FileVersion": 3, 3 | "Version": 310, 4 | "VersionName": "3.1.0", 5 | "FriendlyName": "Project Borealis Character Movement", 6 | "Description": "Half-Life 2 style character movement in Unreal Engine.", 7 | "Category": "Other", 8 | "CreatedBy": "Project Borealis", 9 | "CreatedByURL": "https://www.projectborealis.com", 10 | "CanContainContent": false, 11 | "Modules": [ 12 | { 13 | "Name": "PBCharacterMovement", 14 | "Type": "Runtime", 15 | "LoadingPhase": "Default" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PBCharacterMovement 2 | 3 | Project Borealis character movement component. 4 | 5 | Includes all your standard classic FPS movement from HL2: 6 | 7 | * Strafe bunnyhopping 8 | * Accelerated back hopping (and forward and back hopping) 9 | * Strafe boosting 10 | * Circle strafing 11 | * Surfing 12 | * Ramp sliding/trimping/collision boosting 13 | * Wall strafing 14 | * Smooth crouching and uncrouching 15 | * Crouch jumping 16 | * Optional pogo jumping (automatic bunnyhopping): `move.Pogo` cvar 17 | * Optional forward bunnyhopping: `move.Bunnyhopping` cvar 18 | 19 | More info in this blog post: https://www.projectborealis.com/movement. 20 | 21 | ## Binaries 22 | 23 | Binaries are compiled for `5.5`, and will work on C++ projects. 24 | 25 | If you have a Blueprint project, you must upgrade to a C++ project, or else the game will fail to package. 26 | 27 | If you are using a different version of Unreal Engine, you will need to recompile the plugin. Versions prior to `5.5` may require additional code changes. We are happy to accept PRs to improve the compatibility of the plugin. 28 | 29 | # Instructions 30 | 31 | 1. [Download the PBCharacterMovement plugin](https://github.com/ProjectBorealis/PBCharacterMovement/archive/main.zip) and paste it into your project's `Plugins/` folder. 32 | 2. Open your Unreal Engine project. 33 | 3. Add Enhanced Input actions and mappings for forward, right, look up, turn, jump, and crouch. Setting these assets up properly is not covered here, but many tutorials for this exist online. 34 | 4. Create a new player controller in Blueprint or C++. Here's a [simple Blueprint example](https://blueprintue.com/blueprint/mhk2sgn9/). 35 | 5. Create a Blueprint child class of PBPlayerCharacter. 36 | 6. Create a gamemode with Default Pawn set to your Blueprint character class, and Player Controller set to your player controller. 37 | 7. Enjoy the movement! 38 | 39 | ## Gravity 40 | 41 | You may also want to use HL2 gravity settings. Go to Settings > Project Settings > Engine > Physics > Constants > Default Gravity Z and set it to `-1143`. 42 | 43 | The player movement will automatically adjust its own gravity scale to account for any differences between the gravity Z in your project and HL2, so 44 | it's fine if you want a different gravity for your physics objects, and retain HL2 player gravity for fall speeds and jump heights. However, if you want 45 | to fully use your own gravity instead of HL2 gravity, you can define `USE_HL2_GRAVITY=0` 46 | 47 | ## Physics materials 48 | 49 | Additionally, your default physics material should have a friction of `0.8` and restitution of `0.25` if you want Source defaults. 50 | 51 | You can put this in your `Config/DefaultEngine.ini` file to do so: 52 | 53 | ```ini 54 | [SystemSettings] 55 | p.DefaultCollisionFriction=0.8 56 | p.DefaultCollisionRestitution=0.25 57 | ``` 58 | 59 | ## Move Speeds and Ladder 60 | 61 | Our ladder movement code and sprinting speed logic is game specific and is not publicly redistributed at this time. However, we do have stubs for you to insert your own logic here. 62 | 63 | You can call `SetSprinting` and `SetWantsToWalk` for sprint and walk speed respectively. You can set `SprintSpeed`, `WalkSpeed` to set speeds for those modes, and set `RunSpeed` for the default speed. 64 | 65 | Ladder movement code is a bit less complete due to reliance on ladder entity data to determine various aspects of the movement like direction and state. Integrating your own ladder code is outside 66 | the scope of this document, and is left as an exercise for the reader. 67 | 68 | ## Jump Boosting 69 | 70 | "Jump boosting", a feature of HL2 that causes the player to get a velocity boost when jumping. This also 71 | causes the accelerated back hopping (ABH) bug which allows players to rapidly move around the map. You can control this feature using the `move.JumpBoost` cvar. `0` will entirely remove this feature, resulting in no velocity boost. `1` is HL2 functionality, with the ABH bug. `2` is HL2 functionality, patched to prevent such exploits. You may want to use `0` or `2` for a multiplayer game. 72 | 73 | ## Forward bunnyhopping 74 | 75 | An older version of HL2 had a bug where the jump boosting feature had no speed limit, and players could repeatedly jump to get successively speed boosts with no special movement like ABH requires. You can enable 76 | the feature from the older engine of HL2 by setting `move.Bunnyhopping 1`. 77 | 78 | ## First person mesh support 79 | 80 | Unreal Engine's character movement component assumes the character's mesh is a third person mesh that is grounded within the world. This affects how it is positioned when crouch hitboxes change, and some other effects. 81 | 82 | Therefore, to assist in implementing first-person games and reduce confusion, we have provided a Mesh1P component you can use for FPS meshes for the player's perspective. You will need to implement your own 83 | game-specific positioning system though, and respond to crouch eye height changes. We recommend to do this on camera update. If you need an example, please refer to the old `ShooterGame` example project from Epic. If you do not want or need this functionality, define `USE_FIRST_PERSON=0`. 84 | 85 | ## Always apply friction 86 | 87 | HL2 movement uses friction to bring the player down to the speed cap. In air, this is not 88 | applied by default. If you would like to limit the player's air speed, set `move.AlwaysApplyFriction 1`. 89 | 90 | ## Crouch sliding 91 | 92 | Experimental support for crouch sliding is provided by setting `bShouldCrouchSlide` to `true`. However, some gameplay effects, like camera shakes, sounds, etc have been stripped due to dependencies on some internal gameplay systems. You can implement this for your own game if wanted. 93 | 94 | ## Directional braking 95 | 96 | HL2 movement only applies braking friction in oppposition to the player's full movement. This may be too slippery when strafing or tapping keys for some games, these games can use directional braking which brakes each direction (forward/back and left/right) independently, allowing for each directional to be opposed by friction with full force. Enable this by defining `DIRECTIONAL_BRAKING=1`. 97 | -------------------------------------------------------------------------------- /Source/PBCharacterMovement/PBCharacterMovement.Build.cs: -------------------------------------------------------------------------------- 1 | // Copyright Project Borealis 2 | 3 | using UnrealBuildTool; 4 | 5 | public class PBCharacterMovement : ModuleRules 6 | { 7 | public PBCharacterMovement(ReadOnlyTargetRules Target) : base(Target) 8 | { 9 | PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; 10 | 11 | PublicDependencyModuleNames.AddRange( 12 | new string[] 13 | { 14 | "Core", 15 | "CoreUObject", 16 | "Engine", 17 | "PhysicsCore" 18 | } 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Source/PBCharacterMovement/Private/Character/PBPlayerCharacter.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Project Borealis 2 | 3 | #include "Character/PBPlayerCharacter.h" 4 | 5 | #include "Engine/DamageEvents.h" 6 | #include "GameFramework/DamageType.h" 7 | #include "Components/CapsuleComponent.h" 8 | #include "HAL/IConsoleManager.h" 9 | #include "Engine/World.h" 10 | #include "Net/UnrealNetwork.h" 11 | 12 | #include "Character/PBPlayerMovement.h" 13 | 14 | #include UE_INLINE_GENERATED_CPP_BY_NAME(PBPlayerCharacter) 15 | 16 | static TAutoConsoleVariable CVarAutoBHop(TEXT("move.Pogo"), 1, TEXT("If holding spacebar should make the player jump whenever possible.\n"), ECVF_Default); 17 | 18 | static TAutoConsoleVariable CVarJumpBoost(TEXT("move.JumpBoost"), 1, TEXT("If the player should boost in a movement direction while jumping.\n0 - disables jump boosting entirely\n1 - boosts in the direction of input, even when moving in another direction\n2 - boosts in the direction of input when moving in the same direction\n"), ECVF_Default); 19 | 20 | static TAutoConsoleVariable CVarBunnyhop(TEXT("move.Bunnyhopping"), 0, TEXT("Enable normal bunnyhopping.\n"), ECVF_Default); 21 | 22 | const float APBPlayerCharacter::CAPSULE_RADIUS = 30.48f; 23 | const float APBPlayerCharacter::CAPSULE_HEIGHT = 137.16f; 24 | 25 | #ifndef USE_FIRST_PERSON 26 | #define USE_FIRST_PERSON 1 27 | #endif 28 | 29 | // Sets default values 30 | APBPlayerCharacter::APBPlayerCharacter(const FObjectInitializer& ObjectInitializer) 31 | : Super(ObjectInitializer.SetDefaultSubobjectClass(CharacterMovementComponentName)) 32 | { 33 | PrimaryActorTick.bCanEverTick = true; 34 | 35 | // Use if you need 1st person mesh, for FPS 36 | // the default character mesh is intended for 3rd person as it has some crouch offset code 37 | // you will need to manage updating the location of this Mesh1P on camera update or similar, 38 | // to be in line with eye height or any other desired location / framing 39 | #if USE_FIRST_PERSON 40 | Mesh1P = ObjectInitializer.CreateDefaultSubobject(this, TEXT("Mesh1P")); 41 | Mesh1P->SetupAttachment(GetCapsuleComponent()); 42 | Mesh1P->bOnlyOwnerSee = true; 43 | Mesh1P->bOwnerNoSee = false; 44 | Mesh1P->bCastDynamicShadow = false; 45 | Mesh1P->VisibilityBasedAnimTickOption = EVisibilityBasedAnimTickOption::OnlyTickPoseWhenRendered; 46 | Mesh1P->PrimaryComponentTick.TickGroup = TG_PrePhysics; 47 | Mesh1P->SetCollisionObjectType(ECC_Pawn); 48 | Mesh1P->SetCollisionEnabled(ECollisionEnabled::NoCollision); 49 | Mesh1P->SetCollisionResponseToAllChannels(ECR_Ignore); 50 | #endif 51 | 52 | GetMesh()->bOnlyOwnerSee = false; 53 | GetMesh()->bOwnerNoSee = true; 54 | GetMesh()->SetCollisionObjectType(ECC_Pawn); 55 | GetMesh()->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); 56 | GetMesh()->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block); 57 | 58 | // Set size for collision capsule 59 | const float HalfHeight = CAPSULE_HEIGHT / 2.0f; 60 | GetCapsuleComponent()->InitCapsuleSize(CAPSULE_RADIUS, HalfHeight); 61 | // Set collision settings 62 | // If you don't have a third person mesh, use this to block traces 63 | //GetCapsuleComponent()->SetCollisionResponseToChannel(ECC_Camera, ECR_Block); 64 | 65 | // set our turn rates for input 66 | BaseTurnRate = 45.0f; 67 | BaseLookUpRate = 45.0f; 68 | 69 | // Camera eye level 70 | DefaultBaseEyeHeight = 121.92f - HalfHeight; 71 | BaseEyeHeight = DefaultBaseEyeHeight; 72 | constexpr float CrouchedHalfHeight = 68.58f / 2.0f; 73 | FullCrouchedEyeHeight = 53.34f; 74 | CrouchedEyeHeight = FullCrouchedEyeHeight - CrouchedHalfHeight; 75 | 76 | // get pointer to movement component 77 | MovementPtr = Cast(ACharacter::GetMovementComponent()); 78 | 79 | // Fall Damage Initializations 80 | // PLAYER_MAX_SAFE_FALL_SPEED 81 | MinSpeedForFallDamage = 1002.9825f; 82 | // PLAYER_FATAL_FALL_SPEED 83 | FatalFallSpeed = 1757.3625f; 84 | // PLAYER_MIN_BOUNCE_SPEED 85 | MinLandBounceSpeed = 329.565f; 86 | 87 | CapDamageMomentumZ = 476.25f; 88 | } 89 | 90 | void APBPlayerCharacter::BeginPlay() 91 | { 92 | // Call the base class 93 | Super::BeginPlay(); 94 | // Max jump time to get to the top of the arc 95 | MaxJumpTime = -4.0f * GetCharacterMovement()->JumpZVelocity / (3.0f * GetCharacterMovement()->GetGravityZ()); 96 | } 97 | 98 | void APBPlayerCharacter::Tick(float DeltaTime) 99 | { 100 | Super::Tick(DeltaTime); 101 | 102 | if (bDeferJumpStop) 103 | { 104 | bDeferJumpStop = false; 105 | Super::StopJumping(); 106 | } 107 | } 108 | 109 | void APBPlayerCharacter::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const 110 | { 111 | Super::GetLifetimeReplicatedProps(OutLifetimeProps); 112 | 113 | // everyone except local owner: flag change is locally instigated 114 | DOREPLIFETIME_CONDITION(APBPlayerCharacter, bIsSprinting, COND_SkipOwner); 115 | DOREPLIFETIME_CONDITION(APBPlayerCharacter, bWantsToWalk, COND_SkipOwner); 116 | } 117 | 118 | void APBPlayerCharacter::ApplyDamageMomentum(float DamageTaken, FDamageEvent const& DamageEvent, APawn* PawnInstigator, AActor* DamageCauser) 119 | { 120 | UDamageType const* const DmgTypeCDO = DamageEvent.DamageTypeClass->GetDefaultObject(); 121 | if (GetCharacterMovement()) 122 | { 123 | FVector ImpulseDir; 124 | 125 | if (IsValid(DamageCauser)) 126 | { 127 | ImpulseDir = (GetActorLocation() - DamageCauser->GetActorLocation()).GetSafeNormal(); 128 | } 129 | else 130 | { 131 | FHitResult HitInfo; 132 | DamageEvent.GetBestHitInfo(this, DamageCauser, HitInfo, ImpulseDir); 133 | } 134 | 135 | const float SizeFactor = (60.96f * 60.96f * 137.16f) / (FMath::Square(GetCapsuleComponent()->GetScaledCapsuleRadius() * 2.0f) * GetCapsuleComponent()->GetScaledCapsuleHalfHeight() * 2.0f); 136 | 137 | float Magnitude = 1.905f * DamageTaken * SizeFactor * 5.0f; 138 | Magnitude = FMath::Min(Magnitude, 1905.0f); 139 | 140 | FVector Impulse = ImpulseDir * Magnitude; 141 | bool const bMassIndependentImpulse = !DmgTypeCDO->bScaleMomentumByMass; 142 | float MassScale = 1.f; 143 | if (!bMassIndependentImpulse && GetCharacterMovement()->Mass > SMALL_NUMBER) 144 | { 145 | MassScale = 1.f / GetCharacterMovement()->Mass; 146 | } 147 | if (CapDamageMomentumZ > 0.f) 148 | { 149 | Impulse.Z = FMath::Min(Impulse.Z * MassScale, CapDamageMomentumZ) / MassScale; 150 | } 151 | 152 | GetCharacterMovement()->AddImpulse(Impulse, bMassIndependentImpulse); 153 | } 154 | } 155 | 156 | void APBPlayerCharacter::ClearJumpInput(float DeltaTime) 157 | { 158 | // Don't clear jump input right away if we're auto hopping or noclipping (holding to go up), or if we are deferring a jump stop 159 | if (CVarAutoBHop.GetValueOnGameThread() != 0 || bAutoBunnyhop || GetCharacterMovement()->bCheatFlying || bDeferJumpStop) 160 | { 161 | return; 162 | } 163 | Super::ClearJumpInput(DeltaTime); 164 | } 165 | 166 | void APBPlayerCharacter::Jump() 167 | { 168 | if (GetCharacterMovement()->IsFalling()) 169 | { 170 | bDeferJumpStop = true; 171 | } 172 | 173 | Super::Jump(); 174 | } 175 | 176 | void APBPlayerCharacter::OnMovementModeChanged(EMovementMode PrevMovementMode, uint8 PrevCustomMode) 177 | { 178 | // UE-COPY: ACharacter::OnMovementModeChanged 179 | if (!bPressedJump) 180 | { 181 | ResetJumpState(); 182 | } 183 | 184 | if (GetCharacterMovement()->IsFalling()) 185 | { 186 | // Record jump force start time for proxies. Allows us to expire the jump even if not continually ticking down a timer. 187 | if (bProxyIsJumpForceApplied) 188 | { 189 | ProxyJumpForceStartedTime = GetWorld()->GetTimeSeconds(); 190 | } 191 | } 192 | else 193 | { 194 | JumpCurrentCount = 0; 195 | JumpKeyHoldTime = 0.0f; 196 | JumpForceTimeRemaining = 0.0f; 197 | // Commented to allow for jumps to retain from falling state (see bDeferJumpStop) 198 | // bWasJumping = false; 199 | } 200 | 201 | K2_OnMovementModeChanged(PrevMovementMode, GetCharacterMovement()->MovementMode, PrevCustomMode, GetCharacterMovement()->CustomMovementMode); 202 | MovementModeChangedDelegate.Broadcast(this, PrevMovementMode, PrevCustomMode); 203 | } 204 | 205 | void APBPlayerCharacter::StopJumping() 206 | { 207 | if (!bDeferJumpStop) 208 | { 209 | Super::StopJumping(); 210 | } 211 | } 212 | 213 | void APBPlayerCharacter::OnJumped_Implementation() 214 | { 215 | const int32 JumpBoost = CVarJumpBoost->GetInt(); 216 | if (MovementPtr->IsOnLadder()) 217 | { 218 | // Implement your own ladder jump off code here 219 | } 220 | else if (GetWorld()->GetTimeSeconds() >= LastJumpBoostTime + MaxJumpTime && JumpBoost) 221 | { 222 | LastJumpBoostTime = GetWorld()->GetTimeSeconds(); 223 | // Boost forward speed on jump 224 | const FVector Facing = GetActorForwardVector(); 225 | // FVector Input = GetLocalRole() == ROLE_AutonomousProxy ? MovementPtr->GetLastInputVector().GetClampedToMaxSize2D(1.0f) * MovementPtr->GetMaxAcceleration() : GetCharacterMovement()->GetCurrentAcceleration(); 226 | // Use input direction 227 | FVector Input = GetCharacterMovement()->GetCurrentAcceleration(); 228 | if (JumpBoost != 1) 229 | { 230 | // Only boost input in the direction of current movement axis. 231 | Input *= FMath::IsNearlyZero(Input.GetSafeNormal2D() | GetCharacterMovement()->Velocity.GetSafeNormal2D()) ? 0.0f : 1.0f; 232 | } 233 | const float ForwardSpeed = Input | Facing; 234 | // Adjust how much the boost is 235 | const float SpeedBoostPerc = bIsSprinting || bIsCrouched ? 0.1f : 0.5f; 236 | // How much we are boosting by 237 | float SpeedAddition = FMath::Abs(ForwardSpeed * SpeedBoostPerc); 238 | // We can only boost up to this much 239 | const float MaxBoostedSpeed = GetCharacterMovement()->GetMaxSpeed() * (1.0f + SpeedBoostPerc); 240 | // Calculate new speed 241 | const float NewSpeed = SpeedAddition + GetMovementComponent()->Velocity.Size2D(); 242 | float SpeedAdditionNoClamp = SpeedAddition; 243 | 244 | // Scale the boost down if we are going over 245 | if (NewSpeed > MaxBoostedSpeed) 246 | { 247 | SpeedAddition -= NewSpeed - MaxBoostedSpeed; 248 | } 249 | 250 | const float AccelMagnitude = GetCharacterMovement()->GetCurrentAcceleration().Size2D(); 251 | if (ForwardSpeed < -AccelMagnitude * FMath::Sin(0.6981f)) 252 | { 253 | // Boost backwards if we're going backwards 254 | SpeedAddition *= -1.0f; 255 | SpeedAdditionNoClamp *= -1.0f; 256 | } 257 | 258 | // Boost our velocity 259 | FVector JumpBoostedVel = GetMovementComponent()->Velocity + Facing * SpeedAddition; 260 | float JumpBoostedSizeSq = JumpBoostedVel.SizeSquared2D(); 261 | if (CVarBunnyhop.GetValueOnGameThread() != 0) 262 | { 263 | FVector JumpBoostedUnclampVel = GetMovementComponent()->Velocity + Facing * SpeedAdditionNoClamp; 264 | float JumpBoostedUnclampSizeSq = JumpBoostedUnclampVel.SizeSquared2D(); 265 | if (JumpBoostedUnclampSizeSq > JumpBoostedSizeSq) 266 | { 267 | JumpBoostedVel = JumpBoostedUnclampVel; 268 | JumpBoostedSizeSq = JumpBoostedUnclampSizeSq; 269 | } 270 | } 271 | if (GetMovementComponent()->Velocity.SizeSquared2D() < JumpBoostedSizeSq) 272 | { 273 | GetMovementComponent()->Velocity = JumpBoostedVel; 274 | } 275 | } 276 | } 277 | 278 | void APBPlayerCharacter::ToggleNoClip() 279 | { 280 | MovementPtr->ToggleNoClip(); 281 | } 282 | 283 | float APBPlayerCharacter::GetFallSpeed(bool bAfterLand) 284 | { 285 | // TODO: Handle landing on props & water 286 | return MovementPtr->GetFallSpeed(bAfterLand); 287 | } 288 | 289 | 290 | bool APBPlayerCharacter::CanWalkOn(const FHitResult& Hit) const 291 | { 292 | return MovementPtr->IsWalkable(Hit); 293 | } 294 | 295 | void APBPlayerCharacter::OnCrouch() 296 | { 297 | Crouch(); 298 | } 299 | 300 | void APBPlayerCharacter::OnUnCrouch() 301 | { 302 | UnCrouch(); 303 | } 304 | 305 | void APBPlayerCharacter::CrouchToggle() 306 | { 307 | if (GetCharacterMovement()->bWantsToCrouch) 308 | { 309 | UnCrouch(); 310 | } 311 | else 312 | { 313 | Crouch(); 314 | } 315 | } 316 | 317 | bool APBPlayerCharacter::CanJumpInternal_Implementation() const 318 | { 319 | // UE-COPY: ACharacter::CanJumpInternal_Implementation() 320 | 321 | bool bCanJump = GetCharacterMovement() && GetCharacterMovement()->IsJumpAllowed(); 322 | 323 | if (bCanJump) 324 | { 325 | // Ensure JumpHoldTime and JumpCount are valid. 326 | if (!bWasJumping || GetJumpMaxHoldTime() <= 0.0f) 327 | { 328 | if (JumpCurrentCount == 0 && GetCharacterMovement()->IsFalling()) 329 | { 330 | bCanJump = JumpCurrentCount + 1 < JumpMaxCount; 331 | } 332 | else 333 | { 334 | bCanJump = JumpCurrentCount < JumpMaxCount; 335 | } 336 | } 337 | else 338 | { 339 | // Only consider JumpKeyHoldTime as long as: 340 | // A) We are on the ground 341 | // B) The jump limit hasn't been met OR 342 | // C) The jump limit has been met AND we were already jumping 343 | const bool bJumpKeyHeld = (bPressedJump && JumpKeyHoldTime < GetJumpMaxHoldTime()); 344 | bCanJump = bJumpKeyHeld && 345 | (GetCharacterMovement()->IsMovingOnGround() || (JumpCurrentCount < JumpMaxCount) || (bWasJumping && JumpCurrentCount == JumpMaxCount)); 346 | } 347 | if (GetCharacterMovement()->IsMovingOnGround()) 348 | { 349 | float FloorZ = FVector(0.0f, 0.0f, 1.0f) | GetCharacterMovement()->CurrentFloor.HitResult.ImpactNormal; 350 | float WalkableFloor = GetCharacterMovement()->GetWalkableFloorZ(); 351 | bCanJump &= (FloorZ >= WalkableFloor || FMath::IsNearlyEqual(FloorZ, WalkableFloor)); 352 | } 353 | } 354 | 355 | return bCanJump; 356 | } 357 | 358 | void APBPlayerCharacter::Turn(float Rate) 359 | { 360 | // calculate delta for this frame from the rate information 361 | AddControllerYawInput(Rate); 362 | } 363 | 364 | void APBPlayerCharacter::LookUp(float Rate) 365 | { 366 | // calculate delta for this frame from the rate information 367 | AddControllerPitchInput(Rate); 368 | } 369 | 370 | bool APBPlayerCharacter::IsOnLadder() const 371 | { 372 | // Implement your own ladder code here 373 | return false; 374 | } 375 | 376 | void APBPlayerCharacter::MoveForward(float Val) 377 | { 378 | if (Val != 0.f) 379 | { 380 | // Limit pitch when walking or falling 381 | const bool bLimitRotation = (GetCharacterMovement()->IsMovingOnGround() || GetCharacterMovement()->IsFalling()); 382 | const FRotator Rotation = bLimitRotation ? GetActorRotation() : Controller->GetControlRotation(); 383 | const FVector Direction = FRotationMatrix(Rotation).GetScaledAxis(EAxis::X); 384 | AddMovementInput(Direction, Val); 385 | } 386 | } 387 | 388 | void APBPlayerCharacter::MoveRight(float Val) 389 | { 390 | if (Val != 0.f) 391 | { 392 | const FQuat Rotation = GetActorQuat(); 393 | const FVector Direction = FQuatRotationMatrix(Rotation).GetScaledAxis(EAxis::Y); 394 | AddMovementInput(Direction, Val); 395 | } 396 | } 397 | 398 | void APBPlayerCharacter::MoveUp(float Val) 399 | { 400 | if (Val != 0.f) 401 | { 402 | // Only in noclip 403 | if (!MovementPtr->bCheatFlying) 404 | { 405 | return; 406 | } 407 | 408 | AddMovementInput(FVector::UpVector, Val); 409 | } 410 | } 411 | 412 | void APBPlayerCharacter::TurnAtRate(float Val) 413 | { 414 | // calculate delta for this frame from the rate information 415 | AddControllerYawInput(Val * BaseTurnRate * GetWorld()->GetDeltaSeconds() / GetActorTimeDilation()); 416 | } 417 | 418 | void APBPlayerCharacter::LookUpAtRate(float Val) 419 | { 420 | // calculate delta for this frame from the rate information 421 | AddControllerPitchInput(Val * BaseLookUpRate * GetWorld()->GetDeltaSeconds() / GetActorTimeDilation()); 422 | } 423 | 424 | void APBPlayerCharacter::AddControllerYawInput(float Val) 425 | { 426 | Super::AddControllerYawInput(Val); 427 | } 428 | 429 | void APBPlayerCharacter::AddControllerPitchInput(float Val) 430 | { 431 | Super::AddControllerPitchInput(Val); 432 | } 433 | 434 | void APBPlayerCharacter::RecalculateBaseEyeHeight() 435 | { 436 | const ACharacter* DefaultCharacter = GetClass()->GetDefaultObject(); 437 | const float OldUnscaledHalfHeight = DefaultCharacter->GetCapsuleComponent()->GetUnscaledCapsuleHalfHeight(); 438 | const float CrouchedHalfHeight = GetCharacterMovement()->GetCrouchedHalfHeight(); 439 | const float FullCrouchDiff = OldUnscaledHalfHeight - CrouchedHalfHeight; 440 | const UCapsuleComponent* CharacterCapsule = GetCapsuleComponent(); 441 | const float CurrentUnscaledHalfHeight = CharacterCapsule->GetUnscaledCapsuleHalfHeight(); 442 | const float CurrentAlpha = 1.0f - (CurrentUnscaledHalfHeight - CrouchedHalfHeight) / FullCrouchDiff; 443 | BaseEyeHeight = FMath::Lerp(DefaultCharacter->BaseEyeHeight, CrouchedEyeHeight, SimpleSpline(CurrentAlpha)); 444 | } 445 | 446 | bool APBPlayerCharacter::CanCrouch() const 447 | { 448 | return !GetCharacterMovement()->bCheatFlying && Super::CanCrouch() && !MovementPtr->IsOnLadder(); 449 | } 450 | -------------------------------------------------------------------------------- /Source/PBCharacterMovement/Private/Character/PBPlayerMovement.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Project Borealis 2 | 3 | #include "Character/PBPlayerMovement.h" 4 | 5 | #include "Components/CapsuleComponent.h" 6 | #include "Engine/World.h" 7 | #include "GameFramework/Character.h" 8 | #include "HAL/IConsoleManager.h" 9 | #include "Kismet/GameplayStatics.h" 10 | #include "PhysicalMaterials/PhysicalMaterial.h" 11 | #include "PhysicsEngine/PhysicsSettings.h" 12 | #include "Sound/SoundCue.h" 13 | 14 | #if WITH_EDITOR 15 | #include "DrawDebugHelpers.h" 16 | #endif 17 | 18 | #include "Sound/PBMoveStepSound.h" 19 | 20 | #include UE_INLINE_GENERATED_CPP_BY_NAME(PBPlayerMovement) 21 | 22 | static TAutoConsoleVariable CVarShowPos(TEXT("cl.ShowPos"), 0, TEXT("Show position and movement information.\n"), ECVF_Default); 23 | 24 | static TAutoConsoleVariable CVarAlwaysApplyFriction(TEXT("move.AlwaysApplyFriction"), 0, TEXT("Apply friction, even in air.\n"), ECVF_Default); 25 | 26 | DECLARE_CYCLE_STAT(TEXT("Char StepUp"), STAT_CharStepUp, STATGROUP_Character); 27 | DECLARE_CYCLE_STAT(TEXT("Char PhysFalling"), STAT_CharPhysFalling, STATGROUP_Character); 28 | 29 | // MAGIC NUMBERS 30 | constexpr float JumpVelocity = 266.7f; 31 | const float MAX_STEP_SIDE_Z = 0.08f; // maximum z value for the normal on the vertical side of steps 32 | const float VERTICAL_SLOPE_NORMAL_Z = 0.001f; // Slope is vertical if Abs(Normal.Z) <= this threshold. Accounts for precision problems that sometimes angle 33 | // normals slightly off horizontal for vertical surface. 34 | 35 | #ifndef USE_HL2_GRAVITY 36 | #define USE_HL2_GRAVITY 1 37 | #endif 38 | 39 | // Purpose: override default player movement 40 | UPBPlayerMovement::UPBPlayerMovement() 41 | { 42 | // We have our own air movement handling, so we can allow for full air 43 | // control through UE's logic 44 | AirControl = 1.0f; 45 | // Disable air control boost 46 | AirControlBoostMultiplier = 0.0f; 47 | AirControlBoostVelocityThreshold = 0.0f; 48 | // HL2 cl_(forward & side)speed = 450Hu 49 | MaxAcceleration = 857.25f; 50 | // Set the default walk speed 51 | WalkSpeed = 285.75f; 52 | RunSpeed = 361.9f; 53 | SprintSpeed = 609.6f; 54 | MaxWalkSpeed = RunSpeed; 55 | // Acceleration multipliers (HL2's sv_accelerate and sv_airaccelerate) 56 | GroundAccelerationMultiplier = 10.0f; 57 | AirAccelerationMultiplier = 10.0f; 58 | // 30 air speed cap from HL2 59 | AirSpeedCap = 57.15f; 60 | AirSlideSpeedCap = 57.15f; 61 | // HL2 like friction 62 | // sv_friction 63 | GroundFriction = 4.0f; 64 | BrakingFriction = 4.0f; 65 | SurfaceFriction = 1.0f; 66 | bUseSeparateBrakingFriction = false; 67 | // Edge friction 68 | EdgeFrictionMultiplier = 2.0f; 69 | EdgeFrictionHeight = 64.77f; 70 | EdgeFrictionDist = 30.48f; 71 | bEdgeFrictionOnlyWhenBraking = false; 72 | bEdgeFrictionAlwaysWhenCrouching = false; 73 | // No multiplier 74 | BrakingFrictionFactor = 1.0f; 75 | // Historical value for Source 76 | BrakingSubStepTime = 1 / 66.0f; 77 | // Time step 78 | MaxSimulationTimeStep = 1 / 66.0f; 79 | MaxSimulationIterations = 25; 80 | MaxJumpApexAttemptsPerSimulation = 4; 81 | // Braking deceleration (sv_stopspeed) 82 | FallingLateralFriction = 0.0f; 83 | BrakingDecelerationFalling = 0.0f; 84 | BrakingDecelerationFlying = 190.5f; 85 | BrakingDecelerationSwimming = 190.5f; 86 | BrakingDecelerationWalking = 190.5f; 87 | // HL2 step height 88 | MaxStepHeight = 34.29f; 89 | DefaultStepHeight = MaxStepHeight; 90 | // Step height scaling due to speed 91 | MinStepHeight = 10.0f; 92 | StepDownHeightFraction = 0.9f; 93 | // Perching 94 | // We try to avoid going too broad with perching as it can cause a sliding issue with jumping on edges 95 | PerchRadiusThreshold = 0.5f; // 0.5 is the minimum value to prevent snags 96 | PerchAdditionalHeight = 0.0f; 97 | // Jump z from HL2's 160Hu 98 | // 21Hu jump height 99 | // 510ms jump time 100 | JumpZVelocity = 304.8f; 101 | // Don't bounce off characters 102 | JumpOffJumpZFactor = 0.0f; 103 | // Default show pos to false 104 | bShowPos = false; 105 | // We aren't on a ladder at first 106 | bOnLadder = false; 107 | OffLadderTicks = LADDER_MOUNT_TIMEOUT; 108 | LadderSpeed = 381.0f; 109 | // Speed multiplier bounds 110 | SpeedMultMin = SprintSpeed * 1.7f; 111 | SpeedMultMax = SprintSpeed * 2.5f; 112 | DefaultSpeedMultMin = SpeedMultMin; 113 | DefaultSpeedMultMax = SpeedMultMax; 114 | // Start out braking 115 | bBrakingFrameTolerated = true; 116 | BrakingWindowTimeElapsed = 0.0f; 117 | BrakingWindow = 0.015f; 118 | // Crouching 119 | SetCrouchedHalfHeight(34.29f); 120 | MaxWalkSpeedCrouched = RunSpeed * 0.33333333f; 121 | bCanWalkOffLedgesWhenCrouching = true; 122 | CrouchTime = MOVEMENT_DEFAULT_CROUCHTIME; 123 | UncrouchTime = MOVEMENT_DEFAULT_UNCROUCHTIME; 124 | CrouchJumpTime = MOVEMENT_DEFAULT_CROUCHJUMPTIME; 125 | UncrouchJumpTime = MOVEMENT_DEFAULT_UNCROUCHJUMPTIME; 126 | // Slope angle is 45.57 degrees 127 | SetWalkableFloorZ(0.7f); 128 | DefaultWalkableFloorZ = GetWalkableFloorZ(); 129 | AxisSpeedLimit = 6667.5f; 130 | // Tune physics interactions 131 | StandingDownwardForceScale = 1.0f; 132 | // Just push all objects based on their impact point 133 | // it might be weird with a lot of dev objects due to scale, but 134 | // it's much more realistic. 135 | bPushForceUsingZOffset = false; 136 | PushForcePointZOffsetFactor = -0.66f; 137 | // Scale push force down if we are slow 138 | bScalePushForceToVelocity = true; 139 | // Don't push more if there's more mass 140 | bPushForceScaledToMass = false; 141 | bTouchForceScaledToMass = false; 142 | Mass = 85.0f; // player.mdl is 85kg 143 | // Don't smooth rotation at all 144 | bUseControllerDesiredRotation = false; 145 | // Flat base 146 | bUseFlatBaseForFloorChecks = true; 147 | // Agent props 148 | NavAgentProps.bCanCrouch = true; 149 | NavAgentProps.bCanJump = true; 150 | NavAgentProps.bCanFly = true; 151 | // Crouch sliding 152 | bShouldCrouchSlide = false; 153 | CrouchSlideBoostTime = 0.1f; 154 | CrouchSlideBoostMultiplier = 1.5f; 155 | CrouchSlideSpeedRequirementMultiplier = 0.9f; 156 | MinCrouchSlideBoost = SprintSpeed * CrouchSlideBoostMultiplier; 157 | MaxCrouchSlideVelocityBoost = 6.0f; 158 | MinCrouchSlideVelocityBoost = 2.7f; 159 | CrouchSlideBoostSlopeFactor = 2.7f; 160 | CrouchSlideCooldown = 1.0f; 161 | #if USE_HL2_GRAVITY 162 | // Make sure gravity is correct for player movement 163 | GravityScale = DesiredGravity / UPhysicsSettings::Get()->DefaultGravityZ; 164 | #endif 165 | // Make sure ramp movement in correct 166 | bMaintainHorizontalGroundVelocity = true; 167 | bAlwaysCheckFloor = true; 168 | // Ignore base rotation 169 | // TODO: might do well to ONLY ignore base rotation if our base is simulating physics. 170 | // but the player might want control of their rotation ALWAYS. 171 | bIgnoreBaseRotation = true; 172 | bBasedMovementIgnorePhysicsBase = true; 173 | // Physics interactions 174 | bEnablePhysicsInteraction = true; 175 | RepulsionForce = 1.314f; 176 | MaxTouchForce = 100.0f; 177 | InitialPushForceFactor = 10.0f; 178 | PushForceFactor = 100000.0f; 179 | // Swimming 180 | Buoyancy = 0.99f; 181 | // Allow orient rotation during root motion 182 | bAllowPhysicsRotationDuringAnimRootMotion = true; 183 | // Prevent NaN 184 | RequestedVelocity = FVector::ZeroVector; 185 | // Optimization 186 | bEnableServerDualMoveScopedMovementUpdates = true; 187 | } 188 | 189 | void UPBPlayerMovement::InitializeComponent() 190 | { 191 | Super::InitializeComponent(); 192 | PBPlayerCharacter = Cast(CharacterOwner); 193 | 194 | // Get defaults from BP 195 | MaxWalkSpeed = RunSpeed; 196 | if (SpeedMultMin == DefaultSpeedMultMin) 197 | { 198 | // only update if not already customized in BP 199 | SpeedMultMin = SprintSpeed * 1.7f; 200 | } 201 | if (SpeedMultMax == DefaultSpeedMultMax) 202 | { 203 | // only update if not already customized in BP 204 | SpeedMultMax = SprintSpeed * 2.5f; 205 | } 206 | DefaultStepHeight = MaxStepHeight; 207 | DefaultWalkableFloorZ = GetWalkableFloorZ(); 208 | } 209 | 210 | void UPBPlayerMovement::OnRegister() 211 | { 212 | Super::OnRegister(); 213 | 214 | const bool bIsReplay = (GetWorld() && GetWorld()->IsPlayingReplay()); 215 | if (!bIsReplay && GetNetMode() == NM_ListenServer) 216 | { 217 | NetworkSmoothingMode = ENetworkSmoothingMode::Linear; 218 | } 219 | } 220 | 221 | void UPBPlayerMovement::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) 222 | { 223 | Super::TickComponent(DeltaTime, TickType, ThisTickFunction); 224 | 225 | PlayMoveSound(DeltaTime); 226 | 227 | if (bHasDeferredMovementMode) 228 | { 229 | SetMovementMode(DeferredMovementMode); 230 | bHasDeferredMovementMode = false; 231 | } 232 | 233 | // Skip player movement when we're simulating physics (ie ragdoll) 234 | if (UpdatedComponent->IsSimulatingPhysics()) 235 | { 236 | return; 237 | } 238 | 239 | if (bShowPos || CVarShowPos.GetValueOnGameThread() != 0) 240 | { 241 | const FVector Position = UpdatedComponent->GetComponentLocation(); 242 | const FRotator Rotation = CharacterOwner->GetControlRotation(); 243 | const float Speed = Velocity.Size(); 244 | GEngine->AddOnScreenDebugMessage(1, 1.0f, FColor::Green, FString::Printf(TEXT("pos: %.02f %.02f %.02f"), Position.X, Position.Y, Position.Z)); 245 | GEngine->AddOnScreenDebugMessage(2, 1.0f, FColor::Green, FString::Printf(TEXT("ang: %.02f %.02f %.02f"), Rotation.Pitch, Rotation.Yaw, Rotation.Roll)); 246 | GEngine->AddOnScreenDebugMessage(3, 1.0f, FColor::Green, FString::Printf(TEXT("vel: %.02f"), Speed)); 247 | } 248 | 249 | if (RollAngle != 0 && RollSpeed != 0 && GetPBCharacter()->GetController()) 250 | { 251 | FRotator ControlRotation = GetPBCharacter()->GetController()->GetControlRotation(); 252 | ControlRotation.Roll = GetCameraRoll(); 253 | GetPBCharacter()->GetController()->SetControlRotation(ControlRotation); 254 | } 255 | 256 | if (IsMovingOnGround()) 257 | { 258 | if (!bBrakingFrameTolerated) 259 | { 260 | BrakingWindowTimeElapsed += DeltaTime; 261 | if (BrakingWindowTimeElapsed >= BrakingWindow) 262 | { 263 | bBrakingFrameTolerated = true; 264 | } 265 | } 266 | } 267 | else 268 | { 269 | bBrakingFrameTolerated = false; 270 | BrakingWindowTimeElapsed = 0.0f; // Clear window 271 | } 272 | bCrouchFrameTolerated = IsCrouching(); 273 | } 274 | 275 | bool UPBPlayerMovement::DoJump(bool bClientSimulation) 276 | { 277 | // UE-COPY: UCharacterMovementComponent::DoJump(bool bReplayingMoves) 278 | 279 | if (!bCheatFlying && CharacterOwner && CharacterOwner->CanJump()) 280 | { 281 | // Don't jump if we can't move up/down. 282 | if (!bConstrainToPlane || !FMath::IsNearlyEqual(FMath::Abs(GetGravitySpaceZ(PlaneConstraintNormal)), 1.f)) 283 | { 284 | // If first frame of DoJump, we want to always inject the initial jump velocity. 285 | // For subsequent frames, during the time Jump is held, it depends... 286 | // bDontFallXXXX == true means we want to ensure the character's Z velocity is never less than JumpZVelocity in this period 287 | // bDontFallXXXX == false means we just want to leave Z velocity alone and "let the chips fall where they may" (e.g. fall properly in physics) 288 | 289 | // NOTE: 290 | // Checking JumpCurrentCountPreJump instead of JumpCurrentCount because Character::CheckJumpInput might have 291 | // incremented JumpCurrentCount just before entering this function... in order to compensate for the case when 292 | // on the first frame of the jump, we're already in falling stage. So we want the original value before any 293 | // modification here. 294 | // 295 | const bool bFirstJump = (CharacterOwner->JumpCurrentCountPreJump == 0); 296 | 297 | if (bFirstJump || bDontFallBelowJumpZVelocityDuringJump) 298 | { 299 | const int32 NewJumps = CharacterOwner->JumpCurrentCountPreJump + 1; 300 | if (IsFalling() && GetCharacterOwner()->JumpMaxCount > 1 && NewJumps <= GetCharacterOwner()->JumpMaxCount) 301 | { 302 | if (bAirJumpResetsHorizontal) 303 | { 304 | Velocity.X = 0.0f; 305 | Velocity.Y = 0.0f; 306 | } 307 | FVector InputVector = GetCharacterOwner()->GetPendingMovementInputVector() + GetLastInputVector(); 308 | InputVector = InputVector.GetSafeNormal2D(); 309 | Velocity += InputVector * GetMaxAcceleration() * AirJumpDashMagnitude; 310 | OnAirJump(NewJumps); 311 | } 312 | if (HasCustomGravity()) 313 | { 314 | if (GetGravitySpaceZ(Velocity) < 0.0f) 315 | { 316 | SetGravitySpaceZ(Velocity, 0.0f); 317 | } 318 | SetGravitySpaceZ(Velocity, GetGravitySpaceZ(Velocity) + JumpZVelocity); 319 | } 320 | else 321 | { 322 | if (Velocity.Z < 0.0f) 323 | { 324 | Velocity.Z = 0.0f; 325 | } 326 | 327 | Velocity.Z += FMath::Max(Velocity.Z, JumpZVelocity); 328 | } 329 | } 330 | 331 | SetMovementMode(MOVE_Falling); 332 | return true; 333 | } 334 | } 335 | 336 | return false; 337 | } 338 | 339 | float UPBPlayerMovement::GetFallSpeed(bool bAfterLand) 340 | { 341 | FVector FallVelocity = Velocity; 342 | if (bAfterLand) 343 | { 344 | const float GravityStep = GetGravityZ() * GetWorld()->GetDeltaSeconds() * 0.5f; 345 | // we need to do another integration step of gravity before we get fall speed 346 | if (HasCustomGravity()) 347 | { 348 | SetGravitySpaceZ(FallVelocity, GetGravitySpaceZ(FallVelocity) + GravityStep); 349 | } 350 | else 351 | { 352 | FallVelocity.Z += GravityStep; 353 | } 354 | } 355 | return -FallVelocity.Z; 356 | } 357 | 358 | void UPBPlayerMovement::TwoWallAdjust(FVector& Delta, const FHitResult& Hit, const FVector& OldHitNormal) const 359 | { 360 | Super::TwoWallAdjust(Delta, Hit, OldHitNormal); 361 | } 362 | 363 | float UPBPlayerMovement::SlideAlongSurface(const FVector& Delta, float Time, const FVector& Normal, FHitResult& Hit, bool bHandleImpact) 364 | { 365 | return Super::SlideAlongSurface(Delta, Time, Normal, Hit, bHandleImpact); 366 | } 367 | 368 | FVector UPBPlayerMovement::ComputeSlideVector(const FVector& Delta, const float Time, const FVector& Normal, const FHitResult& Hit) const 369 | { 370 | return Super::ComputeSlideVector(Delta, Time, Normal, Hit); 371 | } 372 | 373 | FVector UPBPlayerMovement::HandleSlopeBoosting(const FVector& SlideResult, const FVector& Delta, const float Time, const FVector& Normal, const FHitResult& Hit) const 374 | { 375 | if (IsOnLadder() || bCheatFlying) 376 | { 377 | return Super::HandleSlopeBoosting(SlideResult, Delta, Time, Normal, Hit); 378 | } 379 | const float WallAngle = FMath::Abs(Hit.ImpactNormal.Z); 380 | FVector ImpactNormal = Normal; 381 | // If too extreme, use the more stable hit normal 382 | if (!(WallAngle <= VERTICAL_SLOPE_NORMAL_Z || WallAngle == 1.0f)) 383 | { 384 | // Only use new normal if it isn't higher Z, to avoid moving higher than intended. 385 | // Similar to how ZLimit works in the Super implementation of this function. 386 | // Second check: if we ARE going for a lower impact normal, make sure it's 387 | // not in conflict with our delta. If the movement is pushing us up, we want to 388 | // slide upwards, rather than get pushed back down. 389 | if (Hit.ImpactNormal.Z <= ImpactNormal.Z && Delta.Z <= 0.0f) 390 | { 391 | ImpactNormal = Hit.ImpactNormal; 392 | } 393 | } 394 | if (bConstrainToPlane) 395 | { 396 | ImpactNormal = ConstrainNormalToPlane(ImpactNormal); 397 | } 398 | const float BounceCoefficient = 1.0f + BounceMultiplier * (1.0f - SurfaceFriction); 399 | return (Delta - BounceCoefficient * Delta.ProjectOnToNormal(ImpactNormal)) * Time; 400 | } 401 | 402 | bool UPBPlayerMovement::ShouldCatchAir(const FFindFloorResult& OldFloor, const FFindFloorResult& NewFloor) 403 | { 404 | // If the new floor is below the old floor by fraction of max step height, catch air 405 | const float HeightDiff = NewFloor.HitResult.ImpactPoint.Z - OldFloor.HitResult.ImpactPoint.Z; 406 | if (HeightDiff < -MaxStepHeight * StepDownHeightFraction) 407 | { 408 | return true; 409 | } 410 | 411 | // Get surface friction 412 | const float OldSurfaceFriction = GetFrictionFromHit(OldFloor.HitResult); 413 | 414 | // As we get faster, make our speed multiplier smaller (so it scales with smaller friction) 415 | const float SpeedMult = SpeedMultMax / Velocity.Size2D(); 416 | const bool bSliding = OldSurfaceFriction * SpeedMult < 0.5f; 417 | 418 | // See if we got less steep or are continuing at the same slope 419 | const float ZDiff = NewFloor.HitResult.ImpactNormal.Z - OldFloor.HitResult.ImpactNormal.Z; 420 | const bool bGainingRamp = ZDiff >= 0.0f; 421 | 422 | // Velocity is always horizontal. Therefore, if we are moving up a ramp, we get >90 deg angle with the normal 423 | // This results in a negative cos. This also checks if our old floor was ramped at all, because a flat floor wouldn't pass this check. 424 | const float Slope = Velocity | OldFloor.HitResult.ImpactNormal; 425 | const bool bWasGoingUpRamp = Slope < 0.0f; 426 | 427 | // Finally, we want to also handle the case of strafing off of a ramp, so check if they're strafing. 428 | const float StrafeMovement = FMath::Abs(GetLastInputVector() | GetOwner()->GetActorRightVector()); 429 | const bool bStrafingOffRamp = StrafeMovement > 0.0f; 430 | 431 | // So, our only relevant conditions are when we are going up a ramp or strafing off of it. 432 | const bool bMovingForCatchAir = bWasGoingUpRamp || bStrafingOffRamp; 433 | 434 | if (bSliding && bGainingRamp && bMovingForCatchAir) 435 | { 436 | return true; 437 | } 438 | 439 | return Super::ShouldCatchAir(OldFloor, NewFloor); 440 | } 441 | 442 | bool UPBPlayerMovement::IsWithinEdgeTolerance(const FVector& CapsuleLocation, const FVector& TestImpactPoint, const float CapsuleRadius) const 443 | { 444 | return Super::IsWithinEdgeTolerance(CapsuleLocation, TestImpactPoint, CapsuleRadius); 445 | } 446 | 447 | bool UPBPlayerMovement::ShouldCheckForValidLandingSpot(float DeltaTime, const FVector& Delta, const FHitResult& Hit) const 448 | { 449 | // TODO: check for flat base valid landing spots? at the moment this check is too generous for the capsule hemisphere 450 | #if 0 451 | return !bUseFlatBaseForFloorChecks && Super::ShouldCheckForValidLandingSpot(DeltaTime, Delta, Hit); 452 | #else 453 | return Super::ShouldCheckForValidLandingSpot(DeltaTime, Delta, Hit); 454 | #endif 455 | } 456 | 457 | void UPBPlayerMovement::HandleImpact(const FHitResult& Hit, float TimeSlice, const FVector& MoveDelta) 458 | { 459 | Super::HandleImpact(Hit, TimeSlice, MoveDelta); 460 | if (TimeSlice > 0.0f && MoveDelta != FVector::ZeroVector && MoveDelta.Z) 461 | { 462 | UpdateSurfaceFriction(true); 463 | } 464 | } 465 | 466 | bool UPBPlayerMovement::IsValidLandingSpot(const FVector& CapsuleLocation, const FHitResult& Hit) const 467 | { 468 | if (!Super::IsValidLandingSpot(CapsuleLocation, Hit)) 469 | { 470 | return false; 471 | } 472 | 473 | // Slope bug fix 474 | // If moving up a slope... 475 | if (Hit.Normal.Z < 1.0f && (Velocity | Hit.Normal) < 0.0f) 476 | { 477 | // Let's calculate how we are gonna deflect off the surface 478 | FVector DeflectionVector = Velocity; 479 | // a step of gravity 480 | DeflectionVector.Z += 0.5f * GetGravityZ() * GetWorld()->GetDeltaSeconds(); 481 | DeflectionVector = ComputeSlideVector(DeflectionVector, 1.0f, Hit.Normal, Hit); 482 | 483 | // going up too fast to land 484 | if (DeflectionVector.Z > JumpVelocity) 485 | { 486 | return false; 487 | } 488 | } 489 | 490 | return true; 491 | } 492 | 493 | void UPBPlayerMovement::OnMovementModeChanged(EMovementMode PreviousMovementMode, uint8 PreviousCustomMode) 494 | { 495 | // Reset step side if we are changing modes 496 | StepSide = false; 497 | 498 | // did we jump or land 499 | bool bJumped = false; 500 | bool bQueueJumpSound = false; 501 | 502 | // We reset landed state if we switch to a disabled mode. Flying mode should be okay though. 503 | if (MovementMode == MOVE_None) 504 | { 505 | bHasEverLanded = false; 506 | } 507 | 508 | if (PreviousMovementMode == MOVE_Walking && MovementMode == MOVE_Falling) 509 | { 510 | // If we were walking and now falling, we could be jumping. 511 | bJumped = true; 512 | // Only if moving up do we play the jump sound effect. 513 | bQueueJumpSound = Velocity.Z > 0.0f; 514 | } 515 | else if (PreviousMovementMode == MOVE_Falling && MovementMode == MOVE_Walking) 516 | { 517 | // We want to queue a jump sound here even if we haven't ever landed yet. 518 | // Since we're in walking state (from falling), we can now do our check for ground to see if we did land. 519 | bQueueJumpSound = true; 520 | if (bDeferCrouchSlideToLand) 521 | { 522 | bDeferCrouchSlideToLand = false; 523 | StartCrouchSlide(); 524 | } 525 | } 526 | 527 | // Noclip goes from: flying -> falling -> walking because of default movement modes 528 | if (bHasDeferredMovementMode) 529 | { 530 | bQueueJumpSound = false; 531 | } 532 | 533 | // In some cases, we don't play a jump sound because it's either not queued, or we haven't ever landed. 534 | // in BOTH cases, we still want to handle detecting the first land. because in some cases we're 535 | // moving from move mode = NONE 536 | bool bDidPlayJumpSound = false; 537 | 538 | // If we are moving between falling/walking, and we meet conditions for playing a sound in our state. 539 | if (bQueueJumpSound) 540 | { 541 | // If we're intentionally falling off of spawn, then we want to play the land sound 542 | if (!bHasEverLanded) 543 | { 544 | if (GetOwner()->GetGameTimeSinceCreation() > 0.1f) 545 | { 546 | bHasEverLanded = true; 547 | } 548 | } 549 | if (bHasEverLanded) 550 | { 551 | // If we have found an initial ground from when we did our initial player spawn, we can play a sound. 552 | FHitResult Hit; 553 | TraceCharacterFloor(Hit); 554 | PlayJumpSound(Hit, bJumped); 555 | bDidPlayJumpSound = true; 556 | } 557 | } 558 | 559 | // This needs to be here AFTER PlayJumpSound or else our velocity Z gets reset to 0 before we do land sound. 560 | Super::OnMovementModeChanged(PreviousMovementMode, PreviousCustomMode); 561 | 562 | if (!bDidPlayJumpSound) 563 | { 564 | if (MovementMode == MOVE_Walking && (GetMovementBase() || CurrentFloor.bBlockingHit)) 565 | { 566 | // This happens in a couple of cases. 567 | // First, on initial player spawn, we default to walking. 568 | // But then we transition to falling immediately after if GetMovementBase() is null. (See SetDefaultMovementMode()) 569 | // So, that walking -> falling transition would be invalid for a jump sound since it's just categorizing initial position in the air. 570 | // Second, once we are falling (when our player spawn is slightly above the ground), we are going to land. 571 | // This is generally because the player spawn was placed not accurately to the floor, which is reasonable. 572 | // But we don't want to play a land sound effect just because our player spawned! 573 | bHasEverLanded = true; 574 | } 575 | } 576 | } 577 | 578 | float UPBPlayerMovement::GetCameraRoll() 579 | { 580 | if (RollSpeed == 0.0f || RollAngle == 0.0f) 581 | { 582 | return 0.0f; 583 | } 584 | float Side = Velocity | FRotationMatrix(GetCharacterOwner()->GetControlRotation()).GetScaledAxis(EAxis::Y); 585 | const float Sign = FMath::Sign(Side); 586 | Side = FMath::Abs(Side); 587 | if (Side < RollSpeed) 588 | { 589 | Side = Side * RollAngle / RollSpeed; 590 | } 591 | else 592 | { 593 | Side = RollAngle; 594 | } 595 | return Side * Sign; 596 | } 597 | 598 | bool UPBPlayerMovement::IsOnLadder() const 599 | { 600 | return bOnLadder; 601 | } 602 | 603 | float UPBPlayerMovement::GetLadderClimbSpeed() const 604 | { 605 | return LadderSpeed; 606 | } 607 | 608 | void UPBPlayerMovement::SetNoClip(bool bNoClip) 609 | { 610 | // We need to defer movement in case we set this outside of main game thread loop, since character movement resets movement back in tick. 611 | if (bNoClip) 612 | { 613 | SetMovementMode(MOVE_Flying); 614 | DeferredMovementMode = MOVE_Flying; 615 | bCheatFlying = true; 616 | GetCharacterOwner()->SetActorEnableCollision(false); 617 | } 618 | else 619 | { 620 | SetMovementMode(MOVE_Walking); 621 | DeferredMovementMode = MOVE_Walking; 622 | bCheatFlying = false; 623 | GetCharacterOwner()->SetActorEnableCollision(true); 624 | } 625 | bHasDeferredMovementMode = true; 626 | } 627 | 628 | void UPBPlayerMovement::ToggleNoClip() 629 | { 630 | SetNoClip(!bCheatFlying); 631 | } 632 | 633 | #ifndef DIRECTIONAL_BRAKING 634 | #define DIRECTIONAL_BRAKING 0 635 | #endif 636 | 637 | void UPBPlayerMovement::ApplyVelocityBraking(float DeltaTime, float Friction, float BrakingDeceleration) 638 | { 639 | // UE4-COPY: void UCharacterMovementComponent::ApplyVelocityBraking(float DeltaTime, float Friction, float BrakingDeceleration) 640 | if (Velocity.IsNearlyZero(0.1f) || !HasValidData() || HasAnimRootMotion() || DeltaTime < MIN_TICK_TIME) 641 | { 642 | return; 643 | } 644 | 645 | #if DIRECTIONAL_BRAKING 646 | const float ForwardSpeed = FMath::Abs(Velocity | UpdatedComponent->GetForwardVector()); 647 | const float SideSpeed = FMath::Abs(Velocity | UpdatedComponent->GetRightVector()); 648 | #else 649 | const float Speed = Velocity.Size2D(); 650 | #endif 651 | const float FrictionFactor = FMath::Max(0.0f, BrakingFrictionFactor); 652 | Friction = FMath::Max(0.0f, Friction * FrictionFactor); 653 | #if DIRECTIONAL_BRAKING 654 | float ForwardBrakingDeceleration = BrakingDeceleration; 655 | float SideBrakingDeceleration = BrakingDeceleration; 656 | #endif 657 | if (ShouldCrouchSlide()) 658 | { 659 | #if DIRECTIONAL_BRAKING 660 | const float Speed = Velocity.Size2D(); 661 | #endif 662 | if (Friction > 1.0f) 663 | { 664 | float CurrentTime = GetWorld()->GetTimeSeconds(); 665 | float TimeDifference = CurrentTime - CrouchSlideStartTime; 666 | // Decay friction reduction 667 | Friction = FMath::Lerp(1.0f, Friction, FMath::Clamp(TimeDifference / CrouchSlideBoostTime, 0.0f, 1.0f)); 668 | } 669 | BrakingDeceleration = FMath::Max(10.0f, Speed); 670 | #if DIRECTIONAL_BRAKING 671 | ForwardBrakingDeceleration = BrakingDeceleration; 672 | SideBrakingDeceleration = BrakingDeceleration; 673 | #endif 674 | } 675 | else 676 | { 677 | #if DIRECTIONAL_BRAKING 678 | ForwardBrakingDeceleration = FMath::Max3(BrakingDeceleration, ForwardSpeed, 0.0f); 679 | SideBrakingDeceleration = FMath::Max3(BrakingDeceleration, SideSpeed, 0.0f); 680 | #else 681 | BrakingDeceleration = FMath::Max(BrakingDeceleration, Speed); 682 | #endif 683 | } 684 | const bool bZeroFriction = FMath::IsNearlyZero(Friction); 685 | #if DIRECTIONAL_BRAKING 686 | const bool bZeroBraking = ForwardBrakingDeceleration == 0.0f && SideBrakingDeceleration == 0.0f; 687 | #else 688 | const bool bZeroBraking = BrakingDeceleration == 0.0f; 689 | #endif 690 | 691 | if (bZeroFriction || bZeroBraking) 692 | { 693 | return; 694 | } 695 | 696 | const FVector OldVel = Velocity; 697 | 698 | // subdivide braking to get reasonably consistent results at lower frame rates 699 | // (important for packet loss situations w/ networking) 700 | float RemainingTime = DeltaTime; 701 | const float MaxTimeStep = FMath::Clamp(BrakingSubStepTime, 1.0f / 75.0f, 1.0f / 20.0f); 702 | 703 | // Decelerate to brake to a stop 704 | #if DIRECTIONAL_BRAKING 705 | const FVector ForwardRevAccel = -FMath::Sign((Velocity.GetSafeNormal() | UpdatedComponent->GetForwardVector())) * UpdatedComponent->GetForwardVector(); 706 | const FVector SideRevAccel = -FMath::Sign((Velocity.GetSafeNormal() | UpdatedComponent->GetRightVector())) * UpdatedComponent->GetRightVector(); 707 | #else 708 | const FVector RevAccel = -Velocity.GetSafeNormal(); 709 | #endif 710 | while (RemainingTime >= MIN_TICK_TIME) 711 | { 712 | const float Delta = (RemainingTime > MaxTimeStep ? FMath::Min(MaxTimeStep, RemainingTime * 0.5f) : RemainingTime); 713 | RemainingTime -= Delta; 714 | 715 | // apply friction and braking 716 | #if DIRECTIONAL_BRAKING 717 | Velocity += (Friction * ForwardBrakingDeceleration * ForwardRevAccel) * Delta; 718 | Velocity += (Friction * SideBrakingDeceleration * SideRevAccel) * Delta; 719 | #else 720 | Velocity += (Friction * BrakingDeceleration * RevAccel) * Delta; 721 | #endif 722 | 723 | // Don't reverse direction 724 | // TODO: make this directionally separated too? 725 | if ((Velocity | OldVel) <= 0.0f) 726 | { 727 | Velocity = FVector::ZeroVector; 728 | return; 729 | } 730 | } 731 | 732 | // Clamp to zero if nearly zero 733 | if (Velocity.IsNearlyZero(KINDA_SMALL_NUMBER)) 734 | { 735 | Velocity = FVector::ZeroVector; 736 | } 737 | } 738 | 739 | bool UPBPlayerMovement::ShouldLimitAirControl(float DeltaTime, const FVector& FallAcceleration) const 740 | { 741 | return false; 742 | } 743 | 744 | FVector UPBPlayerMovement::NewFallVelocity(const FVector& InitialVelocity, const FVector& Gravity, float DeltaTime) const 745 | { 746 | FVector FallVel = Super::NewFallVelocity(InitialVelocity, Gravity, DeltaTime); 747 | FallVel.Z = FMath::Clamp(FallVel.Z, -AxisSpeedLimit, AxisSpeedLimit); 748 | return FallVel; 749 | } 750 | 751 | void UPBPlayerMovement::UpdateCharacterStateBeforeMovement(float DeltaSeconds) 752 | { 753 | Super::UpdateCharacterStateBeforeMovement(DeltaSeconds); 754 | Velocity.Z = FMath::Clamp(Velocity.Z, -AxisSpeedLimit, AxisSpeedLimit); 755 | // reset value for new frame 756 | bSlidingInAir = false; 757 | UpdateCrouching(DeltaSeconds); 758 | } 759 | 760 | void UPBPlayerMovement::UpdateCharacterStateAfterMovement(float DeltaSeconds) 761 | { 762 | Super::UpdateCharacterStateAfterMovement(DeltaSeconds); 763 | Velocity.Z = FMath::Clamp(Velocity.Z, -AxisSpeedLimit, AxisSpeedLimit); 764 | UpdateSurfaceFriction(bSlidingInAir); 765 | // forward to the next frame 766 | bWasSlidingInAir = bSlidingInAir; 767 | UpdateCrouching(DeltaSeconds, true); 768 | } 769 | 770 | void UPBPlayerMovement::UpdateSurfaceFriction(bool bIsSliding) 771 | { 772 | if (!IsFalling() && CurrentFloor.IsWalkableFloor()) 773 | { 774 | bSlidingInAir = false; 775 | if (OldBase.Get() != CurrentFloor.HitResult.GetComponent() || !CurrentFloor.HitResult.Component.IsValid()) 776 | { 777 | OldBase = CurrentFloor.HitResult.GetComponent(); 778 | FHitResult Hit; 779 | TraceCharacterFloor(Hit); 780 | SurfaceFriction = GetFrictionFromHit(Hit); 781 | } 782 | } 783 | else 784 | { 785 | bSlidingInAir = bIsSliding; 786 | const bool bPlayerControlsMovedVertically = IsOnLadder() || Velocity.Z > JumpVelocity || Velocity.Z <= 0.0f || bCheatFlying; 787 | if (bPlayerControlsMovedVertically) 788 | { 789 | SurfaceFriction = 1.0f; 790 | } 791 | else if (bIsSliding) 792 | { 793 | SurfaceFriction = 0.25f; 794 | } 795 | } 796 | } 797 | 798 | void UPBPlayerMovement::UpdateCrouching(float DeltaTime, bool bOnlyUncrouch) 799 | { 800 | if (CharacterOwner->GetLocalRole() == ROLE_SimulatedProxy) 801 | { 802 | return; 803 | } 804 | 805 | // Crouch transition but not in noclip 806 | if (bIsInCrouchTransition && !bCheatFlying) 807 | { 808 | // If the player wants to uncrouch, or we have to uncrouch after movement 809 | if ((!bOnlyUncrouch && !bWantsToCrouch) || (bOnlyUncrouch && !CanCrouchInCurrentState())) 810 | { 811 | // and the player is not locked in a fully crouched position, we uncrouch 812 | if (!(bLockInCrouch && CharacterOwner->bIsCrouched)) 813 | { 814 | if (IsWalking()) 815 | { 816 | // Normal uncrouch 817 | DoUnCrouchResize(UncrouchTime, DeltaTime); 818 | } 819 | else 820 | { 821 | // Uncrouch jump 822 | DoUnCrouchResize(UncrouchJumpTime, DeltaTime); 823 | } 824 | } 825 | } 826 | else if (!bOnlyUncrouch) 827 | { 828 | if (IsOnLadder()) // if on a ladder, cancel this because bWantsToCrouch should be false 829 | { 830 | bIsInCrouchTransition = false; 831 | } 832 | else 833 | { 834 | if (IsWalking()) 835 | { 836 | DoCrouchResize(CrouchTime, DeltaTime); 837 | } 838 | else 839 | { 840 | DoCrouchResize(CrouchJumpTime, DeltaTime); 841 | } 842 | } 843 | } 844 | } 845 | } 846 | 847 | void UPBPlayerMovement::StartCrouchSlide() 848 | { 849 | float CurrentTime = GetWorld()->GetTimeSeconds(); 850 | // Don't boost again if we are already boosting 851 | if (IsCrouchSliding() || CurrentTime - CrouchSlideStartTime <= CrouchSlideCooldown) 852 | { 853 | // Continue crouch sliding if we're going that fast 854 | if (Velocity.SizeSquared2D() >= MinCrouchSlideBoost * MinCrouchSlideBoost) 855 | { 856 | bCrouchSliding = true; 857 | } 858 | return; 859 | } 860 | 861 | const FVector FloorNormal = CurrentFloor.HitResult.ImpactNormal; 862 | const FVector CrouchSlideInput = GetOwner()->GetActorForwardVector(); 863 | float Slope = (CrouchSlideInput | FloorNormal); 864 | float NewSpeed = FMath::Max(MinCrouchSlideBoost, Velocity.Size2D() * CrouchSlideBoostMultiplier); 865 | if (NewSpeed > MinCrouchSlideBoost && Slope < 0.0f) 866 | { 867 | NewSpeed = FMath::Clamp(NewSpeed + CrouchSlideBoostSlopeFactor * (NewSpeed - MinCrouchSlideBoost) * Slope, MinCrouchSlideBoost, NewSpeed); 868 | } 869 | Velocity = NewSpeed * Velocity.GetSafeNormal2D(); 870 | // Set the time 871 | CrouchSlideStartTime = CurrentTime; 872 | bCrouchSliding = true; 873 | } 874 | 875 | bool UPBPlayerMovement::ShouldCrouchSlide() const 876 | { 877 | return bCrouchSliding && IsMovingOnGround(); 878 | } 879 | 880 | void UPBPlayerMovement::StopCrouchSliding() 881 | { 882 | bCrouchSliding = false; 883 | bDeferCrouchSlideToLand = false; 884 | } 885 | 886 | void UPBPlayerMovement::ToggleCrouchLock(bool bLock) 887 | { 888 | bLockInCrouch = bLock; 889 | } 890 | 891 | float UPBPlayerMovement::GetFrictionFromHit(const FHitResult& Hit) const 892 | { 893 | float HitSurfaceFriction = 1.0f; 894 | if (Hit.PhysMaterial.IsValid()) 895 | { 896 | HitSurfaceFriction = FMath::Min(1.0f, Hit.PhysMaterial->Friction * 1.25f); 897 | } 898 | return HitSurfaceFriction; 899 | } 900 | 901 | void UPBPlayerMovement::TraceCharacterFloor(FHitResult& OutHit) const 902 | { 903 | FCollisionQueryParams CapsuleParams(SCENE_QUERY_STAT(CharacterFloorTrace), false, CharacterOwner); 904 | FCollisionResponseParams ResponseParam; 905 | InitCollisionParams(CapsuleParams, ResponseParam); 906 | // must trace complex to get mesh phys materials 907 | CapsuleParams.bTraceComplex = true; 908 | // must get materials 909 | CapsuleParams.bReturnPhysicalMaterial = true; 910 | 911 | const FCollisionShape StandingCapsuleShape = GetPawnCapsuleCollisionShape(SHRINK_None); 912 | const ECollisionChannel CollisionChannel = UpdatedComponent->GetCollisionObjectType(); 913 | FVector PawnLocation = UpdatedComponent->GetComponentLocation(); 914 | PawnLocation.Z -= StandingCapsuleShape.GetCapsuleHalfHeight(); 915 | FVector StandingLocation = PawnLocation; 916 | StandingLocation.Z -= MAX_FLOOR_DIST * 10.0f; 917 | GetWorld()->SweepSingleByChannel(OutHit, PawnLocation, StandingLocation, FQuat::Identity, CollisionChannel, StandingCapsuleShape, CapsuleParams, ResponseParam); 918 | } 919 | 920 | void UPBPlayerMovement::TraceLineToFloor(FHitResult& OutHit) const 921 | { 922 | FCollisionQueryParams CapsuleParams(SCENE_QUERY_STAT(TraceLineToFloor), false, CharacterOwner); 923 | FCollisionResponseParams ResponseParam; 924 | InitCollisionParams(CapsuleParams, ResponseParam); 925 | 926 | const FCollisionShape StandingCapsuleShape = GetPawnCapsuleCollisionShape(SHRINK_None); 927 | const ECollisionChannel CollisionChannel = UpdatedComponent->GetCollisionObjectType(); 928 | FVector PawnLocation = UpdatedComponent->GetComponentLocation(); 929 | PawnLocation.Z -= StandingCapsuleShape.GetCapsuleHalfHeight(); 930 | if (Acceleration.IsNearlyZero()) 931 | { 932 | if (!Velocity.IsNearlyZero()) 933 | { 934 | PawnLocation += Velocity.GetSafeNormal2D() * EdgeFrictionDist; 935 | } 936 | } 937 | else 938 | { 939 | PawnLocation += Acceleration.GetSafeNormal2D() * EdgeFrictionDist; 940 | } 941 | FVector StandingLocation = PawnLocation; 942 | StandingLocation.Z -= EdgeFrictionHeight; 943 | // DrawDebugLine(GetWorld(), PawnLocation, StandingLocation, FColor::Red, false, 10.0f); 944 | GetWorld()->SweepSingleByChannel(OutHit, PawnLocation, StandingLocation, FQuat::Identity, CollisionChannel, StandingCapsuleShape, CapsuleParams, ResponseParam); 945 | } 946 | 947 | void UPBPlayerMovement::PlayMoveSound(const float DeltaTime) 948 | { 949 | if (!bShouldPlayMoveSounds) 950 | { 951 | return; 952 | } 953 | 954 | // Count move sound time down if we've got it 955 | if (MoveSoundTime > 0.0f) 956 | { 957 | MoveSoundTime = FMath::Max(0.0f, MoveSoundTime - 1000.0f * DeltaTime); 958 | } 959 | 960 | // Check if it's time to play the sound 961 | if (MoveSoundTime > 0.0f) 962 | { 963 | return; 964 | } 965 | 966 | const float Speed = Velocity.SizeSquared2D(); 967 | float WalkSpeedThreshold; 968 | float SprintSpeedThreshold; 969 | 970 | if (IsCrouching() || IsOnLadder()) 971 | { 972 | WalkSpeedThreshold = MaxWalkSpeedCrouched; 973 | SprintSpeedThreshold = MaxWalkSpeedCrouched * 1.7f; 974 | } 975 | else 976 | { 977 | WalkSpeedThreshold = WalkSpeed; 978 | SprintSpeedThreshold = SprintSpeed; 979 | } 980 | 981 | // Only play sounds if we are moving fast enough on the ground or on a ladder 982 | const bool bPlaySound = (bBrakingFrameTolerated || IsOnLadder()) && Speed >= WalkSpeedThreshold * WalkSpeedThreshold && !ShouldCrouchSlide(); 983 | 984 | if (!bPlaySound) 985 | { 986 | return; 987 | } 988 | 989 | const bool bSprinting = Speed >= SprintSpeedThreshold * SprintSpeedThreshold; 990 | 991 | float MoveSoundVolume = 0.f; 992 | 993 | UPBMoveStepSound* MoveSound = nullptr; 994 | 995 | if (IsOnLadder()) 996 | { 997 | MoveSoundVolume = 0.5f; 998 | MoveSoundTime = 450.0f; 999 | MoveSound = GetMoveStepSoundBySurface(SurfaceType1); 1000 | } 1001 | else 1002 | { 1003 | MoveSoundTime = bSprinting ? 300.0f : 400.0f; 1004 | FHitResult Hit; 1005 | TraceCharacterFloor(Hit); 1006 | 1007 | if (Hit.PhysMaterial.IsValid()) 1008 | { 1009 | MoveSound = GetMoveStepSoundBySurface(Hit.PhysMaterial->SurfaceType); 1010 | } 1011 | if (!MoveSound) 1012 | { 1013 | MoveSound = GetMoveStepSoundBySurface(SurfaceType_Default); 1014 | } 1015 | 1016 | // Double-check that is valid before accessing it 1017 | if (MoveSound) 1018 | { 1019 | MoveSoundVolume = bSprinting ? MoveSound->GetSprintVolume() : MoveSound->GetWalkVolume(); 1020 | 1021 | if (IsCrouching()) 1022 | { 1023 | MoveSoundVolume *= 0.65f; 1024 | MoveSoundTime += 100.0f; 1025 | } 1026 | } 1027 | } 1028 | 1029 | if (MoveSound) 1030 | { 1031 | TArray MoveSoundCues; 1032 | 1033 | if (bSprinting && !IsOnLadder()) 1034 | { 1035 | MoveSoundCues = StepSide ? MoveSound->GetSprintLeftSounds() : MoveSound->GetSprintRightSounds(); 1036 | } 1037 | if (!bSprinting || IsOnLadder() || MoveSoundCues.Num() < 1) 1038 | { 1039 | MoveSoundCues = StepSide ? MoveSound->GetStepLeftSounds() : MoveSound->GetStepRightSounds(); 1040 | } 1041 | 1042 | // Error handling - Sounds not valid 1043 | if (MoveSoundCues.Num() < 1) // Sounds array not valid 1044 | { 1045 | // Get default sounds 1046 | MoveSound = GetMoveStepSoundBySurface(SurfaceType_Default); 1047 | 1048 | if (!MoveSound) 1049 | { 1050 | return; 1051 | } 1052 | 1053 | if (bSprinting) 1054 | { 1055 | // Get default sprint sounds 1056 | MoveSoundCues = StepSide ? MoveSound->GetSprintLeftSounds() : MoveSound->GetSprintRightSounds(); 1057 | } 1058 | 1059 | if (!bSprinting || MoveSoundCues.Num() < 1) 1060 | { 1061 | // If bSprinting = true, the code enter this IF only if the updated MoveSoundCues with default sprint sounds is not valid (length < 1) 1062 | // If bSprinting = false, the code enter this IF because the walk sounds are not valid and must try to pick them from the default surface 1063 | // Get default walk sounds 1064 | MoveSoundCues = StepSide ? MoveSound->GetStepLeftSounds() : MoveSound->GetStepRightSounds(); 1065 | } 1066 | 1067 | if (MoveSoundCues.Num() < 1) 1068 | { 1069 | // SurfaceType_Default sounds not found, return 1070 | return; 1071 | } 1072 | } 1073 | 1074 | // Sound array is valid, play a sound 1075 | // If the array has just one element pick that one skipping random 1076 | USoundCue* Sound = MoveSoundCues[MoveSoundCues.Num() == 1 ? 0 : FMath::RandRange(0, MoveSoundCues.Num() - 1)]; 1077 | 1078 | Sound->VolumeMultiplier = MoveSoundVolume; 1079 | 1080 | const FVector StepRelativeLocation(0.0f, 0.0f, -GetCharacterOwner()->GetCapsuleComponent()->GetScaledCapsuleHalfHeight()); 1081 | 1082 | UGameplayStatics::SpawnSoundAttached(Sound, UpdatedComponent, NAME_None, StepRelativeLocation, FRotator::ZeroRotator); 1083 | 1084 | StepSide = !StepSide; 1085 | } 1086 | } 1087 | 1088 | void UPBPlayerMovement::PlayJumpSound(const FHitResult& Hit, bool bJumped) 1089 | { 1090 | if (!bShouldPlayMoveSounds) 1091 | { 1092 | return; 1093 | } 1094 | 1095 | UPBMoveStepSound* MoveSound = nullptr; 1096 | TSubclassOf* GotSound = nullptr; 1097 | if (Hit.PhysMaterial.IsValid()) 1098 | { 1099 | GotSound = PBPlayerCharacter->GetMoveStepSound(Hit.PhysMaterial->SurfaceType); 1100 | } 1101 | if (GotSound) 1102 | { 1103 | MoveSound = GotSound->GetDefaultObject(); 1104 | } 1105 | if (!MoveSound) 1106 | { 1107 | if (!PBPlayerCharacter->GetMoveStepSound(TEnumAsByte(SurfaceType_Default))) 1108 | { 1109 | return; 1110 | } 1111 | MoveSound = PBPlayerCharacter->GetMoveStepSound(TEnumAsByte(SurfaceType_Default))->GetDefaultObject(); 1112 | } 1113 | 1114 | if (MoveSound) 1115 | { 1116 | float MoveSoundVolume; 1117 | 1118 | // if we didn't jump, adjust volume for landing 1119 | if (!bJumped) 1120 | { 1121 | const float FallSpeed = GetFallSpeed(true); 1122 | if (FallSpeed > PBPlayerCharacter->GetMinSpeedForFallDamage()) 1123 | { 1124 | MoveSoundVolume = 1.0f; 1125 | } 1126 | else if (FallSpeed > PBPlayerCharacter->GetMinSpeedForFallDamage() / 2.0f) 1127 | { 1128 | MoveSoundVolume = 0.85f; 1129 | } 1130 | else if (FallSpeed < PBPlayerCharacter->GetMinLandBounceSpeed()) 1131 | { 1132 | MoveSoundVolume = 0.0f; 1133 | } 1134 | else 1135 | { 1136 | MoveSoundVolume = 0.5f; 1137 | } 1138 | } 1139 | else 1140 | { 1141 | MoveSoundVolume = PBPlayerCharacter->IsSprinting() ? MoveSound->GetSprintVolume() : MoveSound->GetWalkVolume(); 1142 | } 1143 | 1144 | if (IsCrouching()) 1145 | { 1146 | MoveSoundVolume *= 0.65f; 1147 | } 1148 | 1149 | if (MoveSoundVolume <= 0.0f) 1150 | { 1151 | return; 1152 | } 1153 | 1154 | const TArray& MoveSoundCues = bJumped ? MoveSound->GetJumpSounds() : MoveSound->GetLandSounds(); 1155 | 1156 | if (MoveSoundCues.Num() < 1) 1157 | { 1158 | return; 1159 | } 1160 | 1161 | // If the array has just one element pick that one skipping random 1162 | USoundCue* Sound = MoveSoundCues[MoveSoundCues.Num() == 1 ? 0 : FMath::RandRange(0, MoveSoundCues.Num() - 1)]; 1163 | 1164 | Sound->VolumeMultiplier = MoveSoundVolume; 1165 | 1166 | const FVector StepRelativeLocation(0.0f, 0.0f, -GetCharacterOwner()->GetCapsuleComponent()->GetScaledCapsuleHalfHeight()); 1167 | 1168 | UGameplayStatics::SpawnSoundAttached(Sound, UpdatedComponent, NAME_None, StepRelativeLocation, FRotator::ZeroRotator); 1169 | } 1170 | } 1171 | 1172 | void UPBPlayerMovement::CalcVelocity(float DeltaTime, float Friction, bool bFluid, float BrakingDeceleration) 1173 | { 1174 | // UE4-COPY: void UCharacterMovementComponent::CalcVelocity(float DeltaTime, float Friction, bool bFluid, float BrakingDeceleration) 1175 | 1176 | // Do not update velocity when using root motion or when SimulatedProxy and not simulating root motion - SimulatedProxy are repped their Velocity 1177 | if (!HasValidData() || HasAnimRootMotion() || DeltaTime < MIN_TICK_TIME || (CharacterOwner && CharacterOwner->GetLocalRole() == ROLE_SimulatedProxy && !bWasSimulatingRootMotion)) 1178 | { 1179 | return; 1180 | } 1181 | 1182 | Friction = FMath::Max(0.0f, Friction); 1183 | const float MaxAccel = GetMaxAcceleration(); 1184 | float MaxSpeed = GetMaxSpeed(); 1185 | 1186 | // Player doesn't path follow 1187 | #if 0 1188 | // Check if path following requested movement 1189 | bool bZeroRequestedAcceleration = true; 1190 | FVector RequestedAcceleration = FVector::ZeroVector; 1191 | float RequestedSpeed = 0.0f; 1192 | if (ApplyRequestedMove(DeltaTime, MaxAccel, MaxSpeed, Friction, BrakingDeceleration, RequestedAcceleration, RequestedSpeed)) 1193 | { 1194 | RequestedAcceleration = RequestedAcceleration.GetClampedToMaxSize(MaxAccel); 1195 | bZeroRequestedAcceleration = false; 1196 | } 1197 | #endif 1198 | 1199 | if (bForceMaxAccel) 1200 | { 1201 | // Force acceleration at full speed. 1202 | // In consideration order for direction: Acceleration, then Velocity, then Pawn's rotation. 1203 | if (Acceleration.SizeSquared() > SMALL_NUMBER) 1204 | { 1205 | Acceleration = Acceleration.GetSafeNormal() * MaxAccel; 1206 | } 1207 | else 1208 | { 1209 | Acceleration = MaxAccel * (Velocity.SizeSquared() < SMALL_NUMBER ? UpdatedComponent->GetForwardVector() : Velocity.GetSafeNormal()); 1210 | } 1211 | 1212 | AnalogInputModifier = 1.0f; 1213 | } 1214 | 1215 | #if 0 1216 | // Path following above didn't care about the analog modifier, but we do for everything else below, so get the fully modified value. 1217 | // Use max of requested speed and max speed if we modified the speed in ApplyRequestedMove above. 1218 | const float MaxInputSpeed = FMath::Max(MaxSpeed * AnalogInputModifier, GetMinAnalogSpeed()); 1219 | MaxSpeed = FMath::Max(RequestedSpeed, MaxInputSpeed); 1220 | #else 1221 | MaxSpeed = FMath::Max(MaxSpeed * AnalogInputModifier, GetMinAnalogSpeed()); 1222 | #endif 1223 | 1224 | // Apply braking or deceleration 1225 | const bool bZeroAcceleration = Acceleration.IsNearlyZero(); 1226 | const bool bIsGroundMove = IsMovingOnGround() && bBrakingFrameTolerated; 1227 | 1228 | // Apply friction 1229 | if (bIsGroundMove || CVarAlwaysApplyFriction->GetBool()) 1230 | { 1231 | const bool bVelocityOverMax = IsExceedingMaxSpeed(MaxSpeed); 1232 | const FVector OldVelocity = Velocity; 1233 | 1234 | float ActualBrakingFriction = (bUseSeparateBrakingFriction ? BrakingFriction : Friction) * SurfaceFriction; 1235 | 1236 | if (bIsGroundMove && EdgeFrictionMultiplier != 1.0f) 1237 | { 1238 | bool bDoEdgeFriction = false; 1239 | if (!bEdgeFrictionOnlyWhenBraking) 1240 | { 1241 | bDoEdgeFriction = true; 1242 | } 1243 | else if (bEdgeFrictionAlwaysWhenCrouching && IsCrouching()) 1244 | { 1245 | bDoEdgeFriction = true; 1246 | } 1247 | else if (bZeroAcceleration) 1248 | { 1249 | bDoEdgeFriction = true; 1250 | } 1251 | if (bDoEdgeFriction) 1252 | { 1253 | FHitResult Hit(ForceInit); 1254 | TraceLineToFloor(Hit); 1255 | if (!Hit.bBlockingHit) 1256 | { 1257 | ActualBrakingFriction *= EdgeFrictionMultiplier; 1258 | } 1259 | } 1260 | } 1261 | 1262 | ApplyVelocityBraking(DeltaTime, ActualBrakingFriction, BrakingDeceleration); 1263 | 1264 | // Don't allow braking to lower us below max speed if we started above it. 1265 | if (bVelocityOverMax && Velocity.SizeSquared() < FMath::Square(MaxSpeed) && FVector::DotProduct(Acceleration, OldVelocity) > 0.0f) 1266 | { 1267 | Velocity = OldVelocity.GetSafeNormal() * MaxSpeed; 1268 | } 1269 | } 1270 | 1271 | // Apply fluid friction 1272 | if (bFluid) 1273 | { 1274 | Velocity = Velocity * (1.0f - FMath::Min(Friction * DeltaTime, 1.0f)); 1275 | } 1276 | 1277 | // Limit before 1278 | Velocity.X = FMath::Clamp(Velocity.X, -AxisSpeedLimit, AxisSpeedLimit); 1279 | Velocity.Y = FMath::Clamp(Velocity.Y, -AxisSpeedLimit, AxisSpeedLimit); 1280 | 1281 | // no clip 1282 | if (bCheatFlying) 1283 | { 1284 | StopCrouchSliding(); 1285 | if (bZeroAcceleration) 1286 | { 1287 | Velocity = FVector(0.0f); 1288 | } 1289 | else 1290 | { 1291 | auto LookVec = CharacterOwner->GetControlRotation().Vector(); 1292 | auto LookVec2D = CharacterOwner->GetActorForwardVector(); 1293 | LookVec2D.Z = 0.0f; 1294 | auto PerpendicularAccel = (LookVec2D | Acceleration) * LookVec2D; 1295 | auto TangentialAccel = Acceleration - PerpendicularAccel; 1296 | auto UnitAcceleration = Acceleration; 1297 | auto Dir = UnitAcceleration.CosineAngle2D(LookVec); 1298 | auto NoClipAccelClamp = PBPlayerCharacter->IsSprinting() ? 2.0f * MaxAcceleration : MaxAcceleration; 1299 | Velocity = (Dir * LookVec * PerpendicularAccel.Size2D() + TangentialAccel).GetClampedToSize(NoClipAccelClamp, NoClipAccelClamp); 1300 | } 1301 | } 1302 | // ladder movement 1303 | else if (IsOnLadder()) 1304 | { 1305 | StopCrouchSliding(); 1306 | 1307 | // instantly brake when you're on a ladder 1308 | Velocity = FVector::ZeroVector; 1309 | 1310 | // only set the velocity if the player is moving 1311 | if (!bZeroAcceleration) 1312 | { 1313 | // Handle ladder movement here 1314 | } 1315 | } 1316 | // crouch slide on ground 1317 | else if (ShouldCrouchSlide()) 1318 | { 1319 | const FVector FloorNormal = CurrentFloor.HitResult.ImpactNormal; 1320 | // Direction of our crouch slide 1321 | const FVector CrouchSlideInput = GetOwner()->GetActorForwardVector(); 1322 | float CurrentTime = GetWorld()->GetTimeSeconds(); 1323 | float TimeDifference = CurrentTime - CrouchSlideStartTime; 1324 | // Decay velocity boosting within acceleration over time 1325 | FVector WishAccel = CrouchSlideInput * Velocity.Size2D() * FMath::Lerp(MaxCrouchSlideVelocityBoost, MinCrouchSlideVelocityBoost, FMath::Clamp(TimeDifference / CrouchSlideBoostTime, 0.0f, 1.0f)); 1326 | float Slope = (CrouchSlideInput | FloorNormal); 1327 | // Handle slope (decay more on uphill, boost on downhill) 1328 | WishAccel *= 1.0f + Slope; 1329 | Velocity += WishAccel * DeltaTime; 1330 | // Stop crouch sliding 1331 | if (Velocity.IsNearlyZero()) 1332 | { 1333 | StopCrouchSliding(); 1334 | } 1335 | } 1336 | // walk move 1337 | else 1338 | { 1339 | if (IsMovingOnGround()) 1340 | { 1341 | StopCrouchSliding(); 1342 | } 1343 | // Apply input acceleration 1344 | if (!bZeroAcceleration) 1345 | { 1346 | // Clamp acceleration to max speed 1347 | const FVector WishAccel = Acceleration.GetClampedToMaxSize2D(MaxSpeed); 1348 | // Find veer 1349 | const FVector AccelDir = WishAccel.GetSafeNormal2D(); 1350 | const float Veer = Velocity.X * AccelDir.X + Velocity.Y * AccelDir.Y; 1351 | // Get add speed with an air speed cap, depending on if we're sliding in air or not 1352 | // note: we use b WAS SlidingInAir since we only can categorize our movement after a velocity step, therefore we have to use the slide state from the previous frame while computing velocity 1353 | float SpeedCap = 0.0f; 1354 | if (!bIsGroundMove) 1355 | { 1356 | // use original air speed cap for strafing during a slide, for surfing 1357 | float ForwardAccel = AccelDir | GetOwner()->GetActorForwardVector(); 1358 | if (bWasSlidingInAir && FMath::IsNearlyZero(ForwardAccel)) 1359 | { 1360 | SpeedCap = AirSlideSpeedCap; 1361 | } 1362 | else 1363 | { 1364 | SpeedCap = AirSpeedCap; 1365 | } 1366 | } 1367 | const float AddSpeed = (bIsGroundMove ? WishAccel : WishAccel.GetClampedToMaxSize2D(SpeedCap)).Size2D() - Veer; 1368 | if (AddSpeed > 0.0f) 1369 | { 1370 | // Apply acceleration 1371 | const float AccelerationMultiplier = bIsGroundMove ? GroundAccelerationMultiplier : AirAccelerationMultiplier; 1372 | FVector CurrentAcceleration = WishAccel * AccelerationMultiplier * SurfaceFriction * DeltaTime; 1373 | CurrentAcceleration = CurrentAcceleration.GetClampedToMaxSize2D(AddSpeed); 1374 | Velocity += CurrentAcceleration; 1375 | } 1376 | } 1377 | 1378 | // No requested accel on player 1379 | #if 0 1380 | // Apply additional requested acceleration 1381 | if (!bZeroRequestedAcceleration) 1382 | { 1383 | Velocity += RequestedAcceleration * DeltaTime; 1384 | } 1385 | #endif 1386 | } 1387 | 1388 | // Limit after 1389 | Velocity.X = FMath::Clamp(Velocity.X, -AxisSpeedLimit, AxisSpeedLimit); 1390 | Velocity.Y = FMath::Clamp(Velocity.Y, -AxisSpeedLimit, AxisSpeedLimit); 1391 | 1392 | const float SpeedSq = Velocity.SizeSquared2D(); 1393 | 1394 | // Dynamic step height code for allowing sliding on a slope when at a high speed 1395 | if (IsOnLadder() || SpeedSq <= MaxWalkSpeedCrouched * MaxWalkSpeedCrouched) 1396 | { 1397 | // If we're crouching or not sliding, just use max 1398 | MaxStepHeight = DefaultStepHeight; 1399 | if (GetWalkableFloorZ() != DefaultWalkableFloorZ) 1400 | { 1401 | SetWalkableFloorZ(DefaultWalkableFloorZ); 1402 | } 1403 | } 1404 | else 1405 | { 1406 | // Scale step/ramp height down the faster we go 1407 | float Speed = FMath::Sqrt(SpeedSq); 1408 | float SpeedScale = (Speed - SpeedMultMin) / (SpeedMultMax - SpeedMultMin); 1409 | float SpeedMultiplier = FMath::Clamp(SpeedScale, 0.0f, 1.0f); 1410 | SpeedMultiplier *= SpeedMultiplier; 1411 | if (!IsFalling()) 1412 | { 1413 | // If we're on ground, factor in friction. 1414 | SpeedMultiplier = FMath::Max((1.0f - SurfaceFriction) * SpeedMultiplier, 0.0f); 1415 | } 1416 | MaxStepHeight = FMath::Lerp(DefaultStepHeight, MinStepHeight, SpeedMultiplier); 1417 | const float NewWalkableFloorZ = FMath::Lerp(DefaultWalkableFloorZ, 0.9848f, SpeedMultiplier); 1418 | if (GetWalkableFloorZ() != NewWalkableFloorZ) 1419 | { 1420 | SetWalkableFloorZ(NewWalkableFloorZ); 1421 | } 1422 | } 1423 | 1424 | // Players don't use RVO avoidance 1425 | #if 0 1426 | if (bUseRVOAvoidance) 1427 | { 1428 | CalcAvoidanceVelocity(DeltaTime); 1429 | } 1430 | #endif 1431 | } 1432 | 1433 | void UPBPlayerMovement::Crouch(bool bClientSimulation) 1434 | { 1435 | // TODO: replicate to the client simulation that we are in a crouch transition so they can do the resize too. 1436 | if (bClientSimulation) 1437 | { 1438 | Super::Crouch(true); 1439 | return; 1440 | } 1441 | // Check if we're moving forward fast enough 1442 | // don't init crouch sliding twice 1443 | if (bShouldCrouchSlide) 1444 | { 1445 | if ((Velocity | GetOwner()->GetActorForwardVector()) >= SprintSpeed * CrouchSlideSpeedRequirementMultiplier && !bCrouchSliding) 1446 | { 1447 | // if we have input on ground 1448 | if (!Acceleration.IsNearlyZero() && IsMovingOnGround()) 1449 | { 1450 | StartCrouchSlide(); 1451 | } 1452 | // if we are falling down (to prevent crouch jump slides) 1453 | else if (IsFalling() && Velocity.Z < 0.0f) 1454 | { 1455 | // if we are in the air, falling down, defer crouch slide 1456 | bDeferCrouchSlideToLand = true; 1457 | } 1458 | } 1459 | } 1460 | bIsInCrouchTransition = true; 1461 | } 1462 | 1463 | void UPBPlayerMovement::DoCrouchResize(float TargetTime, float DeltaTime, bool bClientSimulation) 1464 | { 1465 | // UE4-COPY: void UCharacterMovementComponent::Crouch(bool bClientSimulation) 1466 | 1467 | if (!HasValidData() || (!bClientSimulation && !CanCrouchInCurrentState())) 1468 | { 1469 | bIsInCrouchTransition = false; 1470 | return; 1471 | } 1472 | 1473 | // See if collision is already at desired size. 1474 | UCapsuleComponent* CharacterCapsule = CharacterOwner->GetCapsuleComponent(); 1475 | if (FMath::IsNearlyEqual(CharacterCapsule->GetUnscaledCapsuleHalfHeight(), GetCrouchedHalfHeight())) 1476 | { 1477 | if (!bClientSimulation) 1478 | { 1479 | CharacterOwner->bIsCrouched = true; 1480 | } 1481 | CharacterOwner->OnStartCrouch(0.0f, 0.0f); 1482 | bIsInCrouchTransition = false; 1483 | return; 1484 | } 1485 | 1486 | ACharacter* DefaultCharacter = CharacterOwner->GetClass()->GetDefaultObject(); 1487 | 1488 | if (bClientSimulation && CharacterOwner->GetLocalRole() == ROLE_SimulatedProxy) 1489 | { 1490 | // restore collision size before crouching 1491 | CharacterCapsule->SetCapsuleSize(DefaultCharacter->GetCapsuleComponent()->GetUnscaledCapsuleRadius(), DefaultCharacter->GetCapsuleComponent()->GetUnscaledCapsuleHalfHeight()); 1492 | bShrinkProxyCapsule = true; 1493 | } 1494 | 1495 | // Change collision size to crouching dimensions 1496 | const float ComponentScale = CharacterCapsule->GetShapeScale(); 1497 | const float OldUnscaledHalfHeight = DefaultCharacter->GetCapsuleComponent()->GetUnscaledCapsuleHalfHeight(); 1498 | const float OldUnscaledRadius = CharacterCapsule->GetUnscaledCapsuleRadius(); 1499 | const float FullCrouchDiff = OldUnscaledHalfHeight - GetCrouchedHalfHeight(); 1500 | const float CurrentUnscaledHalfHeight = CharacterCapsule->GetUnscaledCapsuleHalfHeight(); 1501 | // Determine the crouching progress 1502 | const bool bInstantCrouch = FMath::IsNearlyZero(TargetTime); 1503 | const float CurrentAlpha = 1.0f - (CurrentUnscaledHalfHeight - GetCrouchedHalfHeight()) / FullCrouchDiff; 1504 | // Determine how much we are progressing this tick 1505 | float TargetAlphaDiff = 1.0f; 1506 | float TargetAlpha = 1.0f; 1507 | if (!bInstantCrouch) 1508 | { 1509 | TargetAlphaDiff = DeltaTime / CrouchTime; 1510 | TargetAlpha = CurrentAlpha + TargetAlphaDiff; 1511 | } 1512 | if (TargetAlpha >= 1.0f || FMath::IsNearlyEqual(TargetAlpha, 1.0f)) 1513 | { 1514 | TargetAlpha = 1.0f; 1515 | TargetAlphaDiff = TargetAlpha - CurrentAlpha; 1516 | bIsInCrouchTransition = false; 1517 | CharacterOwner->bIsCrouched = true; 1518 | } 1519 | // Determine the target height for this tick 1520 | float TargetCrouchedHalfHeight = OldUnscaledHalfHeight - FullCrouchDiff * TargetAlpha; 1521 | // Height is not allowed to be smaller than radius. 1522 | float ClampedCrouchedHalfHeight = FMath::Max3(0.0f, OldUnscaledRadius, TargetCrouchedHalfHeight); 1523 | CharacterCapsule->SetCapsuleSize(OldUnscaledRadius, ClampedCrouchedHalfHeight); 1524 | float HalfHeightAdjust = FullCrouchDiff * TargetAlphaDiff; 1525 | float ScaledHalfHeightAdjust = HalfHeightAdjust * ComponentScale; 1526 | 1527 | if (!bClientSimulation) 1528 | { 1529 | if (bCrouchMaintainsBaseLocation) 1530 | { 1531 | // Intentionally not using MoveUpdatedComponent, where a horizontal 1532 | // plane constraint would prevent the base of the capsule from 1533 | // staying at the same spot. 1534 | UpdatedComponent->MoveComponent(FVector(0.0f, 0.0f, -ScaledHalfHeightAdjust), UpdatedComponent->GetComponentQuat(), true, nullptr, MOVECOMP_NoFlags, ETeleportType::TeleportPhysics); 1535 | } 1536 | else 1537 | { 1538 | UpdatedComponent->MoveComponent(FVector(0.0f, 0.0f, ScaledHalfHeightAdjust), UpdatedComponent->GetComponentQuat(), true, nullptr, MOVECOMP_NoFlags, ETeleportType::None); 1539 | } 1540 | } 1541 | 1542 | bForceNextFloorCheck = true; 1543 | 1544 | const float MeshAdjust = DefaultCharacter->GetCapsuleComponent()->GetUnscaledCapsuleHalfHeight() - ClampedCrouchedHalfHeight; 1545 | AdjustProxyCapsuleSize(); 1546 | CharacterOwner->OnStartCrouch(MeshAdjust, MeshAdjust * ComponentScale); 1547 | 1548 | // Don't smooth this change in mesh position 1549 | if ((bClientSimulation && CharacterOwner->GetLocalRole() == ROLE_SimulatedProxy) || (IsNetMode(NM_ListenServer) && CharacterOwner->GetRemoteRole() == ROLE_AutonomousProxy)) 1550 | { 1551 | FNetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character(); 1552 | if (ClientData) 1553 | { 1554 | ClientData->MeshTranslationOffset -= FVector(0.0f, 0.0f, ScaledHalfHeightAdjust); 1555 | ClientData->OriginalMeshTranslationOffset = ClientData->MeshTranslationOffset; 1556 | } 1557 | } 1558 | } 1559 | 1560 | void UPBPlayerMovement::UnCrouch(bool bClientSimulation) 1561 | { 1562 | // TODO: replicate to the client simulation that we are in a crouch transition so they can do the resize too. 1563 | if (bClientSimulation) 1564 | { 1565 | Super::UnCrouch(true); 1566 | return; 1567 | } 1568 | bIsInCrouchTransition = true; 1569 | StopCrouchSliding(); 1570 | } 1571 | 1572 | void UPBPlayerMovement::DoUnCrouchResize(float TargetTime, float DeltaTime, bool bClientSimulation) 1573 | { 1574 | // UE4-COPY: void UCharacterMovementComponent::UnCrouch(bool bClientSimulation) 1575 | 1576 | if (!HasValidData()) 1577 | { 1578 | bIsInCrouchTransition = false; 1579 | return; 1580 | } 1581 | 1582 | ACharacter* DefaultCharacter = CharacterOwner->GetClass()->GetDefaultObject(); 1583 | 1584 | UCapsuleComponent* CharacterCapsule = CharacterOwner->GetCapsuleComponent(); 1585 | 1586 | // See if collision is already at desired size. 1587 | if (FMath::IsNearlyEqual(CharacterCapsule->GetUnscaledCapsuleHalfHeight(), DefaultCharacter->GetCapsuleComponent()->GetUnscaledCapsuleHalfHeight())) 1588 | { 1589 | if (!bClientSimulation) 1590 | { 1591 | CharacterOwner->bIsCrouched = false; 1592 | } 1593 | CharacterOwner->OnEndCrouch(0.0f, 0.0f); 1594 | bCrouchFrameTolerated = false; 1595 | bIsInCrouchTransition = false; 1596 | return; 1597 | } 1598 | 1599 | const float CurrentCrouchedHalfHeight = CharacterCapsule->GetScaledCapsuleHalfHeight(); 1600 | 1601 | const float ComponentScale = CharacterCapsule->GetShapeScale(); 1602 | const float OldUnscaledHalfHeight = CharacterCapsule->GetUnscaledCapsuleHalfHeight(); 1603 | const float UncrouchedHeight = DefaultCharacter->GetCapsuleComponent()->GetUnscaledCapsuleHalfHeight(); 1604 | const float FullCrouchDiff = UncrouchedHeight - GetCrouchedHalfHeight(); 1605 | // Determine the crouching progress 1606 | const bool bInstantCrouch = FMath::IsNearlyZero(TargetTime); 1607 | float CurrentAlpha = 1.0f - (UncrouchedHeight - OldUnscaledHalfHeight) / FullCrouchDiff; 1608 | float TargetAlphaDiff = 1.0f; 1609 | float TargetAlpha = 1.0f; 1610 | const UWorld* MyWorld = GetWorld(); 1611 | const FVector PawnLocation = UpdatedComponent->GetComponentLocation(); 1612 | if (!bInstantCrouch) 1613 | { 1614 | TargetAlphaDiff = DeltaTime / TargetTime; 1615 | TargetAlpha = CurrentAlpha + TargetAlphaDiff; 1616 | // Don't partial uncrouch in tight places (like vents) 1617 | if (bCrouchMaintainsBaseLocation) 1618 | { 1619 | // Try to stay in place and see if the larger capsule fits. We use a 1620 | // slightly taller capsule to avoid penetration. 1621 | const float SweepInflation = KINDA_SMALL_NUMBER * 10.0f; 1622 | FCollisionQueryParams CapsuleParams(SCENE_QUERY_STAT(CrouchTrace), false, CharacterOwner); 1623 | FCollisionResponseParams ResponseParam; 1624 | InitCollisionParams(CapsuleParams, ResponseParam); 1625 | 1626 | // Check how much we have left to go (with some wiggle room to still allow for partial uncrouches in some areas) 1627 | const float HalfHeightAdjust = ComponentScale * (UncrouchedHeight - OldUnscaledHalfHeight) * GroundUncrouchCheckFactor; 1628 | 1629 | // Compensate for the difference between current capsule size and standing size 1630 | // Shrink by negative amount, so actually grow it. 1631 | const FCollisionShape StandingCapsuleShape = GetPawnCapsuleCollisionShape(SHRINK_HeightCustom, -SweepInflation - HalfHeightAdjust); 1632 | const ECollisionChannel CollisionChannel = UpdatedComponent->GetCollisionObjectType(); 1633 | FVector StandingLocation = PawnLocation + FVector(0.0f, 0.0f, StandingCapsuleShape.GetCapsuleHalfHeight() - CurrentCrouchedHalfHeight); 1634 | bool bEncroached = MyWorld->OverlapBlockingTestByChannel(StandingLocation, FQuat::Identity, CollisionChannel, StandingCapsuleShape, CapsuleParams, ResponseParam); 1635 | if (bEncroached) 1636 | { 1637 | // We're blocked from doing a full uncrouch, so don't attempt for now 1638 | return; 1639 | } 1640 | } 1641 | } 1642 | if (TargetAlpha >= 1.0f || FMath::IsNearlyEqual(TargetAlpha, 1.0f)) 1643 | { 1644 | TargetAlpha = 1.0f; 1645 | TargetAlphaDiff = TargetAlpha - CurrentAlpha; 1646 | bIsInCrouchTransition = false; 1647 | StopCrouchSliding(); 1648 | } 1649 | const float HalfHeightAdjust = FullCrouchDiff * TargetAlphaDiff; 1650 | const float ScaledHalfHeightAdjust = HalfHeightAdjust * ComponentScale; 1651 | 1652 | // Grow to uncrouched size. 1653 | check(CharacterCapsule); 1654 | 1655 | if (!bClientSimulation) 1656 | { 1657 | // Try to stay in place and see if the larger capsule fits. We use a 1658 | // slightly taller capsule to avoid penetration. 1659 | const float SweepInflation = KINDA_SMALL_NUMBER * 10.0f; 1660 | FCollisionQueryParams CapsuleParams(SCENE_QUERY_STAT(CrouchTrace), false, CharacterOwner); 1661 | FCollisionResponseParams ResponseParam; 1662 | InitCollisionParams(CapsuleParams, ResponseParam); 1663 | 1664 | // Compensate for the difference between current capsule size and 1665 | // standing size 1666 | // Shrink by negative amount, so actually grow it. 1667 | const FCollisionShape StandingCapsuleShape = GetPawnCapsuleCollisionShape(SHRINK_HeightCustom, -SweepInflation - ScaledHalfHeightAdjust); 1668 | const ECollisionChannel CollisionChannel = UpdatedComponent->GetCollisionObjectType(); 1669 | bool bEncroached = true; 1670 | 1671 | if (!bCrouchMaintainsBaseLocation) 1672 | { 1673 | // Expand in place 1674 | bEncroached = MyWorld->OverlapBlockingTestByChannel(PawnLocation, FQuat::Identity, CollisionChannel, StandingCapsuleShape, CapsuleParams, ResponseParam); 1675 | 1676 | if (bEncroached) 1677 | { 1678 | // Try adjusting capsule position to see if we can avoid 1679 | // encroachment. 1680 | if (ScaledHalfHeightAdjust > 0.0f) 1681 | { 1682 | // Shrink to a short capsule, sweep down to base to find 1683 | // where that would hit something, and then try to stand up 1684 | // from there. 1685 | float PawnRadius, PawnHalfHeight; 1686 | CharacterCapsule->GetScaledCapsuleSize(PawnRadius, PawnHalfHeight); 1687 | const float ShrinkHalfHeight = PawnHalfHeight - PawnRadius; 1688 | const float TraceDist = PawnHalfHeight - ShrinkHalfHeight; 1689 | // const FVector Down = FVector(0.0f, 0.0f, -TraceDist); 1690 | 1691 | FHitResult Hit(1.0f); 1692 | const FCollisionShape ShortCapsuleShape = GetPawnCapsuleCollisionShape(SHRINK_HeightCustom, ShrinkHalfHeight); 1693 | // const bool bBlockingHit = MyWorld->SweepSingleByChannel(Hit, PawnLocation, PawnLocation + Down, FQuat::Identity, CollisionChannel, 1694 | // ShortCapsuleShape, CapsuleParams); 1695 | 1696 | if (!Hit.bStartPenetrating) 1697 | { 1698 | // Compute where the base of the sweep ended up, and see 1699 | // if we can stand there 1700 | const float DistanceToBase = (Hit.Time * TraceDist) + ShortCapsuleShape.Capsule.HalfHeight; 1701 | const FVector NewLoc = FVector(PawnLocation.X, PawnLocation.Y, PawnLocation.Z - DistanceToBase + StandingCapsuleShape.Capsule.HalfHeight + SweepInflation + MIN_FLOOR_DIST / 2.0f); 1702 | bEncroached = MyWorld->OverlapBlockingTestByChannel(NewLoc, FQuat::Identity, CollisionChannel, StandingCapsuleShape, CapsuleParams, ResponseParam); 1703 | if (!bEncroached) 1704 | { 1705 | // Intentionally not using MoveUpdatedComponent, 1706 | // where a horizontal plane constraint would prevent 1707 | // the base of the capsule from staying at the same 1708 | // spot. 1709 | UpdatedComponent->MoveComponent(NewLoc - PawnLocation, UpdatedComponent->GetComponentQuat(), false, nullptr, MOVECOMP_NoFlags, ETeleportType::TeleportPhysics); 1710 | } 1711 | } 1712 | } 1713 | } 1714 | } 1715 | else 1716 | { 1717 | // Expand while keeping base location the same. 1718 | FVector StandingLocation = PawnLocation + FVector(0.0f, 0.0f, StandingCapsuleShape.GetCapsuleHalfHeight() - CurrentCrouchedHalfHeight); 1719 | bEncroached = MyWorld->OverlapBlockingTestByChannel(StandingLocation, FQuat::Identity, CollisionChannel, StandingCapsuleShape, CapsuleParams, ResponseParam); 1720 | 1721 | if (bEncroached) 1722 | { 1723 | if (IsMovingOnGround()) 1724 | { 1725 | // Something might be just barely overhead, try moving down 1726 | // closer to the floor to avoid it. 1727 | const float MinFloorDist = KINDA_SMALL_NUMBER * 10.0f; 1728 | if (CurrentFloor.bBlockingHit && CurrentFloor.FloorDist > MinFloorDist) 1729 | { 1730 | StandingLocation.Z -= CurrentFloor.FloorDist - MinFloorDist; 1731 | bEncroached = MyWorld->OverlapBlockingTestByChannel(StandingLocation, FQuat::Identity, CollisionChannel, StandingCapsuleShape, CapsuleParams, ResponseParam); 1732 | } 1733 | } 1734 | } 1735 | 1736 | if (!bEncroached) 1737 | { 1738 | // Commit the change in location. 1739 | UpdatedComponent->MoveComponent(StandingLocation - PawnLocation, UpdatedComponent->GetComponentQuat(), false, nullptr, MOVECOMP_NoFlags, ETeleportType::TeleportPhysics); 1740 | bForceNextFloorCheck = true; 1741 | } 1742 | } 1743 | 1744 | // If still encroached then abort. 1745 | if (bEncroached) 1746 | { 1747 | return; 1748 | } 1749 | 1750 | CharacterOwner->bIsCrouched = false; 1751 | } 1752 | else 1753 | { 1754 | bShrinkProxyCapsule = true; 1755 | } 1756 | 1757 | // Now call SetCapsuleSize() to cause touch/untouch events and actually grow the capsule 1758 | CharacterCapsule->SetCapsuleSize(DefaultCharacter->GetCapsuleComponent()->GetUnscaledCapsuleRadius(), OldUnscaledHalfHeight + HalfHeightAdjust, true); 1759 | 1760 | // OnEndCrouch takes the change from the Default size, not the current one (though they are usually the same). 1761 | const float MeshAdjust = DefaultCharacter->GetCapsuleComponent()->GetUnscaledCapsuleHalfHeight() - OldUnscaledHalfHeight + HalfHeightAdjust; 1762 | AdjustProxyCapsuleSize(); 1763 | CharacterOwner->OnEndCrouch(MeshAdjust, MeshAdjust * ComponentScale); 1764 | bCrouchFrameTolerated = false; 1765 | 1766 | // Don't smooth this change in mesh position 1767 | if ((bClientSimulation && CharacterOwner->GetLocalRole() == ROLE_SimulatedProxy) || (IsNetMode(NM_ListenServer) && CharacterOwner->GetRemoteRole() == ROLE_AutonomousProxy)) 1768 | { 1769 | FNetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character(); 1770 | if (ClientData) 1771 | { 1772 | ClientData->MeshTranslationOffset += FVector(0.0f, 0.0f, ScaledHalfHeightAdjust); 1773 | ClientData->OriginalMeshTranslationOffset = ClientData->MeshTranslationOffset; 1774 | } 1775 | } 1776 | } 1777 | 1778 | bool UPBPlayerMovement::MoveUpdatedComponentImpl(const FVector& Delta, const FQuat& NewRotation, bool bSweep, FHitResult* OutHit, ETeleportType Teleport) 1779 | { 1780 | FVector NewDelta = Delta; 1781 | 1782 | // Start from the capsule location pre-move 1783 | FVector Loc = UpdatedComponent->GetComponentLocation(); 1784 | 1785 | bool bResult = Super::MoveUpdatedComponentImpl(NewDelta, NewRotation, bSweep, OutHit, Teleport); 1786 | 1787 | if (bSweep && Teleport == ETeleportType::None && Delta != FVector::ZeroVector && IsFalling() && FMath::Abs(Delta.Z) > 0.0f) 1788 | { 1789 | const float HorizontalMovement = Delta.SizeSquared2D(); 1790 | if (HorizontalMovement > UE_KINDA_SMALL_NUMBER) 1791 | { 1792 | bool bBlockingHit; 1793 | 1794 | // Test with a box that is enclosed by the capsule. 1795 | float PawnRadius, PawnHalfHeight; 1796 | CharacterOwner->GetCapsuleComponent()->GetScaledCapsuleSize(PawnRadius, PawnHalfHeight); 1797 | // Scale by diagonal 1798 | PawnRadius *= 0.707f; 1799 | // Shrink our height so we don't intersect any current floor 1800 | PawnHalfHeight -= SWEEP_EDGE_REJECT_DISTANCE; 1801 | const FCollisionShape BoxShape = FCollisionShape::MakeBox(FVector(PawnRadius, PawnRadius, PawnHalfHeight)); 1802 | 1803 | FVector Start = Loc; 1804 | // this is solely a horizontal movement check, so assume we've already moved the Z delta. 1805 | Start.Z += Delta.Z; 1806 | 1807 | FVector DeltaDir = Delta; 1808 | DeltaDir.Z = 0.0f; 1809 | FVector End = Start + DeltaDir; 1810 | 1811 | const ECollisionChannel TraceChannel = UpdatedComponent->GetCollisionObjectType(); 1812 | FCollisionQueryParams Params(SCENE_QUERY_STAT(CapsuleHemisphereTrace), false, CharacterOwner); 1813 | FCollisionResponseParams ResponseParam; 1814 | InitCollisionParams(Params, ResponseParam); 1815 | 1816 | FHitResult Hit(1.f); 1817 | 1818 | //DrawDebugBox(GetWorld(), End, FVector(PawnRadius, PawnRadius, PawnHalfHeight), FQuat(RotateGravityToWorld(FVector(0.f, 0.f, -1.f)), UE_PI * 0.25f), FColor::Red, false, 10.0f, 0, 0.5f); 1819 | 1820 | // First test with the box rotated so the corners are along the major axes (ie rotated 45 degrees). 1821 | bBlockingHit = GetWorld()->SweepSingleByChannel(Hit, Start, End, FQuat(RotateGravityToWorld(FVector(0.f, 0.f, -1.f)), UE_PI * 0.25f), TraceChannel, BoxShape, Params, ResponseParam); 1822 | 1823 | if (!bBlockingHit) 1824 | { 1825 | // Test again with the same box, not rotated. 1826 | Hit.Reset(1.f, false); 1827 | //DrawDebugBox(GetWorld(), End, FVector(PawnRadius, PawnRadius, PawnHalfHeight), GetWorldToGravityTransform(), FColor::Red, false, 10.0f, 0, 0.5f); 1828 | bBlockingHit = GetWorld()->SweepSingleByChannel(Hit, Start, End, GetWorldToGravityTransform(), TraceChannel, BoxShape, Params, ResponseParam); 1829 | } 1830 | 1831 | // if we hit a wall on the side of the box (not the edge or bottom), then we have to slide since this isn't a valid move for a flat base. 1832 | if (bBlockingHit && !Hit.bStartPenetrating && FMath::Abs(Hit.ImpactNormal.Z) <= VERTICAL_SLOPE_NORMAL_Z) 1833 | { 1834 | //DrawDebugLine(GetWorld(), Start, End, FColor::Blue, false, 10.0f, 0, 0.5f); 1835 | //UE_LOG(LogTemp, Log, TEXT("sliding on z: %f"), Hit.ImpactNormal.Z); 1836 | // Blocked horizontally by box, compute new trajectory 1837 | NewDelta = UMovementComponent::ComputeSlideVector(Delta, 1.0f, Hit.ImpactNormal, Hit); 1838 | // override capsule hit with box hit 1839 | // TODO: should we override some hit properties with the slide vector? 1840 | if (OutHit) 1841 | { 1842 | *OutHit = Hit; 1843 | } 1844 | // reverse the move 1845 | FHitResult DiscardHit; 1846 | Super::MoveUpdatedComponentImpl(NewDelta - Delta, NewRotation, bSweep, &DiscardHit, Teleport); 1847 | 1848 | //DrawDebugLine(GetWorld(), Start, Start + NewDelta, FColor::Green, false, 10.0f, 0, 0.5f); 1849 | } 1850 | } 1851 | } 1852 | 1853 | return bResult; 1854 | } 1855 | 1856 | bool UPBPlayerMovement::CanAttemptJump() const 1857 | { 1858 | bool bCanAttemptJump = IsJumpAllowed(); 1859 | if (IsMovingOnGround()) 1860 | { 1861 | const float FloorZ = FVector(0.0f, 0.0f, 1.0f) | CurrentFloor.HitResult.ImpactNormal; 1862 | const float WalkableFloor = GetWalkableFloorZ(); 1863 | bCanAttemptJump &= (FloorZ >= WalkableFloor) || FMath::IsNearlyEqual(FloorZ, WalkableFloor); 1864 | } 1865 | else if (!IsFalling()) 1866 | { 1867 | bCanAttemptJump &= IsOnLadder(); 1868 | } 1869 | return bCanAttemptJump; 1870 | } 1871 | 1872 | float UPBPlayerMovement::GetMaxSpeed() const 1873 | { 1874 | if (MovementMode != MOVE_Walking && MovementMode != MOVE_NavWalking && MovementMode != MOVE_Falling && MovementMode != MOVE_Flying) 1875 | { 1876 | return Super::GetMaxSpeed(); 1877 | } 1878 | 1879 | if (MovementMode == MOVE_Flying && !IsOnLadder() && !bCheatFlying) 1880 | { 1881 | return Super::GetMaxSpeed(); 1882 | } 1883 | 1884 | if (bCheatFlying) 1885 | { 1886 | return (PBPlayerCharacter->IsSprinting() ? SprintSpeed : WalkSpeed) * 1.5f; 1887 | } 1888 | // No suit can only crouch and walk. 1889 | if (!PBPlayerCharacter->IsSuitEquipped()) 1890 | { 1891 | if (IsCrouching() && bCrouchFrameTolerated) 1892 | { 1893 | return MaxWalkSpeedCrouched; 1894 | } 1895 | return WalkSpeed; 1896 | } 1897 | float Speed; 1898 | if (ShouldCrouchSlide()) 1899 | { 1900 | Speed = MinCrouchSlideBoost * MaxCrouchSlideVelocityBoost; 1901 | } 1902 | else if (IsCrouching() && bCrouchFrameTolerated) 1903 | { 1904 | Speed = MaxWalkSpeedCrouched; 1905 | } 1906 | else if (PBPlayerCharacter->IsSprinting()) 1907 | { 1908 | Speed = SprintSpeed; 1909 | } 1910 | else if (PBPlayerCharacter->DoesWantToWalk()) 1911 | { 1912 | Speed = WalkSpeed; 1913 | } 1914 | else 1915 | { 1916 | Speed = RunSpeed; 1917 | } 1918 | 1919 | return Speed; 1920 | } 1921 | 1922 | bool IsSmallBody(const FBodyInstance* Body, float SizeThreshold, float MassThreshold) 1923 | { 1924 | if (!Body) 1925 | { 1926 | return false; 1927 | } 1928 | 1929 | // if small mass or small size, the body is considered small 1930 | 1931 | if (Body->GetBodyMass() < MassThreshold) 1932 | { 1933 | return true; 1934 | } 1935 | 1936 | const FVector Bounds = Body->GetBodyBounds().GetExtent(); 1937 | return Bounds.SizeSquared() < SizeThreshold * SizeThreshold; 1938 | } 1939 | 1940 | void UPBPlayerMovement::ApplyDownwardForce(float DeltaSeconds) 1941 | { 1942 | if (!CurrentFloor.HitResult.IsValidBlockingHit() || StandingDownwardForceScale == 0.0f) 1943 | { 1944 | return; 1945 | } 1946 | 1947 | UPrimitiveComponent* BaseComp = CurrentFloor.HitResult.GetComponent(); 1948 | if (!BaseComp || BaseComp->Mobility != EComponentMobility::Movable) 1949 | { 1950 | return; 1951 | } 1952 | 1953 | FBodyInstance* BI = BaseComp->GetBodyInstance(CurrentFloor.HitResult.BoneName); 1954 | if (BI && BI->IsInstanceSimulatingPhysics() && !IsSmallBody(BI, 64.0f, 15.0f)) 1955 | { 1956 | const FVector Gravity = -GetGravityDirection() * GetGravityZ(); 1957 | 1958 | if (!Gravity.IsZero()) 1959 | { 1960 | BI->AddForceAtPosition(Gravity * Mass * StandingDownwardForceScale, CurrentFloor.HitResult.ImpactPoint); 1961 | } 1962 | } 1963 | } 1964 | 1965 | UPBMoveStepSound* UPBPlayerMovement::GetMoveStepSoundBySurface(EPhysicalSurface SurfaceType) const 1966 | { 1967 | TSubclassOf* GotSound = GetPBCharacter()->GetMoveStepSound(TEnumAsByte(SurfaceType)); 1968 | 1969 | if (GotSound) 1970 | { 1971 | return GotSound->GetDefaultObject(); 1972 | } 1973 | 1974 | return nullptr; 1975 | } 1976 | -------------------------------------------------------------------------------- /Source/PBCharacterMovement/Private/PBCharacterMovementModule.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Project Borealis 2 | 3 | #include "PBCharacterMovementModule.h" 4 | 5 | IMPLEMENT_MODULE(FPBCharacterMovementModule, PBCharacterMovement) 6 | -------------------------------------------------------------------------------- /Source/PBCharacterMovement/Public/Character/PBPlayerCharacter.h: -------------------------------------------------------------------------------- 1 | // Copyright Project Borealis 2 | 3 | #pragma once 4 | 5 | #include "GameFramework/Character.h" 6 | 7 | #include "PBPlayerCharacter.generated.h" 8 | 9 | class USoundCue; 10 | class UPBMoveStepSound; 11 | class UPBPlayerMovement; 12 | 13 | inline float SimpleSpline(float Value) 14 | { 15 | const float ValueSquared = Value * Value; 16 | return (3.0f * ValueSquared - 2.0f * ValueSquared * Value); 17 | } 18 | 19 | UCLASS(config = Game) 20 | class PBCHARACTERMOVEMENT_API APBPlayerCharacter : public ACharacter 21 | { 22 | GENERATED_BODY() 23 | 24 | public: 25 | static const float CAPSULE_RADIUS; 26 | static const float CAPSULE_HEIGHT; 27 | 28 | APBPlayerCharacter(const FObjectInitializer& ObjectInitializer); 29 | 30 | void BeginPlay() override; 31 | void Tick(float DeltaTime) override; 32 | 33 | void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; 34 | 35 | /* Triggered when player's movement mode has changed */ 36 | void OnMovementModeChanged(EMovementMode PrevMovementMode, uint8 PrevCustomMode) override; 37 | 38 | void ClearJumpInput(float DeltaTime) override; 39 | void Jump() override; 40 | void StopJumping() override; 41 | void OnJumped_Implementation() override; 42 | bool CanJumpInternal_Implementation() const override; 43 | 44 | bool CanCrouch() const override; 45 | 46 | void RecalculateBaseEyeHeight() override; 47 | 48 | void ApplyDamageMomentum(float DamageTaken, FDamageEvent const& DamageEvent, APawn* PawnInstigator, AActor* DamageCauser) override; 49 | 50 | /** 51 | * Called via input to turn at a given rate. 52 | * @param Rate This is a normalized rate, i.e. 1.0 means 100% of desired 53 | * turn rate 54 | */ 55 | UFUNCTION() 56 | void Turn(float Rate); 57 | 58 | /** 59 | * Called via input to turn look up/down at a given rate. 60 | * @param Rate This is a normalized rate, i.e. 1.0 means 100% of desired 61 | * turn rate 62 | */ 63 | UFUNCTION() 64 | void LookUp(float Rate); 65 | 66 | /** 67 | * Move forward/back 68 | * 69 | * @param Val Movment input to apply 70 | */ 71 | void MoveForward(float Val); 72 | 73 | /** 74 | * Strafe right/left 75 | * 76 | * @param Val Movment input to apply 77 | */ 78 | void MoveRight(float Val); 79 | 80 | /** 81 | * Move Up/Down in allowed movement modes. 82 | * 83 | * @param Val Movment input to apply 84 | */ 85 | void MoveUp(float Val); 86 | 87 | /* Frame rate independent turn */ 88 | void TurnAtRate(float Val); 89 | 90 | /* Frame rate independent lookup */ 91 | void LookUpAtRate(float Val); 92 | 93 | void AddControllerYawInput(float Val) override; 94 | 95 | void AddControllerPitchInput(float Val) override; 96 | 97 | /** Get whether the player is on a ladder at the moment */ 98 | UFUNCTION() 99 | bool IsOnLadder() const; 100 | 101 | /** Gets the player's current fall speed */ 102 | UFUNCTION(Category = "Player Movement", BlueprintCallable) 103 | float GetFallSpeed(bool bAfterLand = false); 104 | 105 | UFUNCTION() 106 | void OnCrouch(); 107 | 108 | UFUNCTION() 109 | void OnUnCrouch(); 110 | 111 | UFUNCTION() 112 | void CrouchToggle(); 113 | 114 | /** */ 115 | UFUNCTION(BlueprintCallable) 116 | bool CanWalkOn(const FHitResult& Hit) const; 117 | 118 | TSubclassOf* GetMoveStepSound(TEnumAsByte Surface) { return MoveStepSounds.Find(Surface); } 119 | 120 | #pragma region Mutators 121 | 122 | UFUNCTION(Category = "PB Getters", BlueprintPure) 123 | bool IsSprinting() const 124 | { 125 | return bIsSprinting; 126 | } 127 | UFUNCTION(Category = "PB Setters", BlueprintCallable) 128 | void SetSprinting(bool bSprint) 129 | { 130 | bIsSprinting = bSprint; 131 | } 132 | UFUNCTION(Category = "PB Getters", BlueprintPure) 133 | bool DoesWantToWalk() const 134 | { 135 | return bWantsToWalk; 136 | } 137 | UFUNCTION(Category = "PB Setters", BlueprintCallable) 138 | void SetWantsToWalk(bool bWalk) 139 | { 140 | bWantsToWalk = bWalk; 141 | } 142 | UFUNCTION(Category = "PB Getters", BlueprintPure) 143 | FORCEINLINE float GetBaseTurnRate() const 144 | { 145 | return BaseTurnRate; 146 | }; 147 | UFUNCTION(Category = "PB Setters", BlueprintCallable) 148 | void SetBaseTurnRate(float val) 149 | { 150 | BaseTurnRate = val; 151 | }; 152 | UFUNCTION(Category = "PB Getters", BlueprintPure) 153 | FORCEINLINE float GetBaseLookUpRate() const 154 | { 155 | return BaseLookUpRate; 156 | }; 157 | UFUNCTION(Category = "PB Setters", BlueprintCallable) 158 | void SetBaseLookUpRate(float val) 159 | { 160 | BaseLookUpRate = val; 161 | }; 162 | UFUNCTION(Category = "PB Getters", BlueprintPure) 163 | FORCEINLINE bool GetAutoBunnyhop() const 164 | { 165 | return bAutoBunnyhop; 166 | }; 167 | UFUNCTION(Category = "PB Setters", BlueprintCallable) 168 | void SetAutoBunnyhop(bool val) 169 | { 170 | bAutoBunnyhop = val; 171 | }; 172 | UFUNCTION(Category = "PB Getters", BlueprintPure) 173 | FORCEINLINE UPBPlayerMovement* GetMovementPtr() const 174 | { 175 | return MovementPtr; 176 | }; 177 | UFUNCTION(Category = "PB Getters", BlueprintPure) 178 | bool IsSuitEquipped() const 179 | { 180 | return bSuitEquipped; 181 | } 182 | UFUNCTION(Category = "PB Setters", BlueprintCallable) 183 | void SetSuitEquipped(bool bEquipped, bool bAdmire = true) 184 | { 185 | bSuitEquipped = bEquipped; 186 | } 187 | 188 | UFUNCTION(Category = "PB Getters", BlueprintPure) 189 | float GetDefaultBaseEyeHeight() const 190 | { 191 | return DefaultBaseEyeHeight; 192 | } 193 | 194 | UFUNCTION(Category = "PB Getters", BlueprintPure) 195 | float GetMinSpeedForFallDamage() const 196 | { 197 | return MinSpeedForFallDamage; 198 | }; 199 | UFUNCTION(Category = "PB Getters", BlueprintPure) 200 | float GetFatalFallSpeed() const 201 | { 202 | return FatalFallSpeed; 203 | }; 204 | UFUNCTION(Category = "PB Getters", BlueprintPure) 205 | float GetMinLandBounceSpeed() const 206 | { 207 | return MinLandBounceSpeed; 208 | } 209 | 210 | #pragma endregion Mutators 211 | 212 | UFUNCTION() 213 | void ToggleNoClip(); 214 | 215 | protected: 216 | /** Returns Mesh1P subobject **/ 217 | FORCEINLINE USkeletalMeshComponent* GetMesh1P() const { return Mesh1P; } 218 | 219 | /** Default crouched eye height */ 220 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Camera) 221 | float FullCrouchedEyeHeight; 222 | 223 | private: 224 | /** pawn mesh: 1st person view */ 225 | UPROPERTY(VisibleDefaultsOnly, Category = Mesh) 226 | TObjectPtr Mesh1P; 227 | 228 | /** cached default eye height */ 229 | float DefaultBaseEyeHeight; 230 | 231 | /** throttle jump boost when going up a ramp, so we don't spam it */ 232 | float LastJumpBoostTime; 233 | 234 | /** maximum time it takes to jump */ 235 | float MaxJumpTime; 236 | 237 | /** Base turn rate, in deg/sec. Other scaling may affect final turn rate. */ 238 | UPROPERTY(VisibleAnywhere, meta = (AllowPrivateAccess = "true"), Category = "PB Player|Camera") 239 | float BaseTurnRate; 240 | 241 | /** Base look up/down rate, in deg/sec. Other scaling may affect final rate.*/ 242 | UPROPERTY(VisibleAnywhere, meta = (AllowPrivateAccess = "true"), Category = "PB Player|Camera") 243 | float BaseLookUpRate; 244 | 245 | /** Automatic bunnyhopping */ 246 | UPROPERTY(EditAnywhere, meta = (AllowPrivateAccess = "true"), Category = "PB Player|Gameplay") 247 | bool bAutoBunnyhop; 248 | 249 | /** Has the suit */ 250 | UPROPERTY(EditAnywhere, meta = (AllowPrivateAccess = "true"), Category = "PB Player|Gameplay") 251 | bool bSuitEquipped = true; 252 | 253 | /** Move step sounds by physical surface */ 254 | UPROPERTY(EditDefaultsOnly, meta = (AllowPrivateAccess = "true"), Category = "PB Player|Sounds") 255 | TMap, TSubclassOf> MoveStepSounds; 256 | 257 | /** Minimum speed to play the camera shake for landing */ 258 | UPROPERTY(EditDefaultsOnly, meta = (AllowPrivateAccess = "true"), Category = "PB Player|Damage") 259 | float MinLandBounceSpeed; 260 | 261 | /** Don't take damage below this speed - so jumping doesn't damage */ 262 | UPROPERTY(EditDefaultsOnly, meta = (AllowPrivateAccess = "true"), Category = "PB Player|Damage") 263 | float MinSpeedForFallDamage; 264 | 265 | /** If you're going faster than this, you're dead */ 266 | UPROPERTY(EditDefaultsOnly, meta = (AllowPrivateAccess = "true"), Category = "PB Player|Damage") 267 | float FatalFallSpeed; 268 | 269 | // In HL2, the player has the Z component for applying momentum to the capsule capped 270 | UPROPERTY(EditDefaultsOnly, meta = (AllowPrivateAccess = "true"), Category = "PB Player|Damage") 271 | float CapDamageMomentumZ = 0.f; 272 | 273 | /** Pointer to player movement component */ 274 | UPROPERTY() 275 | TObjectPtr MovementPtr; 276 | 277 | /** True if we're sprinting*/ 278 | UPROPERTY(Transient, Replicated) 279 | bool bIsSprinting; 280 | 281 | UPROPERTY(Transient, Replicated) 282 | bool bWantsToWalk; 283 | 284 | /** defer the jump stop for a frame (for early jumps) */ 285 | bool bDeferJumpStop = false; 286 | }; 287 | -------------------------------------------------------------------------------- /Source/PBCharacterMovement/Public/Character/PBPlayerMovement.h: -------------------------------------------------------------------------------- 1 | // Copyright Project Borealis 2 | 3 | #pragma once 4 | 5 | #include "GameFramework/CharacterMovementComponent.h" 6 | 7 | #include "PBPlayerCharacter.h" 8 | 9 | #include "PBPlayerMovement.generated.h" 10 | 11 | constexpr float LADDER_MOUNT_TIMEOUT = 0.2f; 12 | 13 | // Crouch Timings (in seconds) 14 | constexpr float MOVEMENT_DEFAULT_CROUCHTIME = 0.4f; 15 | constexpr float MOVEMENT_DEFAULT_CROUCHJUMPTIME = 0.0f; 16 | constexpr float MOVEMENT_DEFAULT_UNCROUCHTIME = 0.2f; 17 | constexpr float MOVEMENT_DEFAULT_UNCROUCHJUMPTIME = 0.8f; 18 | 19 | class USoundCue; 20 | 21 | constexpr float DesiredGravity = -1143.0f; 22 | 23 | UCLASS() 24 | class PBCHARACTERMOVEMENT_API UPBPlayerMovement : public UCharacterMovementComponent 25 | { 26 | GENERATED_BODY() 27 | 28 | protected: 29 | /** If the player is using a ladder */ 30 | UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Gameplay) 31 | bool bOnLadder; 32 | 33 | /** Should crouch slide? */ 34 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement: Walking") 35 | bool bShouldCrouchSlide; 36 | 37 | /** If the player is currently crouch sliding */ 38 | UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Gameplay) 39 | bool bCrouchSliding; 40 | 41 | /** schedule a crouch slide to landing */ 42 | bool bDeferCrouchSlideToLand; 43 | 44 | /** Time crouch sliding started */ 45 | float CrouchSlideStartTime; 46 | 47 | /** How long a crouch slide boosts for */ 48 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement: Walking") 49 | float CrouchSlideBoostTime; 50 | 51 | /** The minimum starting boost */ 52 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement: Walking") 53 | float MinCrouchSlideBoost; 54 | 55 | /** The factor for determining the initial crouch slide boost up a slope */ 56 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement: Walking") 57 | float CrouchSlideBoostSlopeFactor; 58 | 59 | /** How much to multiply initial velocity by when starting a crouch slide */ 60 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement: Walking") 61 | float CrouchSlideBoostMultiplier; 62 | 63 | /** How much forward velocity player needs relative to sprint speed in order to start a crouch slide */ 64 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement: Walking") 65 | float CrouchSlideSpeedRequirementMultiplier; 66 | 67 | /** The max velocity multiplier for acceleration in crouch sliding */ 68 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement: Walking") 69 | float MaxCrouchSlideVelocityBoost; 70 | 71 | /** The min velocity multiplier for acceleration in crouch sliding */ 72 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement: Walking") 73 | float MinCrouchSlideVelocityBoost; 74 | 75 | /** Time before being able to crouch slide again */ 76 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement: Walking") 77 | float CrouchSlideCooldown; 78 | 79 | /** Enter crouch slide mode, giving the player a boost and adjusting camera effects */ 80 | void StartCrouchSlide(); 81 | /** If crouch sliding mode is turned on and valid in the current movement state and thus should occur */ 82 | bool ShouldCrouchSlide() const; 83 | 84 | /** The time that the player can remount on the ladder */ 85 | float OffLadderTicks = -1.0f; 86 | 87 | /** The multiplier for acceleration when on ground. */ 88 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement: Walking") 89 | float GroundAccelerationMultiplier; 90 | 91 | /** The multiplier for acceleration when in air. */ 92 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement: Jumping / Falling") 93 | float AirAccelerationMultiplier; 94 | 95 | /* The vector differential magnitude cap when in air. */ 96 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement: Jumping / Falling") 97 | float AirSpeedCap; 98 | 99 | /* The vector differential magnitude cap when in air and sliding. This is here to give player less momentum control while sliding on a slope, but giving more control while jumping using AirSpeedCap. */ 100 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement: Jumping / Falling") 101 | float AirSlideSpeedCap; 102 | 103 | /* Proportion of player input acceleration (0 to disable, 0.5 for half, 2 for double, etc.) to use for the horizontal air dash. */ 104 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement: Jumping / Falling") 105 | float AirJumpDashMagnitude = 0.0f; 106 | 107 | /* If an air jump resets all horizontal movement. Useful in tandom with air dash to reset all velocity to a new direction. */ 108 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement: Jumping / Falling") 109 | bool bAirJumpResetsHorizontal = false; 110 | 111 | /** Time to crouch on ground in seconds */ 112 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement: Walking") 113 | float CrouchTime; 114 | 115 | /** Time to uncrouch on ground in seconds */ 116 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement: Walking") 117 | float UncrouchTime; 118 | 119 | /** Time to crouch in air in seconds */ 120 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement: Walking") 121 | float CrouchJumpTime; 122 | 123 | /** Time to uncrouch in air in seconds */ 124 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement: Walking") 125 | float UncrouchJumpTime; 126 | 127 | /** Speed on a ladder */ 128 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement: Ladder") 129 | float LadderSpeed; 130 | 131 | /** Ladder timeout */ 132 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement: Ladder") 133 | float LadderTimeout; 134 | 135 | /** the minimum step height from moving fast */ 136 | UPROPERTY(Category = "Character Movement: Walking", EditAnywhere, BlueprintReadWrite) 137 | float MinStepHeight; 138 | 139 | /** the fraction of MaxStepHeight to use for step down height, otherwise player enters air move state and lets gravity do the work. */ 140 | UPROPERTY(Category = "Character Movement: Walking", EditAnywhere, BlueprintReadWrite) 141 | float StepDownHeightFraction; 142 | 143 | /** Friction multiplier to use when on an edge */ 144 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement: Walking") 145 | float EdgeFrictionMultiplier; 146 | 147 | /** height away from a floor to apply edge friction */ 148 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement: Walking") 149 | float EdgeFrictionHeight; 150 | 151 | /** distance in the direction of movement to look ahead for the edge */ 152 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement: Walking") 153 | float EdgeFrictionDist; 154 | 155 | /** only apply edge friction when braking (no acceleration) */ 156 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement: Walking") 157 | bool bEdgeFrictionOnlyWhenBraking; 158 | 159 | /** apply edge friction when crouching, even if not braking */ 160 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement: Walking") 161 | float bEdgeFrictionAlwaysWhenCrouching; 162 | 163 | /** Time the player has before applying friction. */ 164 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement: Jumping / Falling") 165 | float BrakingWindow; 166 | 167 | /* Progress checked against the Braking Window */ 168 | float BrakingWindowTimeElapsed; 169 | 170 | /** If the player has already landed for a frame, and breaking may be applied. */ 171 | bool bBrakingFrameTolerated; 172 | 173 | /** Wait a frame before crouch speed. */ 174 | bool bCrouchFrameTolerated; 175 | 176 | /** If in the crouching transition */ 177 | bool bIsInCrouchTransition; 178 | 179 | /** If the player is currently locked in crouch state */ 180 | bool bLockInCrouch = false; 181 | 182 | APBPlayerCharacter* GetPBCharacter() const { return PBPlayerCharacter; } 183 | 184 | /** The PB player character */ 185 | UPROPERTY(Transient, DuplicateTransient) 186 | TObjectPtr PBPlayerCharacter; 187 | 188 | /** The target ground speed when running. */ 189 | UPROPERTY(Category = "Character Movement: Walking", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", UIMin = "0")) 190 | float RunSpeed; 191 | 192 | /** The target ground speed when sprinting. */ 193 | UPROPERTY(Category = "Character Movement: Walking", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", UIMin = "0")) 194 | float SprintSpeed; 195 | 196 | /** The target ground speed when walking slowly. */ 197 | UPROPERTY(Category = "Character Movement: Walking", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", UIMin = "0")) 198 | float WalkSpeed; 199 | 200 | /** The minimum speed to scale up from for slope movement */ 201 | UPROPERTY(Category = "Character Movement: Walking", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", UIMin = "0")) 202 | float SpeedMultMin; 203 | 204 | /** The maximum speed to scale up to for slope movement */ 205 | UPROPERTY(Category = "Character Movement: Walking", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", UIMin = "0")) 206 | float SpeedMultMax; 207 | 208 | /** The maximum angle we can roll for camera adjust */ 209 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement (General Settings)") 210 | float RollAngle; 211 | 212 | /** Speed of rolling the camera */ 213 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement (General Settings)") 214 | float RollSpeed; 215 | 216 | /** Speed of rolling the camera */ 217 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement (General Settings)") 218 | float BounceMultiplier = 0.0f; 219 | 220 | UPROPERTY(Category = "Character Movement: Walking", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", UIMin = "0")) 221 | float AxisSpeedLimit; 222 | 223 | /** Threshold relating to speed ratio and friction which causes us to catch air */ 224 | UPROPERTY(Category = "Character Movement: Walking", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", UIMin = "0")) 225 | float SlideLimit = 0.5f; 226 | 227 | /** Fraction of uncrouch half-height to check for before doing starting an uncrouch. */ 228 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement (General Settings)") 229 | float GroundUncrouchCheckFactor = 0.75f; 230 | 231 | bool bShouldPlayMoveSounds = true; 232 | 233 | /** Milliseconds between step sounds */ 234 | float MoveSoundTime = 0.0f; 235 | /** If we are stepping left, else, right */ 236 | bool StepSide = false; 237 | 238 | /** Plays sound effect according to movement and surface */ 239 | virtual void PlayMoveSound(float DeltaTime); 240 | 241 | virtual void PlayJumpSound(const FHitResult& Hit, bool bJumped); 242 | 243 | UPBMoveStepSound* GetMoveStepSoundBySurface(EPhysicalSurface SurfaceType) const; 244 | 245 | public: 246 | /** Print pos and vel (Source: cl_showpos) */ 247 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Character Movement (General Settings)") 248 | uint32 bShowPos : 1; 249 | 250 | UPBPlayerMovement(); 251 | 252 | virtual void InitializeComponent() override; 253 | virtual void OnRegister() override; 254 | 255 | // Overrides for Source-like movement 256 | virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; 257 | virtual void CalcVelocity(float DeltaTime, float Friction, bool bFluid, float BrakingDeceleration) override; 258 | virtual void ApplyVelocityBraking(float DeltaTime, float Friction, float BrakingDeceleration) override; 259 | bool ShouldLimitAirControl(float DeltaTime, const FVector& FallAcceleration) const override; 260 | FVector NewFallVelocity(const FVector& InitialVelocity, const FVector& Gravity, float DeltaTime) const override; 261 | 262 | void UpdateCharacterStateBeforeMovement(float DeltaSeconds) override; 263 | void UpdateCharacterStateAfterMovement(float DeltaSeconds) override; 264 | 265 | void UpdateSurfaceFriction(bool bIsSliding = false); 266 | void UpdateCrouching(float DeltaTime, bool bOnlyUnCrouch = false); 267 | 268 | // Overrides for crouch transitions 269 | virtual void Crouch(bool bClientSimulation = false) override; 270 | virtual void UnCrouch(bool bClientSimulation = false) override; 271 | virtual void DoCrouchResize(float TargetTime, float DeltaTime, bool bClientSimulation = false); 272 | virtual void DoUnCrouchResize(float TargetTime, float DeltaTime, bool bClientSimulation = false); 273 | 274 | bool MoveUpdatedComponentImpl(const FVector& Delta, const FQuat& NewRotation, bool bSweep, FHitResult* OutHit = nullptr, ETeleportType Teleport = ETeleportType::None) override; 275 | 276 | // Jump overrides 277 | bool CanAttemptJump() const override; 278 | bool DoJump(bool bClientSimulation) override; 279 | 280 | float GetFallSpeed(bool bAfterLand = false); 281 | 282 | /** Exit crouch slide mode, and stop camera effects */ 283 | void StopCrouchSliding(); 284 | 285 | /** If the crouch lock should be toggled on or off, crouch lock locks the player in their crouch state */ 286 | void ToggleCrouchLock(bool bLock); 287 | 288 | void TwoWallAdjust(FVector& OutDelta, const FHitResult& Hit, const FVector& OldHitNormal) const override; 289 | float SlideAlongSurface(const FVector& Delta, float Time, const FVector& Normal, FHitResult& Hit, bool bHandleImpact = false) override; 290 | FVector ComputeSlideVector(const FVector& Delta, const float Time, const FVector& Normal, const FHitResult& Hit) const override; 291 | FVector HandleSlopeBoosting(const FVector& SlideResult, const FVector& Delta, const float Time, const FVector& Normal, const FHitResult& Hit) const override; 292 | bool ShouldCatchAir(const FFindFloorResult& OldFloor, const FFindFloorResult& NewFloor) override; 293 | bool IsWithinEdgeTolerance(const FVector& CapsuleLocation, const FVector& TestImpactPoint, const float CapsuleRadius) const override; 294 | bool ShouldCheckForValidLandingSpot(float DeltaTime, const FVector& Delta, const FHitResult& Hit) const override; 295 | void HandleImpact(const FHitResult& Hit, float TimeSlice = 0.0f, const FVector& MoveDelta = FVector::ZeroVector) override; 296 | bool IsValidLandingSpot(const FVector& CapsuleLocation, const FHitResult& Hit) const override; 297 | 298 | /** called when player does an air jump. can be used for SFX/VFX. */ 299 | UFUNCTION(BlueprintImplementableEvent) 300 | void OnAirJump(int32 JumpTimes); 301 | 302 | float GetFrictionFromHit(const FHitResult& Hit) const; 303 | void TraceCharacterFloor(FHitResult& OutHit) const; 304 | void TraceLineToFloor(FHitResult& OutHit) const; 305 | 306 | // Acceleration 307 | FORCEINLINE FVector GetAcceleration() const { return Acceleration; } 308 | 309 | // Crouch locked 310 | FORCEINLINE bool GetCrouchLocked() const { return bLockInCrouch; } 311 | 312 | float GetSprintSpeed() const { return SprintSpeed; } 313 | 314 | void OnMovementModeChanged(EMovementMode PreviousMovementMode, uint8 PreviousCustomMode) override; 315 | 316 | /** Do camera roll effect based on velocity */ 317 | float GetCameraRoll(); 318 | 319 | /** Is this player on a ladder? */ 320 | UFUNCTION(BlueprintCallable) 321 | bool IsOnLadder() const; 322 | 323 | /** Return the speed used to climb ladders */ 324 | float GetLadderClimbSpeed() const; 325 | 326 | void SetNoClip(bool bNoClip); 327 | 328 | /** Toggle no clip */ 329 | void ToggleNoClip(); 330 | 331 | bool IsBrakingFrameTolerated() const { return bBrakingFrameTolerated; } 332 | 333 | bool IsInCrouchTransition() const { return bIsInCrouchTransition; } 334 | 335 | bool IsCrouchSliding() const { return bCrouchSliding; } 336 | 337 | void SetShouldPlayMoveSounds(bool bShouldPlay) { bShouldPlayMoveSounds = bShouldPlay; } 338 | 339 | virtual float GetMaxSpeed() const override; 340 | 341 | virtual void ApplyDownwardForce(float DeltaSeconds) override; 342 | 343 | private: 344 | float DefaultStepHeight; 345 | float DefaultSpeedMultMin; 346 | float DefaultSpeedMultMax; 347 | float DefaultWalkableFloorZ; 348 | float SurfaceFriction; 349 | TWeakObjectPtr OldBase; 350 | 351 | /** If we have done an initial landing */ 352 | bool bHasEverLanded = false; 353 | 354 | /** if we're currently sliding in air */ 355 | bool bSlidingInAir = false; 356 | 357 | /** if we were sliding in air in the prior frame */ 358 | bool bWasSlidingInAir = false; 359 | 360 | bool bHasDeferredMovementMode; 361 | EMovementMode DeferredMovementMode; 362 | }; 363 | -------------------------------------------------------------------------------- /Source/PBCharacterMovement/Public/PBCharacterMovementModule.h: -------------------------------------------------------------------------------- 1 | // Copyright Project Borealis 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Modules/ModuleManager.h" 7 | 8 | class FPBCharacterMovementModule : public IModuleInterface {}; 9 | -------------------------------------------------------------------------------- /Source/PBCharacterMovement/Public/Sound/PBMoveStepSound.h: -------------------------------------------------------------------------------- 1 | // Copyright Project Borealis 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | 7 | #include "Chaos/ChaosEngineInterface.h" 8 | 9 | #include "PBMoveStepSound.generated.h" 10 | 11 | class USoundCue; 12 | 13 | /** 14 | * 15 | */ 16 | UCLASS(Blueprintable) 17 | class PBCHARACTERMOVEMENT_API UPBMoveStepSound : public UObject 18 | { 19 | GENERATED_BODY() 20 | public: 21 | UFUNCTION() 22 | TEnumAsByte GetSurfaceMaterial() const { return SurfaceMaterial; } 23 | 24 | UFUNCTION() 25 | TArray GetStepLeftSounds() const { return StepLeftSounds; } 26 | 27 | UFUNCTION() 28 | TArray GetStepRightSounds() const { return StepRightSounds; } 29 | 30 | UFUNCTION() 31 | TArray GetSprintLeftSounds() const { return SprintLeftSounds; } 32 | 33 | UFUNCTION() 34 | TArray GetSprintRightSounds() const { return SprintRightSounds; } 35 | 36 | UFUNCTION() 37 | TArray GetJumpSounds() const { return JumpSounds; } 38 | 39 | UFUNCTION() 40 | TArray GetLandSounds() const { return LandSounds; } 41 | 42 | UFUNCTION() 43 | float GetWalkVolume() const { return WalkVolume; } 44 | 45 | UFUNCTION() 46 | float GetSprintVolume() const { return SprintVolume; } 47 | 48 | private: 49 | /** The physical material associated with this move step sound */ 50 | UPROPERTY(EditDefaultsOnly, Category = Material) 51 | TEnumAsByte SurfaceMaterial; 52 | 53 | /** The list of sounds to randomly choose from when stepping left */ 54 | UPROPERTY(EditDefaultsOnly, Category = Sounds) 55 | TArray> StepLeftSounds; 56 | 57 | /** The list of sounds to randomly choose from when stepping right */ 58 | UPROPERTY(EditDefaultsOnly, Category = Sounds) 59 | TArray> StepRightSounds; 60 | 61 | /** The list of sounds to randomly choose from when sprinting left */ 62 | UPROPERTY(EditDefaultsOnly, Category = Sounds) 63 | TArray> SprintLeftSounds; 64 | 65 | /** The list of sounds to randomly choose from when sprinting right */ 66 | UPROPERTY(EditDefaultsOnly, Category = Sounds) 67 | TArray> SprintRightSounds; 68 | 69 | /** The list of sounds to randomly choose from when jumping */ 70 | UPROPERTY(EditDefaultsOnly, Category = Sounds) 71 | TArray> JumpSounds; 72 | 73 | /** The list of sounds to randomly choose from when landing */ 74 | UPROPERTY(EditDefaultsOnly, Category = Sounds) 75 | TArray> LandSounds; 76 | 77 | UPROPERTY(EditDefaultsOnly, Category = Volume) 78 | float WalkVolume = 0.2f; 79 | 80 | UPROPERTY(EditDefaultsOnly, Category = Volume) 81 | float SprintVolume = 0.5f; 82 | }; 83 | --------------------------------------------------------------------------------