Unreal port for Zelda Wall Merge
04:38 26 Feb 2026

Problem Description

I was follwing a tutorial for unity engine to make a feature of wall merging from zelda in unreal engine, and I have implemented the algorithm as described in the tutorial. The system works correctly when the angle between two wall faces is exactly 90°, but breaks when the angle is anything else (e.g. 120°, 135°, etc).

Symptoms:

  • Rotation overshoots or undershoots the target wall
  • Works perfectly for 90 degree corners

I suspect the issue comes from distance to turn calculations becasuse i assume the angle to be 90 degrees, but I’m not sure how to adjust it for arbitrary angles.


Minimal Reproducible Example

    struct FMeshPoint
    {
        FVector Position;
        FVector Normal;
    };

    TArray MeshPoints;
    TArray CornerPoints;

    float OffsetMargin = 0.1f;
    float StepSize = 50.f;
    int32 MaxSteps = 150;

    void FindNext(const FVector& StartPoint, const FVector& StartNormal)
    {
        // Store current point
        MeshPoints.Add({ StartPoint, StartNormal });

        // Detect corner when surface normal changes
        if (MeshPoints.Num() > 1)
        {
            const FVector PrevNormal = MeshPoints.Last(1).Normal;

            // Threshold works for 90° corners, fails for others
            if (PrevNormal.Dot(StartNormal) < 0.98f)
            {
                CornerPoints.Add({ StartPoint, StartNormal });
            }
        }

        if (MeshPoints.Num() >= MaxSteps)
            return;

        // Tangent along the wall
        const FVector Tangent = FVector::CrossProduct(StartNormal, FVector::UpVector).GetSafeNormal();

        // Offset slightly off the wall to avoid self-hit
        const FVector TraceStart = StartPoint + StartNormal * OffsetMargin;
        const FVector TraceEnd = TraceStart + Tangent * StepSize;

        FHitResult Hit;
        const bool bHit = GetWorld()->LineTraceSingleByChannel(
            Hit,
            TraceStart,
            TraceEnd,
            ECC_WorldStatic
        );

        if (bHit)
        {
            FindNext(Hit.ImpactPoint, Hit.ImpactNormal);
        }
    }

    void WallMove(float AxisValue, float DeltaTime)
    {
        const FVector Desired =
            AxisValue > 0 ? Target.Position : Origin.Position;

        ActorPosition = FMath::VInterpConstantTo(ActorPosition, Desired, DeltaTime, FMath::Abs(AxisValue) * 200.f);

        const float DistFromOrigin = FVector::Distance(ActorPosition, Origin.Position);

        const float TotalDist = FVector::Distance(Origin.Position, Target.Position);

        if (DistFromOrigin < DistanceToTurn || DistFromOrigin > TotalDist - DistanceToTurn)
        {
            StartRotation(AxisValue > 0);
        }
    }

    void StartRotation(bool bRight)
    {
        RotationLerp = 0.f;
        CurrentNormal = CurrentNormal.GetSafeNormal();
        TargetNormal  = Target.Normal.GetSafeNormal();

        PivotPoint = ComputePivotPoint(Origin.Position, Target.Position, CurrentNormal, DistanceToTurn);
    }

    void CornerRotate(float AxisValue, float DeltaTime)
    {
        RotationLerp = FMath::Clamp( RotationLerp + AxisValue * DeltaTime * RotationSpeed, 0.f, 1.f);

        // Works for 90°, breaks for arbitrary angles
        const FVector BlendedNormal = FMath::Lerp(CurrentNormal, TargetNormal, RotationLerp).GetSafeNormal();

        // Actor is rotated around PivotPoint
        ActorPosition = PivotPoint + (ActorPosition - PivotPoint) .RotateAngleAxis( FMath::RadiansToDegrees( FMath::Acos( FVector::DotProduct(CurrentNormal, BlendedNormal) ) ), FVector::UpVector);
    }

    FVector ComputePivotPoint( const FVector& CornerPos, const FVector& NextCorner, const FVector& Normal, float Offset)
    {
        const FVector WallDir = (NextCorner - CornerPos).GetSafeNormal();

        const FVector LineAStart = CornerPos + WallDir * Offset;

        const FVector LineADir = Normal;

        const FVector LineBStart = CornerPos + Normal * Offset;

        const FVector LineBDir = WallDir;

        FVector Intersection;
        LineLineIntersection( Intersection, LineAStart, LineADir, LineBStart, LineBDir );

        return Intersection;
    }

    bool LineLineIntersection( FVector& OutIntersection, const FVector& P1, const FVector& D1, const FVector& P2, const FVector& D2)
    {
        const FVector R = P2 - P1;
        const FVector C = FVector::CrossProduct(D1, D2);

        const float Denom = C.SizeSquared();

        if (Denom < KINDA_SMALL_NUMBER)
            return false;

        const float T = FVector::DotProduct( FVector::CrossProduct(R, D2), C ) / Denom;

        OutIntersection = P1 + D1 * T;
        return true;
    }

Question

This algorithm works perfectly for 90 degree corners, but breaks for arbitrary angles. I suspect the issue is in the distance to turn calculations, which assume a 90 degree angle. How can I adjust the algorithm to handle arbitrary angles between wall faces?

Any help or insight would be appreciated.


Additional Context

  • Unreal Engine version: 5.7.3
c++ math unreal