Rotate character and update floor detection functions so you can properly land on ceilings

This commit is contained in:
Kevin Poretti 2023-01-04 20:50:26 -05:00
parent 01f88914cd
commit 94d04f2250
4 changed files with 262 additions and 5 deletions

Binary file not shown.

BIN
GravityStomp/Content/Maps/Debug/Test.umap (Stored with Git LFS)

Binary file not shown.

View File

@ -3,6 +3,7 @@
#include "Character/GSCharacterMovementComponent.h" #include "Character/GSCharacterMovementComponent.h"
// UE includes // UE includes
#include "Components/CapsuleComponent.h"
#include "GameFramework/Character.h" #include "GameFramework/Character.h"
// These are defined in CharacterMovementComponent.cpp and inaccessible here. Just copy and paste too make the PhysFalling work // 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); ECVF_Default);
} }
UGSCharacterMovementComponent::UGSCharacterMovementComponent() UGSCharacterMovementComponent::UGSCharacterMovementComponent()
{ {
} }
void UGSCharacterMovementComponent::PhysFalling(float deltaTime, int32 Iterations) void UGSCharacterMovementComponent::PhysFalling(float deltaTime, int32 Iterations)
{ {
if (deltaTime < MIN_TICK_TIME) 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) void UGSCharacterMovementComponent::SetCharacterUpDirection(FVector NewUpDirection)
{ {
NewUpDirection.Normalize(); NewUpDirection.Normalize();

View File

@ -19,11 +19,19 @@ class GRAVITYSTOMPGAME_API UGSCharacterMovementComponent : public UCharacterMove
public: public:
UGSCharacterMovementComponent(); UGSCharacterMovementComponent();
virtual void PhysFalling(float deltaTime, int32 Iterations) override;
UFUNCTION(BlueprintCallable) UFUNCTION(BlueprintCallable)
void SetCharacterUpDirection(FVector NewUpDirection); 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: private:
FVector CharacterUpDirection = FVector::UpVector; FVector CharacterUpDirection = FVector::UpVector;
}; };