From 94d04f2250b96e34f4708dbf587a5cfb82063325 Mon Sep 17 00:00:00 2001 From: Kevin Poretti Date: Wed, 4 Jan 2023 20:50:26 -0500 Subject: [PATCH] Rotate character and update floor detection functions so you can properly land on ceilings --- .../Content/Characters/BP_GSCharacter.uasset | 4 +- GravityStomp/Content/Maps/Debug/Test.umap | 2 +- .../GSCharacterMovementComponent.cpp | 249 ++++++++++++++++++ .../Character/GSCharacterMovementComponent.h | 12 +- 4 files changed, 262 insertions(+), 5 deletions(-) diff --git a/GravityStomp/Content/Characters/BP_GSCharacter.uasset b/GravityStomp/Content/Characters/BP_GSCharacter.uasset index 540a236..a860480 100644 --- a/GravityStomp/Content/Characters/BP_GSCharacter.uasset +++ b/GravityStomp/Content/Characters/BP_GSCharacter.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d212347f7ac01ec91a86fa0401390855e61c88857652a69ad4e03695c01f74a1 -size 33962 +oid sha256:1a20bf4e0000fdd14349a353a86d96527d2c5ad6b8be19185a631276b67d4e5e +size 114464 diff --git a/GravityStomp/Content/Maps/Debug/Test.umap b/GravityStomp/Content/Maps/Debug/Test.umap index 5e77e93..3e5873f 100644 --- a/GravityStomp/Content/Maps/Debug/Test.umap +++ b/GravityStomp/Content/Maps/Debug/Test.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee4e84afccfef473907467c031698133fc1a7055160d75d324db3b82604185f7 +oid sha256:eb4b77af4aac3121eea4fd753a468000eb531179dd3f4fd1347b542bda3531cb size 47476 diff --git a/GravityStomp/Source/GravityStompGame/Character/GSCharacterMovementComponent.cpp b/GravityStomp/Source/GravityStompGame/Character/GSCharacterMovementComponent.cpp index 4747bbe..460d226 100644 --- a/GravityStomp/Source/GravityStompGame/Character/GSCharacterMovementComponent.cpp +++ b/GravityStomp/Source/GravityStompGame/Character/GSCharacterMovementComponent.cpp @@ -3,6 +3,7 @@ #include "Character/GSCharacterMovementComponent.h" // UE includes +#include "Components/CapsuleComponent.h" #include "GameFramework/Character.h" // These are defined in CharacterMovementComponent.cpp and inaccessible here. Just copy and paste too make the PhysFalling work @@ -22,10 +23,12 @@ namespace CharacterMovementCVars ECVF_Default); } + UGSCharacterMovementComponent::UGSCharacterMovementComponent() { } + void UGSCharacterMovementComponent::PhysFalling(float deltaTime, int32 Iterations) { if (deltaTime < MIN_TICK_TIME) @@ -348,6 +351,252 @@ void UGSCharacterMovementComponent::PhysFalling(float deltaTime, int32 Iteration } } + +void UGSCharacterMovementComponent::ComputeFloorDist(const FVector& CapsuleLocation, float LineDistance, + float SweepDistance, FFindFloorResult& OutFloorResult, float SweepRadius, + const FHitResult* DownwardSweepResult) const +{ + //UE_LOG(LogCharacterMovement, VeryVerbose, TEXT("[Role:%d] ComputeFloorDist: %s at location %s"), (int32)CharacterOwner->GetLocalRole(), *GetNameSafe(CharacterOwner), *CapsuleLocation.ToString()); + OutFloorResult.Clear(); + + float PawnRadius, PawnHalfHeight; + CharacterOwner->GetCapsuleComponent()->GetScaledCapsuleSize(PawnRadius, PawnHalfHeight); + + bool bSkipSweep = false; + if (DownwardSweepResult != NULL && DownwardSweepResult->IsValidBlockingHit()) + { + // Only if the supplied sweep was vertical and downward. + if ((DownwardSweepResult->TraceStart.Z > DownwardSweepResult->TraceEnd.Z) && + (DownwardSweepResult->TraceStart - DownwardSweepResult->TraceEnd).SizeSquared2D() <= UE_KINDA_SMALL_NUMBER) + { + // Reject hits that are barely on the cusp of the radius of the capsule + if (IsWithinEdgeTolerance(DownwardSweepResult->Location, DownwardSweepResult->ImpactPoint, PawnRadius)) + { + // Don't try a redundant sweep, regardless of whether this sweep is usable. + bSkipSweep = true; + + const bool bIsWalkable = IsWalkable(*DownwardSweepResult); + const float FloorDist = (CapsuleLocation.Z - DownwardSweepResult->Location.Z); + OutFloorResult.SetFromSweep(*DownwardSweepResult, FloorDist, bIsWalkable); + + if (bIsWalkable) + { + // Use the supplied downward sweep as the floor hit result. + return; + } + } + } + } + + // We require the sweep distance to be >= the line distance, otherwise the HitResult can't be interpreted as the sweep result. + if (SweepDistance < LineDistance) + { + ensure(SweepDistance >= LineDistance); + return; + } + + bool bBlockingHit = false; + FCollisionQueryParams QueryParams(SCENE_QUERY_STAT(ComputeFloorDist), false, CharacterOwner); + FCollisionResponseParams ResponseParam; + InitCollisionParams(QueryParams, ResponseParam); + const ECollisionChannel CollisionChannel = UpdatedComponent->GetCollisionObjectType(); + + // Sweep test + if (!bSkipSweep && SweepDistance > 0.f && SweepRadius > 0.f) + { + // Use a shorter height to avoid sweeps giving weird results if we start on a surface. + // This also allows us to adjust out of penetrations. + const float ShrinkScale = 0.9f; + const float ShrinkScaleOverlap = 0.1f; + float ShrinkHeight = (PawnHalfHeight - PawnRadius) * (1.f - ShrinkScale); + float TraceDist = SweepDistance + ShrinkHeight; + FCollisionShape CapsuleShape = FCollisionShape::MakeCapsule(SweepRadius, PawnHalfHeight - ShrinkHeight); + + FHitResult Hit(1.f); + bBlockingHit = FloorSweepTest(Hit, CapsuleLocation, CapsuleLocation + (CharacterUpDirection * -TraceDist), CollisionChannel, CapsuleShape, QueryParams, ResponseParam); + + if (bBlockingHit) + { + // Reject hits adjacent to us, we only care about hits on the bottom portion of our capsule. + // Check 2D distance to impact point, reject if within a tolerance from radius. + if (Hit.bStartPenetrating || !IsWithinEdgeTolerance(CapsuleLocation, Hit.ImpactPoint, CapsuleShape.Capsule.Radius)) + { + // Use a capsule with a slightly smaller radius and shorter height to avoid the adjacent object. + // Capsule must not be nearly zero or the trace will fall back to a line trace from the start point and have the wrong length. + CapsuleShape.Capsule.Radius = FMath::Max(0.f, CapsuleShape.Capsule.Radius - SWEEP_EDGE_REJECT_DISTANCE - UE_KINDA_SMALL_NUMBER); + if (!CapsuleShape.IsNearlyZero()) + { + ShrinkHeight = (PawnHalfHeight - PawnRadius) * (1.f - ShrinkScaleOverlap); + TraceDist = SweepDistance + ShrinkHeight; + CapsuleShape.Capsule.HalfHeight = FMath::Max(PawnHalfHeight - ShrinkHeight, CapsuleShape.Capsule.Radius); + Hit.Reset(1.f, false); + + bBlockingHit = FloorSweepTest(Hit, CapsuleLocation, CapsuleLocation + (CharacterUpDirection * -TraceDist), CollisionChannel, CapsuleShape, QueryParams, ResponseParam); + } + } + + // Reduce hit distance by ShrinkHeight because we shrank the capsule for the trace. + // We allow negative distances here, because this allows us to pull out of penetrations. + const float MaxPenetrationAdjust = FMath::Max(MAX_FLOOR_DIST, PawnRadius); + const float SweepResult = FMath::Max(-MaxPenetrationAdjust, Hit.Time * TraceDist - ShrinkHeight); + + OutFloorResult.SetFromSweep(Hit, SweepResult, false); + if (Hit.IsValidBlockingHit() && IsWalkable(Hit)) + { + if (SweepResult <= SweepDistance) + { + // Hit within test distance. + OutFloorResult.bWalkableFloor = true; + return; + } + } + } + } + + // Since we require a longer sweep than line trace, we don't want to run the line trace if the sweep missed everything. + // We do however want to try a line trace if the sweep was stuck in penetration. + if (!OutFloorResult.bBlockingHit && !OutFloorResult.HitResult.bStartPenetrating) + { + OutFloorResult.FloorDist = SweepDistance; + return; + } + + // Line trace + if (LineDistance > 0.f) + { + const float ShrinkHeight = PawnHalfHeight; + const FVector LineTraceStart = CapsuleLocation; + const float TraceDist = LineDistance + ShrinkHeight; + const FVector Down = (CharacterUpDirection * -TraceDist); + QueryParams.TraceTag = SCENE_QUERY_STAT_NAME_ONLY(FloorLineTrace); + + FHitResult Hit(1.f); + bBlockingHit = GetWorld()->LineTraceSingleByChannel(Hit, LineTraceStart, LineTraceStart + Down, CollisionChannel, QueryParams, ResponseParam); + + if (bBlockingHit) + { + if (Hit.Time > 0.f) + { + // Reduce hit distance by ShrinkHeight because we started the trace higher than the base. + // We allow negative distances here, because this allows us to pull out of penetrations. + const float MaxPenetrationAdjust = FMath::Max(MAX_FLOOR_DIST, PawnRadius); + const float LineResult = FMath::Max(-MaxPenetrationAdjust, Hit.Time * TraceDist - ShrinkHeight); + + OutFloorResult.bBlockingHit = true; + if (LineResult <= LineDistance && IsWalkable(Hit)) + { + OutFloorResult.SetFromLineTrace(Hit, OutFloorResult.FloorDist, LineResult, true); + return; + } + } + } + } + + // No hits were acceptable. + OutFloorResult.bWalkableFloor = false; +} + + +bool UGSCharacterMovementComponent::IsValidLandingSpot(const FVector& CapsuleLocation, const FHitResult& Hit) const +{ + if (!Hit.bBlockingHit) + { + return false; + } + + // Skip some checks if penetrating. Penetration will be handled by the FindFloor call (using a smaller capsule) + if (!Hit.bStartPenetrating) + { + // Reject unwalkable floor normals. + if (!IsWalkable(Hit)) + { + return false; + } + + float PawnRadius, PawnHalfHeight; + CharacterOwner->GetCapsuleComponent()->GetScaledCapsuleSize(PawnRadius, PawnHalfHeight); + + // TODO: I understand what this is trying to do but need to go over the math again so I can make it work for different character up orientations + /* + // Reject hits that are above our lower hemisphere (can happen when sliding down a vertical surface). + const float LowerHemisphereZ = Hit.Location.Z - PawnHalfHeight + PawnRadius; + if (Hit.ImpactPoint.Z >= LowerHemisphereZ) + { + return false; + } + */ + + // TODO: Another thing I sort of understand what it is trying to do but need to go over the math again so I can make it work for different character up orientations + /* + // Reject hits that are barely on the cusp of the radius of the capsule + if (!IsWithinEdgeTolerance(Hit.Location, Hit.ImpactPoint, PawnRadius)) + { + return false; + } + */ + } + else + { + // Penetrating + float NormalUp = CharacterUpDirection.Dot(Hit.Normal); + if (NormalUp < UE_KINDA_SMALL_NUMBER) + { + // Normal is nearly horizontal or downward, that's a penetration adjustment next to a vertical or overhanging wall. Don't pop to the floor. + return false; + } + } + + FFindFloorResult FloorResult; + FindFloor(CapsuleLocation, FloorResult, false, &Hit); + + if (!FloorResult.IsWalkableFloor()) + { + return false; + } + + return true; +} + + +bool UGSCharacterMovementComponent::IsWalkable(const FHitResult& Hit) const +{ + if (!Hit.IsValidBlockingHit()) + { + // No hit, or starting in penetration + return false; + } + + float ImpactNormalUp = CharacterUpDirection.Dot(Hit.ImpactNormal); + + // Never walk up vertical surfaces. + if (ImpactNormalUp < UE_KINDA_SMALL_NUMBER) + { + return false; + } + + /* + float TestWalkableZ = WalkableFloorZ; + + // See if this component overrides the walkable floor z. + const UPrimitiveComponent* HitComponent = Hit.Component.Get(); + if (HitComponent) + { + const FWalkableSlopeOverride& SlopeOverride = HitComponent->GetWalkableSlopeOverride(); + TestWalkableZ = SlopeOverride.ModifyWalkableFloorZ(TestWalkableZ); + } + + // Can't walk on this surface if it is too steep. + if (Hit.ImpactNormal.Z < TestWalkableZ) + { + return false; + } + */ + + return true; + +} + + void UGSCharacterMovementComponent::SetCharacterUpDirection(FVector NewUpDirection) { NewUpDirection.Normalize(); diff --git a/GravityStomp/Source/GravityStompGame/Character/GSCharacterMovementComponent.h b/GravityStomp/Source/GravityStompGame/Character/GSCharacterMovementComponent.h index dd032d4..da05fce 100644 --- a/GravityStomp/Source/GravityStompGame/Character/GSCharacterMovementComponent.h +++ b/GravityStomp/Source/GravityStompGame/Character/GSCharacterMovementComponent.h @@ -19,11 +19,19 @@ class GRAVITYSTOMPGAME_API UGSCharacterMovementComponent : public UCharacterMove public: UGSCharacterMovementComponent(); - virtual void PhysFalling(float deltaTime, int32 Iterations) override; - UFUNCTION(BlueprintCallable) void SetCharacterUpDirection(FVector NewUpDirection); + // UCharacterMovementComponent interface + virtual void PhysFalling(float deltaTime, int32 Iterations) override; + + virtual void ComputeFloorDist(const FVector& CapsuleLocation, float LineDistance, float SweepDistance, FFindFloorResult& OutFloorResult, float SweepRadius, const FHitResult* DownwardSweepResult) const override; + + virtual bool IsValidLandingSpot(const FVector& CapsuleLocation, const FHitResult& Hit) const override; + + virtual bool IsWalkable(const FHitResult& Hit) const override; + // End UCharacterMovementComponent interface + private: FVector CharacterUpDirection = FVector::UpVector; };