포인트 세트 사이에 에이전트 패트롤 생성
Unity 서비스

애니메이션과 내비게이션 연결

이 문서는 내비게이션 시스템을 사용하여 내비게이팅 휴머노이드 캐릭터가 움직이도록 설정하는 방법을 설명하기 위해 작성되었습니다.

여기서는 Unity 빌트인 애니메이션 시스템, 내비게이션 시스템, 커스텀 스크립팅을 활용해서 휴머노이드 캐릭터를 움직이게 할 것입니다.

Unity 기본 사용법과 메카님 애니메이션 시스템을 완전히 이해하고 있다는 전제 하에 시작됩니다.

예제 프로젝트를 이용할 수 있으므로 처음부터 스크립트를 추가하거나 애니메이션 및 애니메이션 컨트롤러를 설정할 필요는 없습니다.

애니메이션 컨트롤러 생성

광범위한 움직임을 커버할 수 있는, 즉각적인 대응이 가능하고 다양한 용도로 활용할 수 있는 애니메이션 컨트롤러를 갖추기 위해서는 다양한 방향으로 움직이는 애니메이션 세트가 필요합니다. 세트는 스트레이프 세트(strafe-set)라고도 불립니다.

움직이는 애니메이션 외에도 서 있는 캐릭터를 위한 애니메이션이 필요합니다.

2D 블렌드 트리에 스트레이프 세트를 정렬하는 것으로 블렌드 타입을 선택합니다. 2D Simple Directional을 이용하며 Compute Positions > Velocity XZ를 통해 애니메이션을 적용합니다.

블렌딩 조절을 위해 velxvely 두 개의 플로트 파라미터를 추가하고 블렌드 트리에 할당합니다.

여기서 7개의 달리기 애니메이션을 배치합니다. 각 애니메이션은 다른 속도로 달리고 있습니다. 앞(+ 좌/우)과 뒤(+ 좌/우)에 더해서 제자리에서 달리는 애니메이션 클립을 사용합니다. 후자는 아래의 2D 블렌드 맵의 중앙에 하이라이트되어 있습니다. 제자리 달리기 애니메이션이 필요한 이유는 두 가지가 있습니다. 첫 번째 이유는 다른 애니메이션과 혼합되는 경우에도 달리는 스타일을 유지할 수 있기 때문이며, 두 번째 이유는 혼합되는 동안 발이 미끄러지는 현상을 방지하기 때문입니다.

애니메이션 노드에 유휴 애니메이션 클립을 추가합니다(Idle). 이제 두 개의 트랜지션에 각각 해당하는 두 개의 분리된 애니메이션 상태가 준비되었습니다.

움직임과 대기 상태의 전환을 조절하려면 부울(boolean) 제어 파라미터 move를 추가합니다. 그리고 트랜지션에서 종료 시간 있음(Has Exit Time) 프로퍼티를 비활성화합니다. 이를 통해 애니메이션 중 어느 때라도 트랜지션이 트리거될 수 있습니다. 즉각적으로 대응하는 트랜지션을 위해 트랜지션 시간은 대략 0.10초로 설정합니다.

새롭게 생성한 애니메이션 컨트롤러를 움직일 캐릭터에 위치시킵니다.

계층 구조 창에서 플레이 버튼을 누르고 캐릭터를 선택합니다. 이제 수동으로 애니메이터 창의 애니메이션 값을 조절해서 움직임 상태와 속도를 변경할 수 있습니다.

다음으로 애니메이션 파라미터를 조절할 수 있는 다른 방법을 생성합니다.

내비게이션 컨트롤

NavMeshAgent 컴포넌트를 캐릭터에 적용하고 반지름, 높이를 캐릭터와 일치하도록 조정합니다. 추가적으로 속도 프로퍼티를 애니메이션 블렌드 트리의 최대 속도와 일치하도록 변경합니다.

캐릭터를 배치한 씬의 내비메시를 생성합니다.

다음으로 캐릭터에게 어디로 이동할지 지정합니다. 지정하는 방법은 애플리케이션에 따라 크게 다릅니다. 이번에는 사용자가 화면에서 클릭하는 포인트로 캐릭터가 움직이게 해서 동작을 지정합니다.

// ClickToMove.cs
using UnityEngine;
using UnityEngine.AI;

[RequireComponent (typeof (NavMeshAgent))]
public class ClickToMove : MonoBehaviour {
    RaycastHit hitInfo = new RaycastHit();
    NavMeshAgent agent;

    void Start () {
        agent = GetComponent<NavMeshAgent> ();
    }
    void Update () {
        if(Input.GetMouseButtonDown(0)) {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            if (Physics.Raycast(ray.origin, ray.direction, out hitInfo))
                agent.destination = hitInfo.point;
        }
    }
}

플레이 버튼을 누르고 나서 씬 안을 클릭하면 캐릭터가 움직입니다. 그러나 애니메이션이 움직임과 일치하지 않습니다. 에이전트의 상태와 속도를 애니메이션 컨트롤러와 연동시켜야 합니다.

에이전트의 속도와 상태 정보를 애니메이션 컨트롤러로 전달하기 위해 추가 스크립트를 더합니다.

// LocomotionSimpleAgent.cs
using UnityEngine;
using UnityEngine.AI;

[RequireComponent (typeof (NavMeshAgent))]
[RequireComponent (typeof (Animator))]
public class LocomotionSimpleAgent : MonoBehaviour {
    Animator anim;
    NavMeshAgent agent;
    Vector2 smoothDeltaPosition = Vector2.zero;
    Vector2 velocity = Vector2.zero;

    void Start ()
    {
        anim = GetComponent<Animator> ();
        agent = GetComponent<NavMeshAgent> ();
        // Don’t update position automatically
        agent.updatePosition = false;
    }
    
    void Update ()
    {
        Vector3 worldDeltaPosition = agent.nextPosition - transform.position;

        // Map 'worldDeltaPosition' to local space
        float dx = Vector3.Dot (transform.right, worldDeltaPosition);
        float dy = Vector3.Dot (transform.forward, worldDeltaPosition);
        Vector2 deltaPosition = new Vector2 (dx, dy);

        // Low-pass filter the deltaMove
        float smooth = Mathf.Min(1.0f, Time.deltaTime/0.15f);
        smoothDeltaPosition = Vector2.Lerp (smoothDeltaPosition, deltaPosition, smooth);

        // Update velocity if time advances
        if (Time.deltaTime > 1e-5f)
            velocity = smoothDeltaPosition / Time.deltaTime;

        bool shouldMove = velocity.magnitude > 0.5f && agent.remainingDistance > agent.radius;

        // Update animation parameters
        anim.SetBool("move", shouldMove);
        anim.SetFloat ("velx", velocity.x);
        anim.SetFloat ("vely", velocity.y);

        GetComponent<LookAt>().lookAtTargetPosition = agent.steeringTarget + transform.forward;
    }

    void OnAnimatorMove ()
    {
        // Update position to agent position
        transform.position = agent.nextPosition;
    }
}

스크립트에는 설명이 필요합니다. AnimatorNavMeshAgent 컴포넌트가 추가된 캐릭터에 적용되며 위에서 설명한 클릭한 포인트로 움직이는 스크립트가 적용되어 있습니다.

먼저 스크립트가 에이전트에게 자동으로 캐릭터 포지션을 업데이트하지 말라고 전달합니다. 포지션 업데이트는 스크립트의 마지막에서 다룹니다. 오리엔테이션은 에이전트에 의해 업데이트 됩니다.

애니메이션 블렌드는 에이전트 속도를 읽는 것으로 조절됩니다. 상대적인 속도로 변환되고(캐릭터 오리엔테이션에 기초) 부드럽게 조정됩니다. 변환된 수평 속도 컴포넌트는 Animator로 이어지며 이에 더해서 대기 상태와 이동 사이의 전환은 속도에 의해 조절됩니다(예를 들어, 속도 규모).

OnAnimatorMove() 콜백에서 캐릭터의 포지션을 NavMeshAgent와 일치하도록 업데이트합니다.

씬을 다시 플레이하면 애니메이션이 최대한 움직임과 일치하는 것이 확인됩니다.

내비게이팅 캐릭터의 품질 개선

애니메이션 및 내비게이션 캐릭터의 품질을 개선하기 위해 여러 옵션을 살펴보겠습니다.

바라보기

관심있는 영역으로 캐릭터를 돌려서 바라볼 수 있게 하면 관심과 기대를 전달하는 데 유용합니다. 이제 애니메이션 시스템 바라보기 API를 사용하겠습니다. 이것은 또 다른 스크립트를 호출합니다.

// LookAt.cs
using UnityEngine;
using System.Collections;

[RequireComponent (typeof (Animator))]
public class LookAt : MonoBehaviour {
    public Transform head = null;
    public Vector3 lookAtTargetPosition;
    public float lookAtCoolTime = 0.2f;
    public float lookAtHeatTime = 0.2f;
    public bool looking = true;

    private Vector3 lookAtPosition;
    private Animator animator;
    private float lookAtWeight = 0.0f;

    void Start ()
    {
        if (!head)
        {
            Debug.LogError("No head transform - LookAt disabled");
            enabled = false;
            return;
        }
        animator = GetComponent<Animator> ();
        lookAtTargetPosition = head.position + transform.forward;
        lookAtPosition = lookAtTargetPosition;
    }

    void OnAnimatorIK ()
    {
        lookAtTargetPosition.y = head.position.y;
        float lookAtTargetWeight = looking ? 1.0f : 0.0f;

        Vector3 curDir = lookAtPosition - head.position;
        Vector3 futDir = lookAtTargetPosition - head.position;

        curDir = Vector3.RotateTowards(curDir, futDir, 6.28f*Time.deltaTime, float.PositiveInfinity);
        lookAtPosition = head.position + curDir;

        float blendTime = lookAtTargetWeight > lookAtWeight ? lookAtHeatTime : lookAtCoolTime;
        lookAtWeight = Mathf.MoveTowards (lookAtWeight, lookAtTargetWeight, Time.deltaTime/blendTime);
        animator.SetLookAtWeight (lookAtWeight, 0.2f, 0.5f, 0.7f, 0.5f);
        animator.SetLookAtPosition (lookAtPosition);
    }
}

캐릭터에 스크립트를 추가하고 헤드 프로퍼티를 캐릭터의 트랜스폼 계층 구조에 할당합니다. LookAt 스크립트는 내비게이션 조절과 관련 없습니다. 그러므로 어디를 보는지 조절하려면 LocomotionSimpleAgent.cs 스크립트로 돌아가서 두 행을 추가하는 것으로 바라보기를 조절합니다. Update()의 끝을 추가하려면 다음을 더합니다.

        LookAt lookAt = GetComponent<LookAt> ();
                if (lookAt)
                    lookAt.lookAtTargetPosition = agent.steeringTarget + transform.forward;

위의 행은 LookAt 스크립트가 경로 상에 모서리가 있는 경우에는 다음 모서리에, 없는 경우에는 경로 끝에 관심 지점을 설정하도록 합니다.**

시도해야 합니다.

내비게이션을 사용해 애니메이션으로 구동되는 캐릭터

이제까지 캐릭터는 에이전트가 명시한 포지션에 따라 완전히 제어되었습니다. 이렇게 하면 다른 캐릭터나 장애물이 제어하는 캐릭터의 포지션에 겹치지 않게 되지만, 애니메이션이 원하는 속도를 지원하지 못하는 경우에는 발이 미끄러지는 현상이 발생할 수 있습니다. 여기서는 캐릭터의 제한을 조금 완화하여, 회피 품질을 희생하여 애니메이션 품질을 개선해볼 것입니다.

LocomotionSimpleAgent.cs 스크립트에서 OnAnimatorMove() 콜백을 다음 행으로 교체합니다.

    void OnAnimatorMove ()
        {
            // Update position based on animation movement using navigation surface height
            Vector3 position = anim.rootPosition;
            position.y = agent.nextPosition.y;
            transform.position = position;
        }

교체를 시도할 때 캐릭터가 에이전트 포지션에서 멀어지는 것을 볼 수 있습니다(녹색 와이어프레임 실린더). 캐릭터가 멀어지는 것을 제한해야 합니다. 이렇게 하려면 에이전트를 캐릭터를 향해 당기거나, 캐릭터를 에이전트의 위치를 향해 당기면 됩니다. LocomotionSimpleAgent.cs 스크립트의 Update() 메서드 마지막에 다음을 추가합니다.

        // Pull character towards agent
                if (worldDeltaPosition.magnitude > agent.radius)
                    transform.position = agent.nextPosition - 0.9f*worldDeltaPosition;

아니면 에이전트가 캐릭터를 따라가도록 할 수 있습니다.

        // Pull agent towards character
                if (worldDeltaPosition.magnitude > agent.radius)
                    agent.nextPosition = transform.position + 0.9f*worldDeltaPosition;

각각의 경우에 따라 가장 적합한 방법은 다를 수 있습니다.

결론

지금까지 내비게이션 시스템을 사용하여 움직이고 그에 맞춘 애니메이션을 가지는 캐릭터를 설정해보았습니다. 블렌드 시간이나 바라보기 가중치를 조절해 보십시오. 캐릭터의 품질이 개선될 뿐만 아니라, 설정을 익히는 데에도 도움이 될 것입니다.

포인트 세트 사이에 에이전트 패트롤 생성
Unity 서비스