
게임에서 구현할 수 있는 다양한 유형의 움직임이 있는데, 그 중 Point To Click은 RPG, 전략 게임과 같은 많은 분야에서 채택되는 인기 있는 이동 방식이다. 플레이어가 마우스를 클릭하는 위치에 따라 장면에서 단일 캐릭터를 이동하여 장애물을 피하고 두 가지 유형의 움직임을 번갈아 가며 수행한다. 이 때 Cinemachine을 사용하여 플레이어가 돌아다니는 동안 자동으로 포커스를 유지하도록 해보자.
일단 이동을 먼저 구현하기 전에 Cinemachine을 먼저 작성해보자. GameObject > Cinemachine > Virtual Camera를 하나 추가해준다.

속성은 하단 사진과 같이 설정해주었다. 이 때 Follow에 Player를 할당해준다.

이러면 알아서 플레이어를 따라다니면서 카메라가 움직일 것이다. 이제 Point To Click Movement를 구현해보자.
먼저 마우스 클릭을 받아와야 하기 때문에 Input Action을 하나 생성해준다.
이 때 Run에는 Multi Tap Interactions를 하나 추가해줘서 마우스를 두 번 클릭하면 Run을 하도록 해보자. 이 때 Max Tap Spacing은 각 탭 사이에 허용되는 최대 지연 시간이다. 아래처럼 설정하면 0.25초 안에 두 번 연속 클릭을 하면 Run이 인식된다.

입력 시스템에 이벤트를 연결할 수 있는 방법이 여러가지 있는데 그 중 하나는 스크립트로 이벤트를 처리하는 것이다. 코드를 작성하기 전에 Input Actions에 Generate C# Class를 체크한 Apply를 클릭한다.

이제 이벤트를 처리하는 스크립트를 작성해보자.
using UnityEngine;
using static UnityEngine.InputSystem.InputAction;
public class PlayerMovementController : MonoBehaviour
{
private InputControls _inputMapping;
private void Awake() => _inputMapping = new InputControls();
void Start() {
_inputMapping.Default.Walk.performed += Walk;
_inputMapping.Default.Run.performed += Run;
}
private void OnEnable() => _inputMapping.Enable();
private void OnDisable() => _inputMapping.Disable();
private void Run(CallbackContext context) {
Debug.Log("Run");
}
private void Walk(CallbackContext context) {
Debug.Log("Walk");
}
}
연결할 수 있는 이벤트에는 5가지 유형이 있다.
Canceled,Disabled,Performed,Started,Waiting, 각 타입에 대한 설명은 공식 문서를 참고하자.
Player 오브젝트에 PlayerMovementController를 추가해준 후에 실행해보면 아래와 같이 잘 인식되는 것을 확인할 수 있다.

중요한 점은 우클릭한 위치로 플레이어가 이동할 때 단순히 직선으로 이동하는게 아니라 최적의 경로를 찾아서 이동해야 한다는 점이다. 그래서 유니티의 Navigation System을 사용한다.
플레이어에 Nav Mesh Agent 컴포넌트를 추가해주고 다음 속성을 설정한다.

그 다음 오브젝트를 Bake 해주기 위해 Ground 오브젝트를 클릭하고 Static 체크박스를 체크한다. 그 다음 장애물을 선택하고 새 NavMeshObstacle 컴포넌트를 추가한다.


그런 다음 Window > AI > Navigation(Obsolete)을 열어서 Obstacle Object를 선택해주고 Navigation Static을 체크하고 Not Walkable로 설정해준다.

그런 다음 Bake를 해준다. 아래와 같이 상자 주변의 작은 조각 영역을 뺀 파란색으로 덮인 땅을 볼 수 있다.

플레이어는 걷기 시작하기 전에 걸을 방향을 향하도록 회전해야 한다. 아래와 같이 세 단계로 수행된다.
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.InputSystem;
using static UnityEngine.InputSystem.InputAction;
public class PlayerMovementController : MonoBehaviour
{
private InputControls _inputMapping;
private void Awake() => _inputMapping = new InputControls();
private Camera _camera;
private NavMeshAgent _agent;
private float _rotateSpeed = 5f;
private bool _needToRotate = false;
private Vector3 _moveTarget = Vector3.zero;
private Vector3 _direction = Vector3.zero;
private Quaternion _lookRotation = Quaternion.identity;
void Start() {
_inputMapping.Default.Walk.performed += Walk;
_inputMapping.Default.Run.performed += Run;
_camera = Camera.main;
_agent = GetComponent<NavMeshAgent>();
}
private void Update() {
if (_needToRotate) {
transform.rotation = Quaternion.Slerp(transform.rotation, _lookRotation, Time.deltaTime * _rotateSpeed);
if (Vector3.Dot(_direction, transform.forward) >= .97) {
_needToRotate = false;
}
}
}
private void OnEnable() => _inputMapping.Enable();
private void OnDisable() => _inputMapping.Disable();
private void Run(CallbackContext context) {
Debug.Log("Run");
}
private void Walk(CallbackContext context) {
Debug.Log("Walk");
Ray ray = _camera.ScreenPointToRay(Mouse.current.position.ReadValue());
if (Physics.Raycast(ray, out RaycastHit hit, 50f)) {
if (NavMesh.SamplePosition(hit.point, out NavMeshHit navPos, .25f, 1 << 0)) {
_moveTarget = navPos.position;
_direction = (_moveTarget.WithNewY(0) - transform.position).normalized;
_lookRotation = Quaternion.LookRotation(_direction);
_needToRotate = true;
}
}
}
}
Vector3.Dot은 두 물체가 같은 방향을 향하고 있는지 확인하는 매우 편리한 방법이다. 정확히 같은 방향을 가리키면 1, 완전히 반대 방향을 가리키면 -1, 수직이면 0을 반환한다.
이제 좌클릭을 하는 방향으로 플레이어가 회전하는 것을 볼 수 있다.

이제 플레이어를 걷게 해보자. 로테이션이 끝난 후에 구현한다.
public float _walkSpeed = 2.5f;
public float _runSpeed = 4f;
private MovementStates _currenntMovement;
public MovementStates currentMovement
{
get => _currenntMovement;
set
{
switch (value)
{
case MovementStates.Walk :
_agent.speed = 2.5f;
AnimationController.Instance.CurrentState = MovementStates.Walk;
break;
case MovementStates.Run :
_agent.speed = 4f;
AnimationController.Instance.CurrentState = MovementStates.Run;
break;
case MovementStates.None :
AnimationController.Instance.CurrentState = MovementStates.None;
break;
}
}
}
private void StopNavigation() {
_agent.SetDestination(transform.position);
CurrentMovement = MovementStates.None;
AnimationController.Instance.CurrentState = CurrentMovement;
}
위 코드 수행 로직은 아래와 같다.
CurrentMovement : 캐릭터의 현재 Movement에 따른 이동속도와 Animation을 설정해준다.StopNavigation : 플레이어의 현재 위치를 목적지로 설정해서 내비게이션 시스템을 중지, 또한 현재 재생 중인 애니메이션을 비활성화 해준다.이제 아래와 같이 Update, Walk를 리팩토링해준다.
public bool IsNavigating => _agent.pathPending || _agent.remainingDistance > .25f;
private void Update() {
if (!_needToRotate && !IsNavigating && _currentMovement != MovementStates.None) {
StopNavigation();
}
else if (_needToRotate) {
transform.rotation = Quaternion.Slerp(transform.rotation,
_lookRotation, Time.deltaTime * _rotateSpeed);
if (Vector3.Dot(_direction, transform.forward) >= .99f) {
_agent.SetDestination(_moveTarget);
AnimationController.Instance.CurrentState = CurrentMovement;
_needToRotate = false;
}
}
}
private void Walk(CallbackContext context) {
Ray ray = _camera.ScreenPointToRay(Mouse.current.position.ReadValue());
if (Physics.Raycast(ray, out RaycastHit hit, 50f)) {
if (NavMesh.SamplePosition(hit.point, out NavMeshHit navPos, .25f, 1 << 0)) {
_moveTarget = navPos.position;
_direction = (_moveTarget.WithNewY(0) - transform.position).normalized;
_lookRotation = Quaternion.LookRotation(_direction);
_needToRotate = true;
StopNavigation();
CurrentMovement = MovementStates.Walk;
if (IsNavigating && Vector3.Dot(_direction, transform.forward) >= 0.25f) {
_agent.SetDestination(_moveTarget);
}
}
}
}
이제 실행해보면 마우스 클릭 좌표로 이동하는 것을 확인할 수 있다.

Run을 아래와 같이 작성해주면 뛰도록 해줄 수 있다.
private void Run(CallbackContext context)
{
CurrentMovement = MovementStates.Run;
AnimationController.Instance.CurrentState = CurrentMovement;
}

플레이어가 위치를 확인하는 방법으로 클릭한 위치에 나타나는 애니메이션 원을 추가해보자.
MovementIndicator라는 새 오브젝트를 만들고 Position을 (0, 0.1, 0), Rotation을 (90, 0, 0)으로 설정해준다. 그런 다음 Particle System을 추가해주고 속성을 아래와 같이 설정해준다.
그 다음 Emission을 활성화 해주고 Rate Over Time을 0. 으로해주고 Burst는 아래와 같이 설정해준다.

Shape는 비활성화 해주고 Size over Lifetime을 활성화 해주고 아래와 같은 그래프로 설정해준다.

마지막으로Renderer은 아래와 같은 속성으로 설정해준다.
이제 아래와 같이 스크립트를 업데이트 해준다.
private void Walk(CallbackContext context) {
Ray ray = _camera.ScreenPointToRay(Mouse.current.position.ReadValue());
if (Physics.Raycast(ray, out RaycastHit hit, 50f)) {
if (NavMesh.SamplePosition(hit.point, out NavMeshHit navPos, .25f, 1 << 0)) {
_moveTarget = navPos.position;
WalkDecal.transform.position = _moveTarget.WithNewY(0.1f);
WalkDecal.Play();
_direction = (_moveTarget.WithNewY(0) - transform.position).normalized;
_lookRotation = Quaternion.LookRotation(_direction);
_needToRotate = true;
StopNavigation();
CurrentMovement = MovementStates.Walk;
if (IsNavigating && Vector3.Dot(_direction, transform.forward) >= 0.25f) {
_agent.SetDestination(_moveTarget);
}
}
}
}
아래와 같이 구현을 완료했다. 장애물도 자동으로 피해가는 것을 확인할 수 있다.
