Pathfinder with Animation Movement
기본적으로 Unity에서는 NavMeshAgent를 지원한다. 이 컴포넌트를 이용한다면 굉장히 쉽게 길찾기(Pathfinding)가 가능한 오브젝트를 제작할 수 있다. 하지만, NavMeshAgent의 SetDestination함수를 이용하면 타겟 위치를 주었을 때 해당 위치까지 이동시키는 기능을 쉽게 구현할 수 있다. 하지만 이 함수는 GameObject의 Transform을 엔진 코드 내에서 직접 수정하도록 설계되어 있고, 이동 관련 로직이 숨겨져 있어서 커스텀 이동을 사용하는 경우 문제가 생길 수 있다.
예를 들면 애니메이션 자체에 루트 모션이 포함되어 있는 경우 루트 모션을 활용하여 길찾기 로직을 구현하고 싶을 수 있겠지만 (보폭에 맞춰서 리얼한 모션을 보여주고 싶은 경우), 이런 애니메이션 에셋을 이용하여 길찾기 에이전트를 제작하려 한다면, 애니메이션과 NavMeshAgent가 서로 Transform을 조작하려 할 것이다. 이 상황을 해결해보자.
먼저 NavMeshAgent에는 CalcCalculatePath라는 목표 위치까지의 경로만 구해주는 함수가 있다. 이를 이용하면 된다.
PathFinder.cs
PathFinder 클래스는 NavMeshAgent 컴포넌트를 경로를 구하기 위해서만 사용하고, 직접 이동시키는 부분만 열려 있게 설계한다. 경로를 직접 이동시키는 부분은 이벤트로 만들어 외부로부터 받도록 한다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.Events;
using UnityEngine.UI;
using System;
public class Pathfinder : MonoBehaviour
{
[SerializeField]
private NavMeshAgent _agent;
public Action<Vector3> onMove;
public Action onStop;
private NavMeshPath _path = null;
private Vector3 _currentDest = new Vector3();
private int _cornerIdx = 0;
public void SetDestination(Vector3 dest)
{
Clear();
_agent.CalculatePath(dest, _path);
}
private void Awake()
{
_path = new NavMeshPath();
}
void Update()
{
if (_path.corners.Length == _cornerIdx)
{
Clear();
if (onStop != null)
{
onStop();
}
}
if (_path.status == NavMeshPathStatus.PathComplete)
{
_currentDest = _path.corners[_cornerIdx];
if (HasArriveDest())
{
_cornerIdx++;
}
else
{
Move();
}
}
}
private bool HasArriveDest()
{
if ((_currentDest - transform.position).magnitude < 0.5f)
{
return true;
}
return false;
}
private void Move()
{
if (onMove != null)
{
Vector3 toTarget = (_currentDest - transform.position).normalized;
onMove(toTarget);
}
}
private void Clear()
{
_cornerIdx = 0;
_path.ClearCorners();
}
}
PlayerCharacter.cs
Pathfinder 클래스를 직접 사용해주는 부분이다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class PlayerCharacter : MonoBehaviour
{
[SerializeField]
private NavMeshAgent _agent;
[SerializeField]
private MyAnimController _myAnimController;
[SerializeField]
private Pathfinder _pathfinder;
void Awake()
{
_pathfinder.onMove = _myAnimController.Move;
_pathfinder.onStop = _myAnimController.StopMove;
}
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
RaycastHit hit;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out hit))
{
Vector3 pos = hit.point;
_pathfinder.SetDestination(pos);
}
}
}
}
MyAnimController.cs
애니메이션을 통해 이동시키는 부분이 구현되어있다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MyAnimController : MonoBehaviour
{
[SerializeField]
protected Animator _animator;
[SerializeField]
protected float _roateSpeed = 10;
protected Dictionary<string, float> _clipTimes = new Dictionary<string, float>();
protected void Start()
{
SaveAllClipTimes();
}
protected void Hit() {}
protected void FootL() {}
protected void FootR() {}
protected void Attack()
{
_animator.SetTrigger("AttackTrigger");
Debug.Log("Attacked!!");
}
public void Move(Vector3 toTarget)
{
_animator.SetBool("Moving", true);
Quaternion targetRotation = Quaternion.LookRotation(toTarget);
transform.rotation =
Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * _roateSpeed);
}
public void StopMove()
{
_animator.SetBool("Moving", false);
}
protected void SaveAllClipTimes()
{
RuntimeAnimatorController ac = _animator.runtimeAnimatorController; //Get Animator controller
for (int i = 0; i < ac.animationClips.Length; i++) //For all animations
{
AnimationClip clip = ac.animationClips[i];
Debug.Log(clip.name + " " + clip.length);
_clipTimes.Add(clip.name, clip.length);
}
}
}
Animation Controller
컨트롤러는 대충 이렇게 생겼다.
결과 화면
잘 걷는다.
'게임 엔진 > Unity' 카테고리의 다른 글
[Unity] Hp Bar 만들기 (0) | 2020.07.13 |
---|---|
[Unity] 글로벌 이벤트 관리 방법 (1) | 2020.07.06 |
[Unity] 전략게임 카메라 제작 방법 (0) | 2020.07.06 |
[Unity] 동적타임에 NavMesh 생성하기 (0) | 2020.07.02 |
[Unity] 커스텀 에디터 제작 방법 (Editor Customize) (0) | 2020.06.30 |