diff --git a/Source/ALS/Private/AlsCharacter_Actions.cpp b/Source/ALS/Private/AlsCharacter_Actions.cpp index a9559fa0f..2a604868c 100644 --- a/Source/ALS/Private/AlsCharacter_Actions.cpp +++ b/Source/ALS/Private/AlsCharacter_Actions.cpp @@ -714,6 +714,29 @@ void AAlsCharacter::StartRagdollingImplementation() AnimationInstance->UnFreezeRagdolling(); + // Initialize Ragdolling State + + RagdollingState.PullForce = 0.0f; + RagdollingState.ElapsedTime = 0.0f; + RagdollingState.TimeAfterGrounded = RagdollingState.TimeAfterGroundedAndStopped = 0.0f; + RagdollingState.bFacingUpward = RagdollingState.bGrounded = false; + RagdollingState.bFreezing = false; + RagdollingState.PrevActorLocation = GetActorLocation(); + + if (Settings->Ragdolling.bLimitInitialRagdollSpeed) + { + // Limit the ragdoll's speed for a few frames, because for some unclear reason, + // it can get a much higher initial speed than the character's last speed. + // TODO Find a better solution or wait for a fix in future engine versions. + + auto& MinSpeedLimit{Settings->Ragdolling.MinSpeedLimit}; + + RagdollingState.SpeedLimitFrameTimeRemaining = Settings->Ragdolling.SpeedLimitDurationFrame; + RagdollingState.SpeedLimit = FMath::Max(MinSpeedLimit, LocomotionState.Velocity.Size()); + + LimitRagdollSpeed(); + } + // Initialize bFacingUpward flag by current movement direction. If Velocity is Zero, it is chosen bFacingUpward is true. // And determine target yaw angle of the character. @@ -757,12 +780,6 @@ void AAlsCharacter::StartRagdollingImplementation() SetLocomotionAction(AlsLocomotionActionTags::Ragdolling); - RagdollingState.ElapsedTime = 0.0f; - RagdollingState.TimeAfterGrounded = RagdollingState.TimeAfterGroundedAndStopped = 0.0f; - RagdollingState.bFacingUpward = RagdollingState.bGrounded = false; - RagdollingState.bFreezing = false; - RagdollingState.PrevActorLocation = GetActorLocation(); - OnRagdollingStarted(); } @@ -822,6 +839,47 @@ void AAlsCharacter::RefreshRagdolling(const float DeltaTime) RagdollingState.bGrounded = bGrounded; SetActorLocation(NewActorLocation, true); + // Zero target location means that it hasn't been replicated yet, so we can't apply the logic below. + + if (!bLocallyControlled && !RagdollTargetLocation.IsZero()) + { + // Apply ragdoll location corrections. + + auto& PullForce{Settings->Ragdolling.TargetPullForce}; + auto& InterpolationSpeed{Settings->Ragdolling.PullForceInterpolationSpeed}; + + RagdollingState.PullForce = FMath::FInterpTo(RagdollingState.PullForce, PullForce, DeltaTime, InterpolationSpeed); + + const auto HorizontalSpeedSquared{AlsCharacterMovement->Velocity.SizeSquared2D()}; + + const auto PullForceBoneName{ + HorizontalSpeedSquared > FMath::Square(300.0f) ? UAlsConstants::Spine03BoneName() : UAlsConstants::PelvisBoneName() + }; + + auto* PullForceBody{GetMesh()->GetBodyInstance(PullForceBoneName)}; + + FPhysicsCommand::ExecuteWrite(PullForceBody->ActorHandle, [this](const FPhysicsActorHandle& ActorHandle) + { + if (!FPhysicsInterface::IsRigidBody(ActorHandle)) + { + return; + } + + const auto PullForceVector{ + RagdollTargetLocation - FPhysicsInterface::GetTransform_AssumesLocked(ActorHandle, true).GetLocation() + }; + + auto& MinPullForceDistance{Settings->Ragdolling.MinPullForceDistance}; + auto& MaxPullForceDistance{Settings->Ragdolling.MaxPullForceDistance}; + + if (PullForceVector.SizeSquared() > FMath::Square(MinPullForceDistance)) + { + FPhysicsInterface::AddForce_AssumesLocked( + ActorHandle, PullForceVector.GetClampedToMaxSize(MaxPullForceDistance) * RagdollingState.PullForce, true, true); + } + }); + } + if (IsRagdollingGroundedAndAged()) { // Determine whether the ragdoll is facing upward or downward. @@ -846,6 +904,15 @@ void AAlsCharacter::RefreshRagdolling(const float DeltaTime) } } + // Limit the speed of ragdoll bodies. + + if (RagdollingState.SpeedLimitFrameTimeRemaining > 0) + { + RagdollingState.SpeedLimitFrameTimeRemaining -= 1; + + LimitRagdollSpeed(); + } + AnimationInstance->UpdateRagdollingAnimationState(RagdollingState); if (Settings->Ragdolling.bAllowFreeze) @@ -932,6 +999,31 @@ FVector AAlsCharacter::RagdollTraceGround(bool& bGrounded) const }; } +void AAlsCharacter::LimitRagdollSpeed() const +{ + GetMesh()->ForEachBodyBelow(NAME_None, true, false, [this](FBodyInstance* Body) + { + FPhysicsCommand::ExecuteWrite(Body->ActorHandle, [this](const FPhysicsActorHandle& ActorHandle) + { + if (!FPhysicsInterface::IsRigidBody(ActorHandle)) + { + return; + } + + auto Velocity{FPhysicsInterface::GetLinearVelocity_AssumesLocked(ActorHandle)}; + if (Velocity.SizeSquared() <= FMath::Square(RagdollingState.SpeedLimit)) + { + return; + } + + Velocity.Normalize(); + Velocity *= RagdollingState.SpeedLimit; + + FPhysicsInterface::SetLinearVelocity_AssumesLocked(ActorHandle, Velocity); + }); + }); +} + bool AAlsCharacter::IsRagdollingAllowedToStop() const { return LocomotionAction == AlsLocomotionActionTags::Ragdolling; diff --git a/Source/ALS/Public/AlsCharacter.h b/Source/ALS/Public/AlsCharacter.h index 3c9d1e035..d942fc5b9 100644 --- a/Source/ALS/Public/AlsCharacter.h +++ b/Source/ALS/Public/AlsCharacter.h @@ -586,6 +586,7 @@ class ALS_API AAlsCharacter : public ACharacter bool IsRagdollingGroundedAndAged() const; + void LimitRagdollSpeed() const; // Debug diff --git a/Source/ALS/Public/Settings/AlsRagdollingSettings.h b/Source/ALS/Public/Settings/AlsRagdollingSettings.h index 46753d84f..e45f1fd0f 100644 --- a/Source/ALS/Public/Settings/AlsRagdollingSettings.h +++ b/Source/ALS/Public/Settings/AlsRagdollingSettings.h @@ -19,6 +19,18 @@ struct ALS_API FAlsRagdollingSettings Meta = (ClampMin = 0, EditCondition = "bStartRagdollingOnLand", ForceUnits = "cm/s")) float RagdollingOnLandSpeedThreshold{1000.0f}; + // If checked, the ragdoll's speed will be limited by the character's last speed for a few frames + // after activation. This hack is used to prevent the ragdoll from getting a very high initial speed + // at unstable FPS, which can be reproduced by jumping and activating the ragdoll at the same time. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ALS") + uint8 bLimitInitialRagdollSpeed : 1 {true}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ALS|LimitInitialRagdollSpeed", Meta = (ClampMin = 0, EditCondition = "bLimitInitialRagdollSpeed")) + float MinSpeedLimit{200.0f}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ALS|LimitInitialRagdollSpeed", Meta = (ClampMin = 0, EditCondition = "bLimitInitialRagdollSpeed")) + int32 SpeedLimitDurationFrame{8}; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ALS") TEnumAsByte GroundTraceChannel{ECC_Visibility}; @@ -34,6 +46,18 @@ struct ALS_API FAlsRagdollingSettings UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ALS") TObjectPtr GetUpBackMontage{nullptr}; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ALS|Multiply") + float TargetPullForce{750.0f}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ALS|Multiply") + float PullForceInterpolationSpeed{0.6f}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ALS|Multiply", Meta = (ClampMin = 0, ForceUnits = "cm")) + float MinPullForceDistance{5.0f}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ALS|Multiply", Meta = (ClampMin = 0, ForceUnits = "cm")) + float MaxPullForceDistance{50.0f}; + // If checked, it stops the physical simulation and returns control of the bone to kinematic // when the conditions mentioned later are met. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ALS") diff --git a/Source/ALS/Public/State/AlsRagdollingState.h b/Source/ALS/Public/State/AlsRagdollingState.h index 95d80f8c3..38a55c8b9 100644 --- a/Source/ALS/Public/State/AlsRagdollingState.h +++ b/Source/ALS/Public/State/AlsRagdollingState.h @@ -6,6 +6,14 @@ USTRUCT(BlueprintType) struct ALS_API FAlsRagdollingState { GENERATED_BODY() + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ALS", Meta = (ForceUnits = "N")) + float PullForce{0.0f}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ALS", Meta = (ClampMin = 0)) + int32 SpeedLimitFrameTimeRemaining{0}; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ALS", Meta = (ClampMin = 0, ForceUnits = "cm/s")) + float SpeedLimit{0.0f}; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ALS", Meta = (ForceUnits = "deg")) float LyingDownYawAngleDelta{0.0f};