Game/Unity

Pakour - 경사 및 낮은 낙차 처리

hvv_an 2025. 6. 17. 15:25

 

 

경사 및 낮은 낙차

경사면을 오를 때 뚝뚝 끊기는 현상과 낮은 낙차를 내려갈 때 점프 애니메이션이 나오는 부분을 개선해 보자.

그리고 점프 시 콜라이더는 크게 상승하지 않아 캐릭터의 발은 걸리지 않았는데 콜라이더에 의해 충돌되는 문제도 같이 해결해 보자.


 

 

 

 

 

경사 이동

경사면을 오를 때 뚝뚝 끊기는 원인은 CharacterController에서 Move를 통해 이동을 수행하면서 충돌 발생 시 벽, 경사로, 계단에 대한 체크를 진행하기 때문이다.

CharacterController에는 Step Offset, Slope Limit이라는 옵션이 존재한다.

Step Offset은 오를 수 있는 계단의 높이 차에 대한 설정 값이고 Slope Limit은 오를 수 있는 경사로의 최대 각도를 의미한다.

CharacterController에서 충돌을 판정할 때 Step Offset이 Slope Limit보다 우선순위가 높아 경사면 폴리곤에 미세한 턱이 존재한다면 계단으로 인식하여 처리된다.

if (수직 높이 차이 <= Step Offset) {
    // 계단 오르기 시도
    캐릭터를 위로 올린 후 앞으로 이동
}
else if (표면 각도 <= Slope Limit) {
    // 경사면으로 인식
    경사면을 따라 이동
}
else {
    // 벽으로 인식
    이동 차단 또는 미끄러짐
}

따라서 Step Offset을 높여도 어느 정도의 경사면은 자연스럽게 올라갈 수 있다.

하지만 이로 해결되지 않는 경우도 있다.

따라서 계단과 경사로를 감지해서 자연스럽게 올라가도록 해야 한다.

경사로의 경우 이동하려는 벡터를 경사로에 투영하여 경사로를 따라가는 벡터를 만들어 적용하면 된다.

하지만 계산의 경우 수직 벡터가 있기 때문에 그대로 투영하는 것은 불가능하다.

 

이를 해결하기 위해서는 경사면을 체크해야 한다.

경사면을 체크하는 방법은 여러 가지가 있다.

처음에는 FootTracker를 통해 발의 위치를 받아올 수 있기 때문에 이를 이용하여 왼발과 오른발의 위치에서 지면에 ray를 쏘고 hit 지점의 y좌표의 차이로 경사각을 구하려 했다.

이는 잘 동작하는 듯 보였으나 발이 애니메이션을 통해 동일한 높이에 있게 되면 문제가 발생했다.

 

따라서 애니메이션과 경사로 판정을 분리하였고 삼점 탐지를 통해 현재 플레이어가 밟고 있는 땅의 normal을 구하였다.

private void UpdateFootGroundInfo()
{
    // 삼점 탐지
    var currentPos = GetGroundPosition(transform.position + transform.up * footUpOffset);
    var forwardPos = GetGroundPosition(transform.position + transform.forward * footStepOffset + transform.up * footUpOffset);
    var rightPos = GetGroundPosition(transform.position + transform.right * footStepOffset + transform.up * footUpOffset);

    groundNormal = Vector3.Cross(forwardPos - currentPos, rightPos - currentPos);
    groundAngle = Vector3.Angle(groundNormal, Vector3.up);

    isOnSlope = groundAngle is > 5f and < 55f;
}

화면에 보이는 파란 선이 normal을 나타내는 선분이다.

발근처에 Cyan색의 스피어들이 경사를 판정하기 위해 ray를 쏜 hit 지점이다.

 

이제 올라가지 못하는 각도의 경사는 올라갈 수 없게 막는 로직을 추가해 보자.

경사로 취급할 임계값과 올라갈 수 있는 각도의 한계를 playerMovementData에 정의했다.

public class PlayerMovementDataSo : ScriptableObject
{
	...

    // Slope
    public float slopeLimit;
    public float slopeThreshold;

	...
}

FootTracker에서 Threshold를 넘으면 경사로 취급하고 Controller에서 Limit이 넘는 경사는 이동하지 않게 막으면 된다.

private void UpdateFootGroundInfo()
{
    // 삼점 탐지
    var currentPos = GetGroundPosition(transform.position + transform.up * footUpOffset);
    var forwardPos = GetGroundPosition(transform.position + transform.forward * footStepOffset + transform.up * footUpOffset);
    var rightPos = GetGroundPosition(transform.position + transform.right * footStepOffset + transform.up * footUpOffset);

    groundNormal = Vector3.Cross(forwardPos - currentPos, rightPos - currentPos);
    groundAngle = Vector3.Angle(groundNormal, Vector3.up);

    isOnSlope = groundAngle > playerMovementData.slopeThreshold;
}
if (footTracker.isOnSlope)
{
    // 올라가지 못하는 경사 처리                
    if (footTracker.groundAngle > playerMovementData.slopeLimit) return;

    _slopeMovement = Vector3.ProjectOnPlane(deltaPosition, footTracker.groundNormal).normalized * deltaPosition.magnitude * speedMult;
    characterController.Move(_slopeMovement);
}

 

그런데 한 가지 문제가 있다.

현재 시스템으로는 세 점의 hit지점으로 지면의 normal을 계산하기 때문에 절벽 같은 경우 noraml이 가파른 내리막처럼 판정이 될 것이다.

따라서 절벽 앞에서 떨어지지 못하는 문제가 있다.

이를 해결하기 위해 forward 쪽의 hit지점과 현재 위치의 hit지점의 높이 차이를 계산하여 오르막길인지 내리막길인지 판정하여 오르막길의 경우에만 이동을 제한시켰다.

isUp = forwardHit.point.y > currentHit.point.y;
private void OnAnimatorMove()
{
    ...
    if (footTracker.isUp && footTracker.groundAngle > playerMovementData.slopeLimit) return;

    ...
}


 

 

 

 

 

3점 탐지 오류

3점 탐지를 통해 지면의 기울기를 구하는 것은 효과적이나 문제가 발생할 경우가 있다.

캐릭터가 회전하며 3점 탐지 지점이 올바르게 되지 않을 때 이다.

이는 right hit 지점이 current와 같은 지면이 아닐 경우 발생한다.

이는 사실 간단하게 해결할 수 있다.

right hit지점은 실제 hit 될 필요가 없다.

무슨 말이냐면 right는 현재와 앞쪽 지점의 기울기를 계산하기 위한 보조 지점이라는 얘기이다.

또한 캐릭터는 앞으로만 이동하기 때문에 right를 실제로 hit 시킬 필요가 없다는 것이다.

right를 current에서 right Vector를 더해줘서 가상의 지점을 만들어 주면 간단하게 해결할 수 있다.


 

 

 

 

 

 

낮은 낙차

높이 차이가 많이 나지 않는 단차를 내려올 때도 jump 상태가 되었다가 착지하는 애니메이션이 실행되어 조작감이 좋지 않다.

이를 개선하기 위해 높이 차이가 낮은 턱은 착지 애니메이션이 발생하지 않도록 개선할 필요가 있다.

 

착지 애니메이션이 출력되는 조건은 grounded가 false가 되어 Jump상태에 있다가 착지하여 grounded가 true가 되면 실행된다.

이를 개선하기 위해서는 StateMachineBehaviour를 활용하여 낙차를 받아와 애니메이션을 실행할지 넘길지 결정하면 된다.

public class ParkourLanding : StateMachineBehaviour
{
    private ParkourCharacterController _controller;
    
    public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        _controller = animator.gameObject.GetComponent<ParkourCharacterController>();
        if (!_controller) return;

        if (!_controller.playLandingAnim)
        {
            // 애니메이션 즉시 변환
            animator.Play("Idle", layerIndex);
        }
    }
}

 

Landing 애니메이션은 실행이 안되지만 Jump 애니메이션이 실행된 이후 바로 Idle로 전환되어 조금 어색한 부분이 있다.

Jump자체가 실행되지 않도록 변경하는 게 좋을 것 같다.

 

Jump가 실행되는 조건은 grounded가 false일 때이다.

발이 떨어지는 판정은 Sphere로 체크하고 있기 때문에 transform보다 약간 뒤까지 판정이 될 것이다.

따라서, 발이 떨어지는 순간 아래로 ray를 쏘면 낙하지점의 y좌표를 얻을 수 있을 것이다.

var currentGrounded = Physics.CheckSphere(checkPosition, playerMovementData.groundedRadius, playerMovementData.groundLayer, QueryTriggerInteraction.Ignore);
// 발이 떨어지는 순간
if(_grounded && !currentGrounded)
{
    if (Physics.Raycast(transform.position, Vector3.down, out var hit, float.MaxValue,
            playerMovementData.groundLayer, QueryTriggerInteraction.Ignore))
    {
        _grounded = hit.distance < playerMovementData.fallThreshold;
    }
}
else
{
    _grounded = currentGrounded;
}