I am currently developing a edge grab mechanism through C++, and I have hit a dead end.
Right now, the system largely works. I can turn corners and round surfaces and the edge detection is not bad either (the reason why it looks too low in the video is because my animation sucks, but I made that in a few minutes).
The problems start when I turn inside corners that are rather sharp (in the range of 90 degrees or less). Then the character suddenly gets stuck in the wall, travels through the wall to attach himself to some nonsensical place, and so on. I have attached a video for demonstration (the funky corner stuff is towards the end, though the screen recorder slowed the footage down a fair bit making me unable to reproduce the weird “travelling through walls” bug). It should be sufficient in pointing out what my problem is.
My code is as follows:
The trace code
FEdgegrabData UBK_PlayerState_Edgegrabbing::DetectLedge(float Value)
{
//The final hitresults to store our information in
FHitResult TraceNormal;
FHitResult TraceLocation;
const TArray<AActor*> ActorsToIgnore; //main actors to ignore; same for all traces
bool bCanMove = true; //we assume that there is a ledge, until a trace proves otherwise
float ValueAbs = Value / abs(Value); //make sure trace is either -1 or 1, so we can use the direction vectors properly for tracing
FVector SocketLocation; //Set our active socket for tracing
if (ValueAbs < 0.f)
{
SocketLocation = Owner->GetMesh()->GetSocketLocation("Socket_EdgeGrab_Right");
}
else
{
SocketLocation = Owner->GetMesh()->GetSocketLocation("Socket_EdgeGrab_Left");
}
//////////////////////////////////////////////////////////////////////////////////////////////
//
// First, let's trace the sides
//
//////////////////////////////////////////////////////////////////////////////////////////////
//attempts to get the normal of the grabbed wall right in front of the player.
FHitResult PhaseAHitResultA;
FHitResult ConfirmationHit;
FVector PhaseATraceStart = SocketLocation + (Owner->GetActorForwardVector() * -32.f) + (Owner->GetActorRightVector() * DetectionOffset * ValueAbs);
PhaseATraceStart.Z = EdgeData.TraceLocation.ImpactPoint.Z - DetectionOffset;
FVector PhaseATraceEnd = PhaseATraceStart + (Owner->GetActorForwardVector() * TraceOffset.Y);
bool bPhaseAResultA = Owner->LineTraceSingle(this, PhaseATraceStart, PhaseATraceEnd, ECC_Visibility, false, ActorsToIgnore, EDrawDebugTraceInternal::ForDuration, PhaseAHitResultA, true);
TraceNormal = PhaseAHitResultA;
//If we get a hit, perform the second trace to check whether the ledge is valid
if (bPhaseAResultA)
{
PhaseATraceEnd = PhaseAHitResultA.ImpactPoint + (PhaseAHitResultA.ImpactNormal * -DetectionOffset); //the minimum grabbing space that should be there.
PhaseATraceEnd.Z += TraceOffset.Z;
PhaseATraceStart = PhaseATraceEnd + (Owner->GetActorForwardVector() * -TraceOffset.Y);
bool bPhaseAResultB = Owner->LineTraceSingle(this, PhaseATraceStart, PhaseATraceEnd, ECC_Visibility, false, ActorsToIgnore, EDrawDebugTraceInternal::ForDuration, ConfirmationHit, true);
if (!bPhaseAResultB)
{
//get the up-normal of the ledge, so we can use it later to alter the Zposition of the player
PhaseATraceEnd = PhaseAHitResultA.ImpactPoint + (PhaseAHitResultA.ImpactNormal * -DetectionOffset);
PhaseATraceStart = PhaseATraceEnd;
PhaseATraceEnd.Z -= TraceOffset.Z;
PhaseATraceStart.Z += TraceOffset.Z + DetectionOffset;
bool bPhaseAResultC = Owner->LineTraceSingle(this, PhaseATraceStart, PhaseATraceEnd, ECC_Visibility, false, ActorsToIgnore, EDrawDebugTraceInternal::ForDuration, TraceLocation, true);
}
else
{
bCanMove = false;
}
}
else
{
//////////////////////////////////////////////////////////////////////////////////////////////
//
// Next, let's trace for the inside and outside corner
//
//////////////////////////////////////////////////////////////////////////////////////////////
//first let's check whether we have run into an inside corner
FHitResult PhaseBHitResultA;
FVector PhaseBTraceStart = Owner->GetActorLocation();
PhaseBTraceStart.Z = EdgeData.TraceLocation.ImpactPoint.Z - DetectionOffset;
FVector PhaseBTraceEnd = PhaseBTraceStart + (Owner->GetActorRightVector() * (TraceOffset.X + DetectionOffset * 2.f) * ValueAbs);
bool bPhaseBResultA = Owner->LineTraceSingle(this, PhaseBTraceStart, PhaseBTraceEnd, ECC_Visibility, false, ActorsToIgnore, EDrawDebugTraceInternal::ForDuration, PhaseBHitResultA, true);
TraceNormal = PhaseBHitResultA;
//if we get another hit, perform the second trace of phase B to check whether the ledge is valid
//we can also assume that if this is true, there won't be an outside corner
if (bPhaseBResultA)
{
FHitResult PhaseBHitResultB;
PhaseBTraceEnd = PhaseBHitResultA.ImpactPoint + (PhaseBHitResultA.ImpactNormal * -DetectionOffset);
PhaseBTraceStart = PhaseATraceEnd;
PhaseBTraceEnd.Z -= TraceOffset.Z;
PhaseBTraceStart.Z += TraceOffset.Z + DetectionOffset;
bool bPhaseBResultB = Owner->LineTraceSingle(this, PhaseBTraceStart, PhaseBTraceEnd, ECC_Visibility, false, ActorsToIgnore, EDrawDebugTraceInternal::ForDuration, PhaseBHitResultB, true);
TraceLocation = PhaseBHitResultB;
if (!bPhaseBResultB)
{
bCanMove = false;
}
}
else
{
//////////////////////////////////////////////////////////////////////////////////////////////
//
// if the result was false, let's check for an outside corner, as the final step in our trace for ledges
//
//////////////////////////////////////////////////////////////////////////////////////////////
FHitResult PhaseCHitResultA;
FVector PhaseCTraceStart = SocketLocation + (Owner->GetActorRightVector() * TraceOffset.X * ValueAbs) + (Owner->GetActorForwardVector() * DetectionOffset);
PhaseCTraceStart.Z = EdgeData.TraceLocation.ImpactPoint.Z - DetectionOffset;
FVector PhaseCTraceEnd = PhaseCTraceStart + (Owner->GetActorRightVector() * (TraceOffset.X + 1.f) * (ValueAbs*-1));
bool bPhaseCResultA = Owner->LineTraceSingle(this, PhaseCTraceStart, PhaseCTraceEnd, ECC_Visibility, false, ActorsToIgnore, EDrawDebugTraceInternal::ForDuration, PhaseCHitResultA, true);
TraceNormal = PhaseCHitResultA;
if (bPhaseCResultA)
{
FHitResult PhaseCHitResultB;
PhaseCTraceEnd = PhaseCHitResultA.ImpactPoint + (PhaseCHitResultA.ImpactNormal * -DetectionOffset);
PhaseCTraceStart = PhaseCTraceEnd;
PhaseCTraceEnd.Z -= TraceOffset.Z;
PhaseCTraceStart.Z += TraceOffset.Z + DetectionOffset;
bool bPhaseBResultB = Owner->LineTraceSingle(this, PhaseCTraceStart, PhaseCTraceEnd, ECC_Visibility, false, ActorsToIgnore, EDrawDebugTraceInternal::ForDuration, PhaseCHitResultB, true);
TraceLocation = PhaseCHitResultB;
//the chance of this being false is small, but not impossible, so we need to keep this one in
if (!bPhaseBResultB)
{
bCanMove = false;
}
}
else
{
bCanMove = false; //the final check failed, so player cannot move.
}
}
}
FEdgegrabData Result;
Result.bCanMove = bCanMove;
Result.TraceLocation = TraceLocation;
Result.TraceNormal = TraceNormal;
return Result;
}
The input part
void UBK_PlayerState_Edgegrabbing::MoveRight(float Value)
{
FEdgegrabData EdgeData;
if (Value != 0.f)
{
EdgeData = DetectLedge(Value);
if (EdgeData.bCanMove)
{
//first get the opposite socket location, which we need later to determine the angle.
FVector SocketLoc;
if (Value < 0.f) //-1 is left, 1 is right
{
SocketLoc = Owner->GetMesh()->GetSocketLocation("Socket_EdgeGrab_Right");
}
else
{
SocketLoc = Owner->GetMesh()->GetSocketLocation("Socket_EdgeGrab_Left");
}
FRotator NewRotation = EdgeData.TraceNormal.ImpactNormal.Rotation();
NewRotation.Yaw += 180.f;
FVector NewLocation = EdgeData.TraceLocation.ImpactPoint + Owner->GetActorRightVector()*(Value*10.f);// -(Owner->GetActorForwardVector() * TraceOffset.X);
FVector PawnAndSocketOffset = SocketLoc - Owner->GetActorLocation();
NewLocation -= PawnAndSocketOffset;
Owner->SetActorRotation(FMath::RInterpTo(Owner->GetActorRotation(), NewRotation, GetWorld()->GetDeltaSeconds(), 20.f));
Owner->SetActorLocation(FMath::VInterpTo(Owner->GetActorLocation(), NewLocation, GetWorld()->GetDeltaSeconds(), 20.f));
}
}
}
I am using socket locations for traces, to have something a bit more flexible (and since the placement is going to rely on the animation rather than the capsule component location, I figured this was better).