새소식

⇥ 2D Game/Unity

Unity 게임 만들기 프로젝트 - Camera, Click 이동 구현

  • -
반응형

1. Camera

보통 RPG 게임에서 Player Object 가 이동하면 Camera 는 그에 맞춰 계속 이동해야 한다. 
먼저 enum 을 작성해서 가독성을 높일 수 있도록 준비한다.

# Defins.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Define
{
    public enum MouseEvent
    {
        Press,
        Click,
    }

    public enum CameraMode
    {
        QuarterView,
    }
}

CameraMode 와 Click 이동을 구현하기 위한 MouseEvent 를 작성한다. 이후에 CameraController 를 작성해준다.

# CameraController.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraController : MonoBehaviour
{
    [SerializeField]
    Define.CameraMode _mode = Define.CameraMode.QuarterView;

    [SerializeField]
    Vector3 _delta = new Vector3(0.0f, 6.0f, -5.0f);

    [SerializeField]
    GameObject _player;

    void Start()
    {
        
    }


    void LateUpdate()
    {
        if (_mode == Define.CameraMode.QuarterView)
        {
            RaycastHit hit;
            if(Physics.Raycast(_player.transform.position, _delta, out hit, _delta.magnitude, LayerMask.GetMask("Wall")))
            {
                float dist = (hit.point - _player.transform.position).magnitude * 0.8f;
                transform.position = _player.transform.position + _delta.normalized * dist;
            }
            else
            {
                transform.position = _player.transform.position + _delta;
                transform.LookAt(_player.transform); // _player 를 주시하도록 설정 (각도 대신)

                // 유저의 위치가 먼저 변할지 카메라가 먼저 변할지 랜덤이라서 유저 먼저 이동하도록 지정 -> LateUpdate

            }
        }
    }

    public void SetQuaterView(Vector3 delta)
    {
        _mode = Define.CameraMode.QuarterView;
        _delta = delta;
    }
}

해당 스크립트는 SerializeField 로 _mode, delta, GameObject 를 받는다. 모드를 설정하고 카메라가 위치할 각도, 그리고 주시할 게임 오브젝트를 지정해준다.

LateUpdate 를 사용하는 이유는 Unity 엔진에서 Player Object가 이동한 후 카메라도 함께 이동하는데, 어떤게 먼저 이동하도록 처리할지는 항상 랜덤이기 때문에 Player Object 가 먼저 이동한 후 Camera 가 이동하여 끊김을 방지하고자 LastUpdate 를 사용한다.

RaycastHit hit;
if(Physics.Raycast(_player.transform.position, _delta, out hit, _delta.magnitude, LayerMask.GetMask("Wall")))
{
    float dist = (hit.point - _player.transform.position).magnitude * 0.8f;
    transform.position = _player.transform.position + _delta.normalized * dist;
}

만약 Camera 와 Player Object 사이에 Wall Layer 로 지정한 Object 가 있다면 Raycast 를 통해 만난 Wall Object 지점의 Point 를 계산한 후 해당 Point 와 Player Object의 magnitude 를 계산한다.
이후 해당 Wall Layer 보다 카메라가 앞쪽에 위치해야 Player Object 를 볼 수 있기 때문에 0.8 정도의 값을 곱해서 거리를 지정한다.
그리고 카메라의 위치를 Player Object 의 포지션 * 앞에서 구한 거리만큼 연산하여 벽의 앞 쪽으로 카메라를 이동 시킨다.

transform.position = _player.transform.position + _delta;
transform.LookAt(_player.transform); // _player 를 주시하도록 설정 (각도 대신)

그 외에 카메라와 Player Object 의 사이에 Wall Layer 가 없다면 Player Object 의 위치에 _delta 각도만큼 이동하고, 해당 카메라는 Player Object를 주시하도록 LookAt 을 사용해준다.

 

2. 클릭 이동 구현

# InputManager.cs

public class InputManager
{
    public Action KeyAction = null;
    public Action<Define.MouseEvent> MouseAction = null;

    bool _pressed = false;

    public void OnUpdate()
    {   
        if (Input.anyKey && KeyAction != null)
            KeyAction.Invoke();

        if (MouseAction != null)
        {
            if (Input.GetMouseButton(0))
            {
                MouseAction.Invoke(Define.MouseEvent.Press);
                _pressed = true;
            }
            else
            {
                if (_pressed)
                    MouseAction.Invoke(Define.MouseEvent.Click);
                _pressed = false;
            }
        }
    }
}

기존 InputManager 에서 마우스 클릭 이벤트를 받을 수 있도록 수정한다.
MouseAction 을 통해 MouseEvent 를 받을 수 있돌고 선언해주고, 좌클릭 이벤트가 발생했다면 _pressed 를 True, Press 발생,
이후 마우스를 띤다면 Click 이벤트를 발생시키고 _pressed 를 false 로 선언해준다.

InputManager 를 통해 마우스 이벤트를 감지하고 처리할 수 있게 되었다면 PlayerController 에서 클릭 이벤트를 통해 동작을 구현할 수 있게 되었다.

# PlayerController.cs

void OnMouseClicked(Define.MouseEvent evt)
{
    if (evt != Define.MouseEvent.Click)
        return;

    Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red, 1.0f);

    RaycastHit hit;
    if (Physics.Raycast(ray, out hit, 100.0f, LayerMask.GetMask("Wall")))
    {
        _destPos = hit.point;
        _moveToDest = true;
    }
}

먼저 OnMouseClicked 함수를 작성한다. MouseEvent 라는 값을 매개변수로 받고, evt가 만약 Click 이 아니라면 종료,
Click 이 되었다면 Ray 를 통해 클릭 된 지점의 좌표를 계산한다. 
그리고 클릭된 좌표에 존재하는 Wall Layer 와 만나는 지점의 좌표를 계산한다. 이를 _destPos 라는 목적지 좌표로 계산하고 _moveToDest 값을 통해 이동 중이라는 flag 값을 지정해주면 된다.

void Start()
    {
        Managers.Input.KeyAction -= OnKeyboard;
        Managers.Input.KeyAction += OnKeyboard;
        Managers.Input.MouseAction -= OnMouseClicked;
        Managers.Input.MouseAction += OnMouseClicked;
    }

그리고 Start 에서 동일하게 MouseAction 을 구독해주면 된다.

void Update()
    {
         if (_moveToDest)
        {
            Vector3 dir = _destPos - transform.position;
            if (dir.magnitude < 0.00001f)
            {
                _moveToDest = false;
            }
            else
            {
                float moveDist = Mathf.Clamp(_speed * Time.deltaTime, 0, dir.magnitude);
                
                transform.position += dir.normalized * moveDist;

                transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 20 * Time.deltaTime);
                transform.LookAt(_destPos);
            }
        }
    }

그리고 Update 문에서 _moveToDest 값을 통해 이동 중인지 체크하여 이동 중인 상태라면 목적지의 좌표와 PlayerObject 의 좌표 값의 차이를 구하여 벡터 값을 구한다.
벡터 값이 아주 미세한 값으로 작아지면 이동을 중지하고 _moveToDest 를 false 로 하여 이동을 종료 한다.
아직 값이 큰 차이를 지닌다면 도착하지 못한 것이므로 이동을 유지한다.
_speed * Time.deltaTime 의 값으로 이동할 텐데, 실제 도착할 거리보다 크다면 목적지를 지나칠 수 있기 때문에 Clamp 를 통해 목적지 까지의 거리를 넘지 않도록 지정해준다.
이동 거리와 방향을 곱하여 Player Object Postion 을 계속해서 더해주면서 이동하게 된다.
마찬가지로 Player Object 가 이동하면서 회전할텐데, Slerp , LookAt 을 통해 부드러운 회전을 하도록 하면 이동 구현이 완료 된다.

반응형
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.