This problem has plagued me. This is the only post on all of the internet that directly asked how to do this in unreal lol.
After many months, I have made my own solution to this problem. I imagine the OP has moved on, but to anyone else that need this, Here you go.
I originally did this in blueprints but moved it to C++ for efficiency. It essentially overrides the AddMovementInput function and does traces to check for walkable surface, taking into account MaxStepHeight and whether you are jumping. It sets a LastGoodLocation along the way. When it doesnt see a walkable surface, it searches left and right then left and right further around etc, until it finds one. If not, it takes you back to LastGoodLocation. If it does, it will adjust the movement vector, scaled to SearchAngle, to a walkable direction.
This seems to work in multiplayer with average lag emulation but time will tell.
I added options that can be set in Character Bluprint.
KeepPlayerOnNavMesh was a neat one that I just added today. It just checks if walkable surface is on nav mesh. Works pretty good so far. In Edit->Project Settings->Navigation System, you’ll need to enable the option for Allow Client Side Navigation for Multiplayer
That said, this thing has so many holes in it. It traces on visibility so what happens if something is in the way like an enemy? I haven’t even attempted it with AI. And so many more lol.
StoryCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "StoryCharacter.generated.h"
UCLASS()
class HEROIC_API AStoryCharacter : public ACharacter
{
GENERATED_BODY()
public:
virtual void Tick(float DeltaTime) override;
virtual void GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const override;
protected:
virtual void Jump() override;
virtual void AddMovementInput(FVector WorldDirection, float ScaleValue, bool bForce /*=false*/) override;
FHitResult TraceForLedge(FVector TraceVector) const;
FVector GetNewLedgeMove(FVector Delta, float& OutScalar) const;
bool IsStepHeightWalkable(FVector Location, bool CheckTouchingGround = false) const;
UPROPERTY(Replicated, VisibleAnywhere, BlueprintReadOnly, Category="Input")
bool bIsJumping;
float JumpHangTime = 0.f;
float EdgeDetectTime = 0.f;
UPROPERTY(EditDefaultsOnly, Category="Movement|Edge Detection")
bool StopWalkOffEdge;
UPROPERTY(EditDefaultsOnly, Category="Movement|Edge Detection")
bool KeepPlayerOnNavMesh;
UPROPERTY(EditDefaultsOnly, Category="Movement|Edge Detection", meta=(ClampMin="0", UIMin="0", ForceUnits=cm))
float SearchAngleSteps = 15.f;
UPROPERTY(EditDefaultsOnly, Category="Movement|Edge Detection", meta=(ClampMin="0", UIMin="0", ForceUnits=cm))
float SearchAngleEndAddition = 45.f;
UPROPERTY(EditDefaultsOnly, Category="Movement|Edge Detection", meta=(ClampMin="0", UIMin="0", ForceUnits=cm))
float SearchDistance = 15.f;
UPROPERTY(EditDefaultsOnly, Category="Movement|Edge Detection")
bool DebugLines = false;
UPROPERTY(Replicated)
FVector LastGoodLocation;
};
StoryCharacter.cpp
#include "Character/StoryCharacter.h"
#include "NavigationSystem.h"
#include "AI/NavigationSystemBase.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/PawnMovementComponent.h"
#include "Kismet/KismetMathLibrary.h"
#include "Net/UnrealNetwork.h"
void AStoryCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AStoryCharacter, LastGoodLocation);
}
// Called every frame
void AStoryCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if(StopWalkOffEdge)
{
FHitResult HitResult;
FVector TraceStart = GetActorLocation();
FVector TraceEnd = TraceStart + FVector(0, 0, -200);
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(GetOwner());
GetWorld()->LineTraceSingleByChannel(HitResult, TraceStart, TraceEnd, ECC_Visibility, QueryParams);
if(!IsStepHeightWalkable(HitResult.Location) && !LastGoodLocation.IsZero())
{
if(!bIsJumping)
{
if(EdgeDetectTime > .00001f)
{
//GEngine->AddOnScreenDebugMessage(-1, 5, FColor::Blue, FString::Printf(TEXT("Teleported!")));
SetActorLocation(LastGoodLocation);
EdgeDetectTime = 0.f;
}
else { EdgeDetectTime += DeltaTime; }
}
else
{
if(JumpHangTime > 2.f)
{
SetActorLocation(LastGoodLocation);
JumpHangTime = 0;
bIsJumping = false;
}
else
{
JumpHangTime += DeltaTime;
//GEngine->AddOnScreenDebugMessage(2, 5, FColor::Green, FString::Printf(TEXT("Jumping: %f"), JumpHangTime));
}
}
}
else
{
if(IsStepHeightWalkable(HitResult.Location, true))
{
LastGoodLocation = GetActorLocation();
bIsJumping = false;
JumpHangTime = 0;
if(DebugLines) DrawDebugSphere(GetWorld(), LastGoodLocation, 15,12,FColor::Yellow);
}
}
}
}
void AStoryCharacter::Jump()
{
Super::Jump();
bIsJumping = true;
}
void AStoryCharacter::AddMovementInput(FVector WorldDirection, float ScaleValue, bool bForce /*=false*/)
{
FVector NewWorldDirection = WorldDirection;
if(StopWalkOffEdge)
{
if(DebugLines) DrawDebugLine(GetWorld(), GetActorLocation(), GetActorLocation() + (WorldDirection * 100.f),FColor::Yellow);
FHitResult WorldDirectionHitResult = TraceForLedge(WorldDirection);
if(!IsStepHeightWalkable(WorldDirectionHitResult.Location) && !bIsJumping)
{
NewWorldDirection = GetNewLedgeMove(WorldDirection, ScaleValue);
NewWorldDirection += (LastGoodLocation - GetActorLocation()).GetSafeNormal()*WorldDirection.Length();
}
}
if(NewWorldDirection.IsZero()){ return; }
UPawnMovementComponent* MovementComponent = GetMovementComponent();
if (MovementComponent)
{
MovementComponent->AddInputVector(NewWorldDirection * ScaleValue, bForce);
}
else
{
Internal_AddMovementInput(WorldDirection * ScaleValue, bForce);
}
}
FHitResult AStoryCharacter::TraceForLedge(FVector TraceVector) const
{
TraceVector = TraceVector.GetSafeNormal();
FHitResult HitResult;
FVector TraceStart = GetActorLocation() + TraceVector * FVector(SearchDistance, SearchDistance, 0);
FVector TraceEnd = TraceStart + FVector(0, 0, -200);
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(GetOwner());
GetWorld()->LineTraceSingleByChannel(HitResult, TraceStart, TraceEnd, ECC_Visibility, QueryParams);
if(DebugLines) DrawDebugLine(GetWorld(), TraceStart, TraceEnd, HitResult.bBlockingHit ? FColor::Blue : FColor::Green, false, -1, 0, 1.f);
return HitResult;
}
FVector AStoryCharacter::GetNewLedgeMove(FVector Delta, float& OutScalar) const
{
if (Delta.IsZero())
{
return FVector::ZeroVector;
}
float SearchAngle = 1.f;
FVector SearchVector = Delta.GetSafeNormal();
while(SearchAngle < 360.f && SearchAngle > -360.f)
{
FHitResult SearchHitResult = TraceForLedge(SearchVector);
if(IsStepHeightWalkable(SearchHitResult.Location))
{
SearchAngle += SearchAngle >= 0 ? SearchAngleEndAddition : -SearchAngleEndAddition;
FVector NewDelta = Delta.RotateAngleAxis(SearchAngle, FVector(0,0,1));
float DeltaScaler = UKismetMathLibrary::MapRangeClamped(FMath::Abs(FMath::Abs(SearchAngle)-90), 0.f, 45.f, 0.f, 1.f);
OutScalar *= DeltaScaler;
if(DebugLines) DrawDebugDirectionalArrow(GetWorld(), GetActorLocation(), GetActorLocation() + (NewDelta * 100.f), 1.f, FColor::Red, false, -1);
return NewDelta;
}
SearchAngle = SearchAngle > 0 ? (SearchAngle + SearchAngleSteps) * -1.f : (SearchAngle - SearchAngleSteps) * -1.f;
SearchVector = Delta.RotateAngleAxis(SearchAngle, FVector(0,0,1));
}
return FVector::ZeroVector;
}
bool AStoryCharacter::IsStepHeightWalkable(FVector Location, bool CheckTouchingGround) const
{
if(Location.IsZero()){ return false; }
float CapsuleHalfHeight = GetCapsuleComponent()->GetScaledCapsuleHalfHeight();
float CharacterFeetZ = GetActorLocation().Z - CapsuleHalfHeight;
if(CheckTouchingGround)
{
//GEngine->AddOnScreenDebugMessage(1, 5, FColor::Blue, FString::Printf(TEXT("Jumping: %f"), FMath::Abs(Location.Z - CharacterFeetZ)));
if(GetCharacterMovement()->Velocity.Z > 0) { return false; }
if(FMath::Abs(Location.Z - CharacterFeetZ) > 15.f){ return false; }
}
if(FMath::Abs(Location.Z - CharacterFeetZ) > GetCharacterMovement()->MaxStepHeight){ return false; }
if(KeepPlayerOnNavMesh)
{
UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
FNavLocation LocationOnNavMesh;
bool IsOnNavMesh = NavSys->ProjectPointToNavigation(Location, LocationOnNavMesh, FVector(1.f, 1.f, GetCharacterMovement()->MaxStepHeight));
if(IsOnNavMesh){
//DrawDebugSphere(GetWorld(), LocationOnNavMesh, 5.f, 6, FColor::Cyan, false, 1);
}
else { return false; }
}
return true;
}