[Unity] Zerry Library (9) - Point To Click Movement & Cinemachine

suhan0304·2024년 7월 14일

유니티 - Zerry Library

목록 보기
8/11
post-thumbnail

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


일단 이동을 먼저 구현하기 전에 Cinemachine을 먼저 작성해보자. GameObject > Cinemachine > Virtual Camera를 하나 추가해준다.

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

이러면 알아서 플레이어를 따라다니면서 카메라가 움직일 것이다. 이제 Point To Click Movement를 구현해보자.


InputControls

먼저 마우스 클릭을 받아와야 하기 때문에 Input Action을 하나 생성해준다.

  • Walk : Left Button [Mouse]
  • Run : Left Button [Mouse]

이 때 Run에는 Multi Tap Interactions를 하나 추가해줘서 마우스를 두 번 클릭하면 Run을 하도록 해보자. 이 때 Max Tap Spacing은 각 탭 사이에 허용되는 최대 지연 시간이다. 아래처럼 설정하면 0.25초 안에 두 번 연속 클릭을 하면 Run이 인식된다.


Route the events via code

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

이제 이벤트를 처리하는 스크립트를 작성해보자.

PlayerMovementController.cs

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를 추가해준 후에 실행해보면 아래와 같이 잘 인식되는 것을 확인할 수 있다.


Setup the navigation system

중요한 점은 우클릭한 위치로 플레이어가 이동할 때 단순히 직선으로 이동하는게 아니라 최적의 경로를 찾아서 이동해야 한다는 점이다. 그래서 유니티의 Navigation System을 사용한다.

플레이어에 Nav Mesh Agent 컴포넌트를 추가해주고 다음 속성을 설정한다.

  • Steering > Speed : 2.5
  • Obstacle Avoidance > Height : 2

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

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

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


Rotate the player

플레이어는 걷기 시작하기 전에 걸을 방향을 향하도록 회전해야 한다. 아래와 같이 세 단계로 수행된다.

  1. 먼저 한 번의 왼쪽 클릭에 대해 입력을 감지
  2. 레이캐스트를 사용하여 플레이어가 월드 공간에서 걸을 수 있는 가장 가까운 장소를 탐색
  3. 위치가 감지되면 플레이어는 방향을 향하도록 회전

PlayerMovementController.cs

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을 반환한다.

이제 좌클릭을 하는 방향으로 플레이어가 회전하는 것을 볼 수 있다.


Make the player walk

이제 플레이어를 걷게 해보자. 로테이션이 끝난 후에 구현한다.

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);
            }
        }
    }
}

이제 실행해보면 마우스 클릭 좌표로 이동하는 것을 확인할 수 있다.


Make the player run

Run을 아래와 같이 작성해주면 뛰도록 해줄 수 있다.

private void Run(CallbackContext context)
{
    CurrentMovement = MovementStates.Run;
    AnimationController.Instance.CurrentState = CurrentMovement;
}


Show a ‘click position’ decal

플레이어가 위치를 확인하는 방법으로 클릭한 위치에 나타나는 애니메이션 원을 추가해보자.

MovementIndicator라는 새 오브젝트를 만들고 Position을 (0, 0.1, 0), Rotation을 (90, 0, 0)으로 설정해준다. 그런 다음 Particle System을 추가해주고 속성을 아래와 같이 설정해준다.

  • Looping: False
  • Start Lifetime: 0.5
  • Play on Awake: False
  • Max Particles: 1

그 다음 Emission을 활성화 해주고 Rate Over Time을 0. 으로해주고 Burst는 아래와 같이 설정해준다.

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

마지막으로Renderer은 아래와 같은 속성으로 설정해준다.

  • Renderer Mode: Mesh
  • Meshes: Quad
  • Material: Walk Decal
  • Renderer Alignment: Local

이제 아래와 같이 스크립트를 업데이트 해준다.


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);
            }
        }
    }
}

아래와 같이 구현을 완료했다. 장애물도 자동으로 피해가는 것을 확인할 수 있다.

profile
Be Honest, Be Harder, Be Stronger

0개의 댓글