From 01f88914cd6e733eecb573a3991d88f57abb8581 Mon Sep 17 00:00:00 2001 From: Kevin Poretti Date: Wed, 4 Jan 2023 17:03:58 -0500 Subject: [PATCH] Semi-functional gravity direction changing --- .../Content/Characters/BP_GSCharacter.uasset | 4 +- .../Input/Actions/IA_ChangeGravity.uasset | 3 + GravityStomp/Content/Input/IMC_Default.uasset | 4 +- GravityStomp/GravityStomp.uproject | 5 +- .../Character/GSCharacter.cpp | 15 +- .../GravityStompGame/Character/GSCharacter.h | 10 +- .../GSCharacterMovementComponent.cpp | 355 ++++++++++++++++++ .../Character/GSCharacterMovementComponent.h | 29 ++ 8 files changed, 416 insertions(+), 9 deletions(-) create mode 100644 GravityStomp/Content/Input/Actions/IA_ChangeGravity.uasset create mode 100644 GravityStomp/Source/GravityStompGame/Character/GSCharacterMovementComponent.cpp create mode 100644 GravityStomp/Source/GravityStompGame/Character/GSCharacterMovementComponent.h diff --git a/GravityStomp/Content/Characters/BP_GSCharacter.uasset b/GravityStomp/Content/Characters/BP_GSCharacter.uasset index d8a5c90..540a236 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:1d3a6e67f2471cbe40d48d70ed4503d295a06dbe392934d846086b1738822947 -size 33811 +oid sha256:d212347f7ac01ec91a86fa0401390855e61c88857652a69ad4e03695c01f74a1 +size 33962 diff --git a/GravityStomp/Content/Input/Actions/IA_ChangeGravity.uasset b/GravityStomp/Content/Input/Actions/IA_ChangeGravity.uasset new file mode 100644 index 0000000..97ed79f --- /dev/null +++ b/GravityStomp/Content/Input/Actions/IA_ChangeGravity.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6400f66303668949257450011ca8b30c100f8f00521da629b87858e798b1067b +size 1518 diff --git a/GravityStomp/Content/Input/IMC_Default.uasset b/GravityStomp/Content/Input/IMC_Default.uasset index 6adf69b..70c1c50 100644 --- a/GravityStomp/Content/Input/IMC_Default.uasset +++ b/GravityStomp/Content/Input/IMC_Default.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:752bf0509c42fa9b42f3482eee83cc08fe27fa5a75f775925b11a0e1879cb775 -size 9160 +oid sha256:8d1f40f1b3d99628461340618f1e2bb66ffe650d0904979f683b293bae5f2015 +size 9146 diff --git a/GravityStomp/GravityStomp.uproject b/GravityStomp/GravityStomp.uproject index 7340146..885b78c 100644 --- a/GravityStomp/GravityStomp.uproject +++ b/GravityStomp/GravityStomp.uproject @@ -7,7 +7,10 @@ { "Name": "GravityStompGame", "Type": "Runtime", - "LoadingPhase": "Default" + "LoadingPhase": "Default", + "AdditionalDependencies": [ + "Engine" + ] }, { "Name": "GravityStompEditor", diff --git a/GravityStomp/Source/GravityStompGame/Character/GSCharacter.cpp b/GravityStomp/Source/GravityStompGame/Character/GSCharacter.cpp index 153f794..910cae0 100644 --- a/GravityStomp/Source/GravityStompGame/Character/GSCharacter.cpp +++ b/GravityStomp/Source/GravityStompGame/Character/GSCharacter.cpp @@ -9,13 +9,15 @@ #include "GameFramework/SpringArmComponent.h" #include "EnhancedInputComponent.h" #include "EnhancedInputSubsystems.h" +#include "GSCharacterMovementComponent.h" #include "Kismet/KismetMathLibrary.h" ////////////////////////////////////////////////////////////////////////// // AGravityStompCharacter -AGSCharacter::AGSCharacter() +AGSCharacter::AGSCharacter(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer.SetDefaultSubobjectClass(ACharacter::CharacterMovementComponentName)) { // Set size for collision capsule GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f); @@ -80,6 +82,7 @@ void AGSCharacter::SetupPlayerInputComponent(class UInputComponent* PlayerInputC { //Moving EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AGSCharacter::Move); + EnhancedInputComponent->BindAction(ChangeGravityAction, ETriggerEvent::Triggered, this, &AGSCharacter::ChangeGravityDirection); } } @@ -93,4 +96,12 @@ void AGSCharacter::Move(const FInputActionValue& Value) // add movement AddMovementInput(FVector::UpVector, MovementVector.Y); AddMovementInput(FVector::RightVector, MovementVector.X); -} \ No newline at end of file +} + +void AGSCharacter::ChangeGravityDirection(const FInputActionValue& Value) +{ + FVector2D GravityDirection = Value.Get(); + FVector NewCharacterUpDirection(0.0f, -GravityDirection.X, -GravityDirection.Y); + + GetCharacterMovement()->SetCharacterUpDirection(NewCharacterUpDirection); +} diff --git a/GravityStomp/Source/GravityStompGame/Character/GSCharacter.h b/GravityStomp/Source/GravityStompGame/Character/GSCharacter.h index de355a9..9fe92cc 100644 --- a/GravityStomp/Source/GravityStompGame/Character/GSCharacter.h +++ b/GravityStomp/Source/GravityStompGame/Character/GSCharacter.h @@ -27,16 +27,22 @@ class AGSCharacter : public ACharacter /** Move Input Action */ UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true")) - class UInputAction* MoveAction;; + class UInputAction* MoveAction; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true")) + class UInputAction* ChangeGravityAction; public: - AGSCharacter(); + AGSCharacter(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); protected: /** Called for movement input */ void Move(const FInputActionValue& Value); + /** Called when the player changes the gravity direction */ + void ChangeGravityDirection(const FInputActionValue& Value); + protected: // APawn interface virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override; diff --git a/GravityStomp/Source/GravityStompGame/Character/GSCharacterMovementComponent.cpp b/GravityStomp/Source/GravityStompGame/Character/GSCharacterMovementComponent.cpp new file mode 100644 index 0000000..4747bbe --- /dev/null +++ b/GravityStomp/Source/GravityStompGame/Character/GSCharacterMovementComponent.cpp @@ -0,0 +1,355 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#include "Character/GSCharacterMovementComponent.h" + +// UE includes +#include "GameFramework/Character.h" + +// These are defined in CharacterMovementComponent.cpp and inaccessible here. Just copy and paste too make the PhysFalling work +namespace CharacterMovementConstants +{ + // MAGIC NUMBERS + const float VERTICAL_SLOPE_NORMAL_Z = 0.001f; // Slope is vertical if Abs(Normal.Z) <= this threshold. Accounts for precision problems that sometimes angle normals slightly off horizontal for vertical surface. +} + +namespace CharacterMovementCVars +{ + int32 ForceJumpPeakSubstep = 1; + FAutoConsoleVariableRef CVarForceJumpPeakSubstep( + TEXT("gs.ForceJumpPeakSubstep"), + ForceJumpPeakSubstep, + TEXT("If 1, force a jump substep to always reach the peak position of a jump, which can often be cut off as framerate lowers."), + ECVF_Default); +} + +UGSCharacterMovementComponent::UGSCharacterMovementComponent() +{ +} + +void UGSCharacterMovementComponent::PhysFalling(float deltaTime, int32 Iterations) +{ + if (deltaTime < MIN_TICK_TIME) + { + return; + } + + FVector FallAcceleration = GetFallingLateralAcceleration(deltaTime); + FallAcceleration.Z = 0.f; + const bool bHasLimitedAirControl = ShouldLimitAirControl(deltaTime, FallAcceleration); + + float remainingTime = deltaTime; + while( (remainingTime >= MIN_TICK_TIME) && (Iterations < MaxSimulationIterations) ) + { + Iterations++; + float timeTick = GetSimulationTimeStep(remainingTime, Iterations); + remainingTime -= timeTick; + + const FVector OldLocation = UpdatedComponent->GetComponentLocation(); + const FQuat PawnRotation = UpdatedComponent->GetComponentQuat(); + bJustTeleported = false; + + const FVector OldVelocityWithRootMotion = Velocity; + + RestorePreAdditiveRootMotionVelocity(); + + const FVector OldVelocity = Velocity; + + // Apply input + const float MaxDecel = GetMaxBrakingDeceleration(); + if (!HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity()) + { + // Compute Velocity + { + // Acceleration = FallAcceleration for CalcVelocity(), but we restore it after using it. + TGuardValue RestoreAcceleration(Acceleration, FallAcceleration); + Velocity.Z = 0.f; + CalcVelocity(timeTick, FallingLateralFriction, false, MaxDecel); + Velocity.Z = OldVelocity.Z; + } + } + + // Compute current gravity + const FVector Gravity = CharacterUpDirection * GetGravityZ(); + float GravityTime = timeTick; + + // If jump is providing force, gravity may be affected. + bool bEndingJumpForce = false; + if (CharacterOwner->JumpForceTimeRemaining > 0.0f) + { + // Consume some of the force time. Only the remaining time (if any) is affected by gravity when bApplyGravityWhileJumping=false. + const float JumpForceTime = FMath::Min(CharacterOwner->JumpForceTimeRemaining, timeTick); + GravityTime = bApplyGravityWhileJumping ? timeTick : FMath::Max(0.0f, timeTick - JumpForceTime); + + // Update Character state + CharacterOwner->JumpForceTimeRemaining -= JumpForceTime; + if (CharacterOwner->JumpForceTimeRemaining <= 0.0f) + { + CharacterOwner->ResetJumpState(); + bEndingJumpForce = true; + } + } + + // Apply gravity + Velocity = NewFallVelocity(Velocity, Gravity, GravityTime); + + //UE_LOG(LogCharacterMovement, Log, TEXT("dt=(%.6f) OldLocation=(%s) OldVelocity=(%s) OldVelocityWithRootMotion=(%s) NewVelocity=(%s)"), timeTick, *(UpdatedComponent->GetComponentLocation()).ToString(), *OldVelocity.ToString(), *OldVelocityWithRootMotion.ToString(), *Velocity.ToString()); + ApplyRootMotionToVelocity(timeTick); + DecayFormerBaseVelocity(timeTick); + + // See if we need to sub-step to exactly reach the apex. This is important for avoiding "cutting off the top" of the trajectory as framerate varies. + if (CharacterMovementCVars::ForceJumpPeakSubstep && OldVelocityWithRootMotion.Z > 0.f && Velocity.Z <= 0.f && NumJumpApexAttempts < MaxJumpApexAttemptsPerSimulation) + { + const FVector DerivedAccel = (Velocity - OldVelocityWithRootMotion) / timeTick; + if (!FMath::IsNearlyZero(DerivedAccel.Z)) + { + const float TimeToApex = -OldVelocityWithRootMotion.Z / DerivedAccel.Z; + + // The time-to-apex calculation should be precise, and we want to avoid adding a substep when we are basically already at the apex from the previous iteration's work. + const float ApexTimeMinimum = 0.0001f; + if (TimeToApex >= ApexTimeMinimum && TimeToApex < timeTick) + { + const FVector ApexVelocity = OldVelocityWithRootMotion + (DerivedAccel * TimeToApex); + Velocity = ApexVelocity; + Velocity.Z = 0.f; // Should be nearly zero anyway, but this makes apex notifications consistent. + + // We only want to move the amount of time it takes to reach the apex, and refund the unused time for next iteration. + const float TimeToRefund = (timeTick - TimeToApex); + + remainingTime += TimeToRefund; + timeTick = TimeToApex; + Iterations--; + NumJumpApexAttempts++; + + // Refund time to any active Root Motion Sources as well + for (TSharedPtr RootMotionSource : CurrentRootMotion.RootMotionSources) + { + const float RewoundRMSTime = FMath::Max(0.0f, RootMotionSource->GetTime() - TimeToRefund); + RootMotionSource->SetTime(RewoundRMSTime); + } + } + } + } + + if (bNotifyApex && (Velocity.Z < 0.f)) + { + // Just passed jump apex since now going down + bNotifyApex = false; + NotifyJumpApex(); + } + + // Compute change in position (using midpoint integration method). + FVector Adjusted = 0.5f * (OldVelocityWithRootMotion + Velocity) * timeTick; + + // Special handling if ending the jump force where we didn't apply gravity during the jump. + if (bEndingJumpForce && !bApplyGravityWhileJumping) + { + // We had a portion of the time at constant speed then a portion with acceleration due to gravity. + // Account for that here with a more correct change in position. + const float NonGravityTime = FMath::Max(0.f, timeTick - GravityTime); + Adjusted = (OldVelocityWithRootMotion * NonGravityTime) + (0.5f*(OldVelocityWithRootMotion + Velocity) * GravityTime); + } + + // Move + FHitResult Hit(1.f); + SafeMoveUpdatedComponent( Adjusted, PawnRotation, true, Hit); + + if (!HasValidData()) + { + return; + } + + float LastMoveTimeSlice = timeTick; + float subTimeTickRemaining = timeTick * (1.f - Hit.Time); + + if ( IsSwimming() ) //just entered water + { + remainingTime += subTimeTickRemaining; + StartSwimming(OldLocation, OldVelocity, timeTick, remainingTime, Iterations); + return; + } + else if ( Hit.bBlockingHit ) + { + if (IsValidLandingSpot(UpdatedComponent->GetComponentLocation(), Hit)) + { + remainingTime += subTimeTickRemaining; + ProcessLanded(Hit, remainingTime, Iterations); + return; + } + else + { + // Compute impact deflection based on final velocity, not integration step. + // This allows us to compute a new velocity from the deflected vector, and ensures the full gravity effect is included in the slide result. + Adjusted = Velocity * timeTick; + + // See if we can convert a normally invalid landing spot (based on the hit result) to a usable one. + if (!Hit.bStartPenetrating && ShouldCheckForValidLandingSpot(timeTick, Adjusted, Hit)) + { + const FVector PawnLocation = UpdatedComponent->GetComponentLocation(); + FFindFloorResult FloorResult; + FindFloor(PawnLocation, FloorResult, false); + if (FloorResult.IsWalkableFloor() && IsValidLandingSpot(PawnLocation, FloorResult.HitResult)) + { + remainingTime += subTimeTickRemaining; + ProcessLanded(FloorResult.HitResult, remainingTime, Iterations); + return; + } + } + + HandleImpact(Hit, LastMoveTimeSlice, Adjusted); + + // If we've changed physics mode, abort. + if (!HasValidData() || !IsFalling()) + { + return; + } + + // Limit air control based on what we hit. + // We moved to the impact point using air control, but may want to deflect from there based on a limited air control acceleration. + FVector VelocityNoAirControl = OldVelocity; + FVector AirControlAccel = Acceleration; + if (bHasLimitedAirControl) + { + // Compute VelocityNoAirControl + { + // Find velocity *without* acceleration. + TGuardValue RestoreAcceleration(Acceleration, FVector::ZeroVector); + TGuardValue RestoreVelocity(Velocity, OldVelocity); + Velocity.Z = 0.f; + CalcVelocity(timeTick, FallingLateralFriction, false, MaxDecel); + VelocityNoAirControl = FVector(Velocity.X, Velocity.Y, OldVelocity.Z); + VelocityNoAirControl = NewFallVelocity(VelocityNoAirControl, Gravity, GravityTime); + } + + const bool bCheckLandingSpot = false; // we already checked above. + AirControlAccel = (Velocity - VelocityNoAirControl) / timeTick; + const FVector AirControlDeltaV = LimitAirControl(LastMoveTimeSlice, AirControlAccel, Hit, bCheckLandingSpot) * LastMoveTimeSlice; + Adjusted = (VelocityNoAirControl + AirControlDeltaV) * LastMoveTimeSlice; + } + + const FVector OldHitNormal = Hit.Normal; + const FVector OldHitImpactNormal = Hit.ImpactNormal; + FVector Delta = ComputeSlideVector(Adjusted, 1.f - Hit.Time, OldHitNormal, Hit); + + // Compute velocity after deflection (only gravity component for RootMotion) + const UPrimitiveComponent* HitComponent = Hit.GetComponent(); + if (/*CharacterMovementCVars::UseTargetVelocityOnImpact &&*/ !Velocity.IsNearlyZero() && MovementBaseUtility::IsSimulatedBase(HitComponent)) + { + const FVector ContactVelocity = MovementBaseUtility::GetMovementBaseVelocity(HitComponent, NAME_None) + MovementBaseUtility::GetMovementBaseTangentialVelocity(HitComponent, NAME_None, Hit.ImpactPoint); + const FVector NewVelocity = Velocity - Hit.ImpactNormal * FVector::DotProduct(Velocity - ContactVelocity, Hit.ImpactNormal); + Velocity = HasAnimRootMotion() || CurrentRootMotion.HasOverrideVelocityWithIgnoreZAccumulate() ? FVector(Velocity.X, Velocity.Y, NewVelocity.Z) : NewVelocity; + } + else if (subTimeTickRemaining > UE_KINDA_SMALL_NUMBER && !bJustTeleported) + { + const FVector NewVelocity = (Delta / subTimeTickRemaining); + Velocity = HasAnimRootMotion() || CurrentRootMotion.HasOverrideVelocityWithIgnoreZAccumulate() ? FVector(Velocity.X, Velocity.Y, NewVelocity.Z) : NewVelocity; + } + + if (subTimeTickRemaining > UE_KINDA_SMALL_NUMBER && (Delta | Adjusted) > 0.f) + { + // Move in deflected direction. + SafeMoveUpdatedComponent( Delta, PawnRotation, true, Hit); + + if (Hit.bBlockingHit) + { + // hit second wall + LastMoveTimeSlice = subTimeTickRemaining; + subTimeTickRemaining = subTimeTickRemaining * (1.f - Hit.Time); + + if (IsValidLandingSpot(UpdatedComponent->GetComponentLocation(), Hit)) + { + remainingTime += subTimeTickRemaining; + ProcessLanded(Hit, remainingTime, Iterations); + return; + } + + HandleImpact(Hit, LastMoveTimeSlice, Delta); + + // If we've changed physics mode, abort. + if (!HasValidData() || !IsFalling()) + { + return; + } + + // Act as if there was no air control on the last move when computing new deflection. + if (bHasLimitedAirControl && Hit.Normal.Z > CharacterMovementConstants::VERTICAL_SLOPE_NORMAL_Z) + { + const FVector LastMoveNoAirControl = VelocityNoAirControl * LastMoveTimeSlice; + Delta = ComputeSlideVector(LastMoveNoAirControl, 1.f, OldHitNormal, Hit); + } + + FVector PreTwoWallDelta = Delta; + TwoWallAdjust(Delta, Hit, OldHitNormal); + + // Limit air control, but allow a slide along the second wall. + if (bHasLimitedAirControl) + { + const bool bCheckLandingSpot = false; // we already checked above. + const FVector AirControlDeltaV = LimitAirControl(subTimeTickRemaining, AirControlAccel, Hit, bCheckLandingSpot) * subTimeTickRemaining; + + // Only allow if not back in to first wall + if (FVector::DotProduct(AirControlDeltaV, OldHitNormal) > 0.f) + { + Delta += (AirControlDeltaV * subTimeTickRemaining); + } + } + + // Compute velocity after deflection (only gravity component for RootMotion) + if (subTimeTickRemaining > UE_KINDA_SMALL_NUMBER && !bJustTeleported) + { + const FVector NewVelocity = (Delta / subTimeTickRemaining); + Velocity = HasAnimRootMotion() || CurrentRootMotion.HasOverrideVelocityWithIgnoreZAccumulate() ? FVector(Velocity.X, Velocity.Y, NewVelocity.Z) : NewVelocity; + } + + // bDitch=true means that pawn is straddling two slopes, neither of which it can stand on + bool bDitch = ( (OldHitImpactNormal.Z > 0.f) && (Hit.ImpactNormal.Z > 0.f) && (FMath::Abs(Delta.Z) <= UE_KINDA_SMALL_NUMBER) && ((Hit.ImpactNormal | OldHitImpactNormal) < 0.f) ); + SafeMoveUpdatedComponent( Delta, PawnRotation, true, Hit); + if ( Hit.Time == 0.f ) + { + // if we are stuck then try to side step + FVector SideDelta = (OldHitNormal + Hit.ImpactNormal).GetSafeNormal2D(); + if ( SideDelta.IsNearlyZero() ) + { + SideDelta = FVector(OldHitNormal.Y, -OldHitNormal.X, 0).GetSafeNormal(); + } + SafeMoveUpdatedComponent( SideDelta, PawnRotation, true, Hit); + } + + if ( bDitch || IsValidLandingSpot(UpdatedComponent->GetComponentLocation(), Hit) || Hit.Time == 0.f ) + { + remainingTime = 0.f; + ProcessLanded(Hit, remainingTime, Iterations); + return; + } + else if (GetPerchRadiusThreshold() > 0.f && Hit.Time == 1.f && OldHitImpactNormal.Z >= GetWalkableFloorZ()) + { + // We might be in a virtual 'ditch' within our perch radius. This is rare. + const FVector PawnLocation = UpdatedComponent->GetComponentLocation(); + const float ZMovedDist = FMath::Abs(PawnLocation.Z - OldLocation.Z); + const float MovedDist2DSq = (PawnLocation - OldLocation).SizeSquared2D(); + if (ZMovedDist <= 0.2f * timeTick && MovedDist2DSq <= 4.f * timeTick) + { + Velocity.X += 0.25f * GetMaxSpeed() * (RandomStream.FRand() - 0.5f); + Velocity.Y += 0.25f * GetMaxSpeed() * (RandomStream.FRand() - 0.5f); + Velocity.Z = FMath::Max(JumpZVelocity * 0.25f, 1.f); + Delta = Velocity * timeTick; + SafeMoveUpdatedComponent(Delta, PawnRotation, true, Hit); + } + } + } + } + } + } + + if (Velocity.SizeSquared2D() <= UE_KINDA_SMALL_NUMBER * 10.f) + { + Velocity.X = 0.f; + Velocity.Y = 0.f; + } + } +} + +void UGSCharacterMovementComponent::SetCharacterUpDirection(FVector NewUpDirection) +{ + NewUpDirection.Normalize(); + CharacterUpDirection = NewUpDirection; +} diff --git a/GravityStomp/Source/GravityStompGame/Character/GSCharacterMovementComponent.h b/GravityStomp/Source/GravityStompGame/Character/GSCharacterMovementComponent.h new file mode 100644 index 0000000..dd032d4 --- /dev/null +++ b/GravityStomp/Source/GravityStompGame/Character/GSCharacterMovementComponent.h @@ -0,0 +1,29 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +// UE includes +#include "CoreMinimal.h" +#include "GameFramework/CharacterMovementComponent.h" + +#include "GSCharacterMovementComponent.generated.h" + +/** + * Subclass of CharacterMovementComponent which allows for a custom gravity direction to be applied to the character while falling + */ +UCLASS() +class GRAVITYSTOMPGAME_API UGSCharacterMovementComponent : public UCharacterMovementComponent +{ + GENERATED_BODY() + +public: + UGSCharacterMovementComponent(); + + virtual void PhysFalling(float deltaTime, int32 Iterations) override; + + UFUNCTION(BlueprintCallable) + void SetCharacterUpDirection(FVector NewUpDirection); + +private: + FVector CharacterUpDirection = FVector::UpVector; +};