유니티에서 제공하는 간편한 연출 도구다. 카메라와 관련한 여러 설정들을 통해 게임의 컷씬, 연출, 시점 변경등이 가능하고 TPS 등 독특한 시점을 가지고 있는 게임들도 만들 수 있다

씨네머신

  • 게임을 영화처럼 만들어준다
  • 유니티에서 지원하는 기능. 게임의 컷씬, 연출, 시점 변경등이 가능할 수 있게 지원하는 기능이다
  • 카메라를 추가하고, 카메라의 여러 설정들을 손쉽게 가능하게 해준다

씨네머신 설치

  • 패키지 매니저를 연다
  • Unity Registry를 선택한다
  • Cinemachine 을 검색하고, 설치 해준다
  • 원한다면 추가적으로 SampleImport한다

TIP

  • 프로그래밍에 익숙해지면, 직접 기능들을 구현해서 사용하는 것이 빠를 수도 있다

Virtual Camera

  • 가장 기본적인 요소인 가상 카메라. 실제 카메라가 아니라 카메라를 제어할 Transform 정보주시대상 등에 대한 정보를 가지고 있는 컴포넌트
  • 직접적인 카메라 추가보다 성능 상의 이득을 볼 수 있다

가상 카메라 추가

  • 하이라키 창에서 위와 같이 선택하면 추가된다
  • 추가하면, 자동으로 메인 카메라에는 CinemachineBrain 컴포넌트가, 가상 카메라에는 CinemachineVirtualCamera 컴포넌트가 추가된다
  • Default Blend로 블렌드 방법을 조절, 블렌드가 끝나기 까지의 시간도 조절 할 수 있다
  • 가상 카메라에서의 설정이다. Follow : 따라다닐 대상, Look At : 주시할 대상

카메라 블렌딩하기

  • 가상 카메라를 같은 방식으로 하나 더 추가한다
  • 카메라를 원하는 위치로 배치한다

스크립트 작성

  • 카메라 전환 버튼을 구현하기 위해 스크립트를 작성한다
  • 네임스페이스 Cinemachine을 추가해줘야 한다
using Cinemachine;

public class CamChanger : MonoBehaviour
{
    [SerializeField] private CinemachineVirtualCamera[] cameras;
    [SerializeField] private Button[] buttons;

    private void OnEnable()
    {
        buttons[0].onClick.AddListener(FirstCamOn);
        buttons[1].onClick.AddListener(SecondCamOn);
    }
    private void FirstCamOn()
    {
        cameras[0].Priority = 11;
    }
    private void SecondCamOn()
    {
        cameras[0].Priority = 9;
    }
    private void OnDisable()
    {
        buttons[0].onClick.RemoveListener(FirstCamOn);
        buttons[1].onClick.RemoveListener(SecondCamOn);
    }
}

실행

  • 빈 오브젝트를 하나 생성하고 스크립트를 추가한다

  • 버튼 1,2에 각각 카메라 1,2를 넣어준다

  • 플레이를 누르고 버튼을 눌러보면 카메라가 전환 되는 것을 확인할 수 있다. 각각 First Cam, Second Cam 이다

  • BodyDamping을 설정해줘서 캐릭터가 격하게 움직일 때의 카메라 움직임을 부드럽게 만들 수 있다

Blend List

  • 전환 될 가상카메라들에 대한 정보를 지니고 있는 가상 카메라 그룹. 개별 카메라로 넘어가는 과정과 시간을 제어할 수 있다

블렌드 리스트 추가

  • 하이라키 창에서 위와 같이 추가한다
  • 블렌드 리스트 하위로 버추얼 카메라 3대를 추가한다. 추가한 다음, 각 카메라들의 위치를 설정해준다

설정

  • Loop : 카메라 전환을 루프 시킨다
  • Blend in : 블렌드 방법, 전환 시간, 유지 시간을 정해줄 수 있다

실행

  • 재생해보면 정해둔 시간에 따라 카메라 전환이 일어난다

Clear Shot

  • 여러 대의 가상 카메라를 대기시키고 주시대상이 다른 오브젝트(벽, 지형)등에 가려졌을 때 주시대상을 볼 수 있는 카메라로 자동으로 전환한다

CleaShot Camera 추가

  • 하이라키 창에서 위와 같이 추가

설정

  • 블렌드 리스트와 거의 비슷하다 블렌드 방법, 전환 시간을 설정 할 수 있다
  • Priority 설정으로 더 높은 값을 가진 카메라 순으로 먼저 비춰진다

  • 이렇게 장애물 인식을 위해 Walls 레이어로 설정해준다
  • 하위 가상 카메라를 보면, 위와 같이 Cinemachine Collider 컴포넌트가 있다
  • 여기서 Collide Against 에서 레이어를 지정해주면, 해당 레이어에 주시대상이 가려지면 자동으로 카메라가 전환된다

실행


  • 위와 같이 기존의 카메라에서 벗어나면 다른 카메라로 자동으로 전환된다

주의점

  • 모든 카메라가 벽 바깥이면 캐릭터를 볼 방법이 없어진다

Dolly Cam

  • 기찻길을 깔고 그 위에서만 카메라가 이동하게 할 수 있다

Dolly Camera With Track 추가

  • 위와 같이 추가하면 된다

  • 그러면 이렇게 오브젝트 2개가 추가된다

  • 먼저 WayPoint를 추가해서 기찻길이 지나갈 구간들을 정해준다. 이후, Looped를 체크하면 자연스럽게 원형으로 구현된다

  • 가상 카메라에서 주시대상을 설정해준다

  • 가상 카메라에서 Body -> Auto Dolly -> Enabled를 체크해준다

실행


  • 캐릭터의 움직임에 따라 카메라가 트랙을 이동하면서 따라온다

Dolly Track With Cart 추가

  • 같은 이름으로 Cinemachine에서 고르면 된다. 그러면 위 사진과 같이 추가된다. 가상 카메라는 추가해준 것이다
  • Track은 위와 같은 방법으로 원하는 대로 설정해준다. 속도는 5 정도를 넣어본다
  • 여기서 Path에 위에서 설정한 Track을 참조시킨다
  • 가상 카메라의 FollowDolly Cart를, Look At은 플레이어 캐릭터를 참조시키면 된다
  • BodyAuto Dolly -> Enabled를 체크해준다

실행

  • 지정한 속도에 따라 카트가 움직이며, 카메라가 주시대상을 쳐다본다

TIP

  • Dolly Track은 일반적인 게임 오브젝트에도 적용할 수 있다. 지정된 경로를 반복적으로 움직이게 해야할 때 쓸 수 있다.

Free Look

  • 주시 대상에 대한 자유로운 시점 변환이 가능. Top, Middle, Bottom 시점을 각자 다른 위치를 조절할 수 있어, 캐릭터를 클로즈업 하는 TPS 시점을 구현하기 좋다
  • 이번에는 TPS의 시점을 구현해 볼 것이다

추가

  • 하이라키 창에서 위와 같이 추가한다
  • 그러면 위와 같은 오브젝트가 추가된다

설정

  • Follow로 플레이어 캐릭터를 넣어준다
  • Orbits는 위와 같이 설정 해준다
  • Top, Middle, Bottom Rig를 각각 위와같이 플레이어 캐릭터 오브젝트의 안쪽에 있는 오브젝트들을 추가해준다
  • Axis Control 에서 세부적인 조정으로 보다 자연스러운 카메라 구현이 가능하다

실행

  • 각각 Top, Middle, Bottom 시점이다. 매우 자연스럽게 전환된다

Target Group

  • 여러 오브젝트들을 한 화면에 담을 수 있도록 중간 지점을 포커싱 하는 기능. 무리를 표현할 때 쓰기 좋다

추가

  • 하이라키 창에서 위와같이 추가한다
  • 먼저, 새로 캐릭터 들을 씬 창에 추가한 다음 Target에 캐릭터들을 참조 시킨다
  • Weight를 설정하면 가중치를 넣어 줄 수 있다
  • Radius는 공처럼 생각하면 된다. 대상을 기준으로 카메라가 잡는 범위를 설정한다
  • 타겟 중 하나를 멀리 떨어뜨려도 화면에 잡는 것을 볼 수 있다. 단, 너무 멀리 떨어지면 화면에서 벗어난다

Impulse

  • 화면의 진동을 구현하기 위한 기능. 일반 오브젝트에도 진동, 지진효과를 적용하는 것이 가능하다
  • Impulse Source : 충격을 발생시키는 원인이 된다.
  • Impulse Listener : 충격 원인으로 인해 충격을 받는 것을 구현할 수 있다

설정

  • 먼저, 핸드헬드 효과를 적용해보자. 가상카메라를 추가하고, Add Extension에서 CinemachineImpulseListener를 추가한다
  • Noise에서 사진과 같이 설정해준다. Noise Profile에서 Handheld 중 아무거나 선택하면 된다
  • 전공이라서 이미 알고 있지만, 짚고 넘어간다. 해당 그래프는 A×y=sin(2πft)A\times y=sin(2πft)의 그래프에서 각각 Amplitude Gain , Frequency Gain을 바꿀 경우를 보여준다.
  • Amplitude강도, 식에서는 AA에 해당한다. 커질 수록 진동이 강해진다
  • Frequency gain주기, 식에서는 ff에 해당한다. 커질수록 반복 주기가 짧아진다 = 더 빠르게 진동한다
  • 공중에 Sphere를 배치하고, Rigidbody 컴포넌트를 추가해서 재생 시 땅으로 떨어지게 한다. Impulse Source 컴포넌트를 추가해준다. 땅에 충돌 시 Impulse Source가 수행된다
  • 가상 카메라를 추가하고 Impulse Listener 컴포넌트를 추가한다
  • 추가로 Cube 들도 배치해서 Impulse Listener를 추가한다

실행

  • 플레이 해보면, Shpere가 땅에 충돌할 때마다 카메라 뿐만 아니라 Cube들 또한 진동이 일어난다

TIP

  • 게임의 진동처럼 만들고 싶으면 Reaction Settings를 조절 해보자

TPS 게임 만들기 - 1

  • 이 TPS 게임은 장기적으로 새로 배우는 기능들이 추가될 거다
  • 예시 게임은 몬스터 헌터 4G이다. 사용 무기는 라이트 보우건. 무기 조준 시, 화면이 확대되며 캐릭터가 정중앙만 바라본다. 오늘은 이것을 구현해 볼 것이다

프로젝트 설계

  • 일단 플레이어 캐릭터가 어떤 요소들을 가질 지 알아보자

PlayerCharacter

  • 플레이어 캐릭터는 다음과 같은 것들을 가진다

Controller

  • Status, Movement, Weapon을 관리한다

Status

  • 캐릭터의 능력치
  • 상태

Movement

  • 캐릭터의 회전, 이동
  • 기능

Weapon

  • 총으로 가정
  • 연사

구현


  • 플레이어 아바타Aim. Aim은 조준 중 플레이어 시점의 카메라 상, 하 회전을 담당한다. 좌, 우는 캐릭터 자체가 회전한다. 하위로 muzzlePoint도 넣을 수 있다(총게임)
  • 메인카메라를 Idle Camera로, 카메라를 추가해서 Aim Camera로 이름을 바꾸고 플레이어 캐릭터의 하위로 넣는다
  • Idle CameraTPS 시점, Aim Camera는 총을 조준했을 때처럼 배치한다

스크립트 구현

  • 플레이어 캐릭터에 추가할 컴포넌트들을 작성한다. 먼저, 싱글톤 패턴옵저버 패턴을 사용할 예정이니 그와 관련해 스크립트를 작성한다
  • 디자인 패턴들을 구현하는 것은 따로 네임스페이스(Design Pattern)를 정해줬다

Singleton

  • 싱글톤 설정 스크립트. 싱글톤들의 부모 클래스로서의 역할을 하게 된다
namespace DesignPattern
{
    // 싱글톤을 MonoBehaviour를 상속받는 컴포넌트 타입으로 제한한다
    public class JYL_SingleTon<T> : MonoBehaviour where T : MonoBehaviour
    {
        private static T instance;
        public static T Instance
        {
            get
            {
                if (instance == null)
                {
                    // 프로젝트 전체(씬 포함)에서 찾아서 있으면 가져온다
                    // 전역으로 한 개의 인스턴스만 보장
                    instance = FindObjectOfType<T>();
                    // 씬 전환 시에도 파괴되지 않게 설정
                    DontDestroyOnLoad(instance);
                }
                return instance;
            }
        }
        // 자식 클래스들도 써야하기 때문에 protected 설정
        protected void SingletonInit()
        {
            // 이미 해당 싱글톤이 있을 경우
            if (instance != null && instance != this)
            {
                Destroy(gameObject);
            }
            else
            {
                // 요거와 같음 GetComponent<T>();
                // 근데, GetComponent는 탐색 시간이 더 길기 때문에 아래와 같이 쓴다
                instance = this as T;
                DontDestroyOnLoad(instance);
            }
        }
    }
}

ObservableProperty

  • 옵저버 패턴의 주시대상, 구독, 해지를 편하게 세팅하기 위한 클래스
  • System 네임스페이스에서 IObservable 인터페이스가 기본 제공되어 있긴 하다. 이번에는 직접 구현한다
namespace DesignPattern
{
    public class JYL_ObservableProperty<T>
    {
        [SerializeField] private T value;
        public T Value
        {
            get
            {
                return value;
            }
            set
            {
                // 기존의 값과 같다면 return
                if (this.value.Equals(value)) return;
                this.value = value;
                // 값이 변경되면 자동으로 알림 = 콜백, 이벤트
                Notify();
            }
        }
        private UnityEvent<T> OnValueChanged = new();

        // 생성자로 초기화
        // value 입력이 없으면 기본값으로 설정
        public JYL_ObservableProperty(T value = default)
        {
            this.value = value;
        }

        // 사실 AddListener로 바로 써도 되지만, 가독성을 위해 새로 작성
        public void Subscribe(UnityAction<T> action)
        {
            OnValueChanged.AddListener(action);
        }
        private void Notify()
        {
            OnValueChanged?.Invoke(value);
        }
        public void UnSubscribe(UnityAction<T> action)
        {
            OnValueChanged.RemoveListener(action);
        }
        public void UnSubscribeAll()
        {
            OnValueChanged.RemoveAllListeners();
        }
    }
}

Controller

  • 사용자의 입력을 받아 컨트롤 하는 스크립트
public class JYL_PlayerController : MonoBehaviour
{
	// 왜 퍼블릭 변수인데 프로퍼티를 쓰는지는 아래에서 확인하자
    public bool IsControlActivate { get; set; } = true;

    private JYL_PlayerStatus status;
    private JYL_PlayerMovement movement;

    [SerializeField] private GameObject mainCamera;
    [SerializeField] private GameObject aimCamera;
    [SerializeField] private KeyCode aimKey = KeyCode.Mouse1;

    void Awake() => Init();
    private void OnEnable() => SubscribeEvents();
    private void Update() => HandlePlayerControl();
    private void OnDisable() => UnsubscribeEvents();

    private void Init()
    {
        status = GetComponent<JYL_PlayerStatus>();
        movement = GetComponent<JYL_PlayerMovement>();
        mainCamera = Camera.main.gameObject;
    }

    // 이동과 에임을 합쳐서 플레이어 컨트롤로 묶음
    private void HandlePlayerControl()
    {
        // 만약, 컨트롤 할 수 없는 상황이라면 return
        if (!IsControlActivate) return;
        HandleMovement();
        HandleAiming();
    }
    
    private void HandleMovement()
	{
	    // test에서 만들었던 코드 고대로 가져옴
	
	    // 이동속도 선언
	    float moveSpeed;
	    // 만약 에임 중이라면 걷는속도로 제한한다
	    if (status.IsAiming.Value) moveSpeed = status.walkSpeed;
	    // 그 외에는 뛰는 속도로 제한을 푼다
	    else moveSpeed = status.runSpeed;
	
	    // SetMove의 반환값은 이동방향 단위벡터
	    Vector3 moveDir = movement.SetMove(moveSpeed);
	    // 현재 움직이고 있는지, 아닌지 판별 결과 대입
	    // 값에 변동이 있을 경우, 이벤트 수행 - 현재는 구독중인 함수 없음
	    status.IsMoving.Value = (moveDir != Vector3.zero);
	
	    // 플레이어 캐릭터 몸체의 회전
	    // 카메라 각도는 캐릭터의 정면방향
	    Vector3 camRotateDir = movement.SetAimRotation();
	    Vector3 avatarDir;
	    // 조준 중이라면, 캐릭터의 정면방향을 대입
	    if (status.IsAiming.Value) avatarDir = camRotateDir;
	    // Idle 카메라면 이동방향을 대입
	    else avatarDir = moveDir;
	
	    // Idle이면, 이동방향으로 캐릭터 회전, Aim이면 카메라 정중앙으로 회전
	    movement.SetAvatarRotation(avatarDir);
	
	}
    
    private void HandleAiming()
    {
        // GetKey가 bool로 반환되서 IsAiming의 값이 변함
        // 즉, 마우스 오른쪽 버튼을 클릭하고 있을 때는 true
        // 손에서 떼면 false가 된다
        // 값이 변하면, 자동으로 Notify로 이벤트 수행됨
        // 요 경우, aimCamera.gameObject.SetActive(IsAiming.Value)가 수행됨
        status.IsAiming.Value = Input.GetKey(aimKey);
    }
    
    public void SubscribeEvents()
    {
        status.IsAiming.Subscribe(value => SetActivateAimCamera(value));
        // value => SetAcitvateAimCamera(value) 는 익명 메서드(delegate 또는 람다식) 로 평가되며
        // 결국 Action<bool> 타입에 맞는 콜백으로 전달된다
        // status.IsAiming.Subscribe(SetActivateAimCamera)과 기능적인 차이는 없다
        // 그런데 이와같이 쓰면 더 간결하지만 이해는 어렵다
        // 람다식으로 적은 이유는, 풀어 써서 이해하기 쉽게 하기 위해서다
        // 람다식이 애초에 익명메서드의 종류이기 때문에 쓸 수 있는 방식
    }
    
    public void UnsubscribeEvents()
    {
        status.IsAiming.UnSubscribe(value => SetActivateAimCamera(value));
    }
    
    private void SetActivateAimCamera(bool value)
    {
        aimCamera.SetActive(value);
        mainCamera.SetActive(!value);
    }
}

왜 public도 프로퍼티를 써야 하는가

  • 접근제한자 public으로 두는 변수라도 가급적 자동구현 프로퍼티라도 작성하자

  • 만약 값이 바뀌는 것으로 인해서 디버깅이 필요할 때 setterDebug.Log를 걸고 추적하거나, getter로도 어떤 곳 들에서 참조하고 있는지 추적하고 싶을 때 쓸 수 있다

  • public변수의 경우와 프로퍼티의 경우 각각 IDE에서 어디서 참조했는지 뜨는 기능들에서 차이를 보인다

    public 변수에 프로퍼티를 쓰는 이유

  • getter, setter에 각각 Shift +F12 키를 누르면 어디서 참조하고 있는지 확인할 수 있다. public 변수에서는 불가능하다

  • 프로퍼티 getter, setter는 함수라서 참조 갯수가 바로 뜬다. 누르면 어디서 썼는지 바로 확인할 수 있다

  • public 변수는 어디서 갖다 썼는지 확인을 할 수 없다

  • 이런 식으로 getter에 디버그 로그를 작성하여 호출되는 타이밍을 디버깅 할 수도 있다

Status

  • 플레이어 캐릭터의 상태필드가 모여있는 스크립트
public class JYL_PlayerStatus : MonoBehaviour
{
    // field 를 붙이면 프로퍼티 전용으로 사용된다
    // 그냥 SerializeField를 쓰면 프로퍼티가 있는 변수는 에디터에 뜨지 않는다
    [field: SerializeField][field: Range(0, 10)]
    public float walkSpeed { get; set; }
    [field: SerializeField][field: Range(0, 10)]
    public float runSpeed {  get; set; }
    [field: SerializeField][field:Range(0,10)]
    public float RotateSpeed { get; set; }

    // 옵저버 패턴으로 상태 패턴을 구현했다고 이해할 수 있다
    //private set으로 프로퍼티를 선언해서 직접적으로 +=,-= 대입할 수 없다. 함수로만 가능
    public JYL_ObservableProperty<bool> IsAiming { get; private set; } = new();
    public JYL_ObservableProperty<bool> IsMoving { get; private set; } = new();
    public JYL_ObservableProperty<bool> IsAttacking { get; private set; } = new();
}

Movement

  • 사용자의 입력을 토대로 움직임을 구현하는 스크립트. 수행은 컨트롤러가 담당한다
  • 플레이어가 이동 시, 구해지는 이동방향 벡터. SetMove() 함수의 결과이다
public class JYL_PlayerMovement : MonoBehaviour
{
    [Header("References")]
    [SerializeField] private Transform avatar;
    [SerializeField] private Transform aim;

    [Header("Mouse Config")]
    // 일반적으로는 이런 세팅 값들은 따로 클래스 같은 것들로 묶어 관리한다
    // 최소 각도
    [SerializeField][Range(-90, 0)] private float minPitch;
    // 최대 각도
    [SerializeField][Range(0, 90)] private float maxPitch;
    // 마우스 감도
    [SerializeField][Range(1, 10)] private float mouseSensitivity = 1;

    private Rigidbody rig;
    private JYL_PlayerStatus playerStatus;
    private Vector2 currentRotation;

    private void Awake() => Init();

    private void Init()
    {
        rig = GetComponent<Rigidbody>();
        playerStatus = GetComponent<JYL_PlayerStatus>();
    }

    public Vector3 SetMove(float moveSpeed)
    {
        Vector3 moveDirection = GetMoveDirection();

        Vector3 velocity = rig.velocity;
        velocity.x = moveDirection.x * moveSpeed;
        velocity.z = moveDirection.z * moveSpeed;

        // 플레이어 캐릭터의 현재 속도(방향) 업데이트
        rig.velocity = velocity;
        // 모델 설계상 컨트롤러에 반환을 해줘야 하기 때문에, 이동 방향만 반환
        return moveDirection;
    }

    public Vector3 SetAimRotation()
    {
        // 에임 상태에서는 캐릭터가 카메라 기준으로 정면만 바라보게 한다
        Vector2 mouseDir = GetMouseDirection();

        // x축은 제한이 없다. 360도 회전가능
        currentRotation.x += mouseDir.x;
        // y축은 위로 최대90도, 아래로 최소 -90도만 가능해야 한다
        currentRotation.y = Mathf.Clamp(currentRotation.y + mouseDir.y, minPitch, maxPitch);


        // 캐릭터가 y축을 기준으로 마우스가 좌, 우로 이동한 값 만큼 회전한다
        // 상,하, 옆으로 눕는 회전을 하지 않는다
        transform.rotation = Quaternion.Euler(0, currentRotation.x, 0);

        // 에임상태에서 캐릭터는 카메라 기준 정중앙만 바라본다
        Vector3 currentEuler = aim.localEulerAngles;
        // 세로로 입력받는 마우스의 값 수치만큼 x축에 더해줘야 위아래로 회전한다
        // 직접 rotation에서 x를 잡고 값을 변경해보면 이해가 빠르다
        // y,z값은 변동사항이 없으니 고대로 넣는다
        // x값은 에임 상태에서 카메라 상하 움직임으로 인한 회전값
        // y값은 y축을 기준으로 회전한 값
        // z값은 0이다
        // aim은 에임상태에서의 카메라다
        // 즉, 여기서는 에임 카메라의 위아래 회전만 수행한다
        aim.localEulerAngles = new Vector3(currentRotation.y, currentEuler.y, currentEuler.z);

        // 캐릭터의 정면 방향 단위벡터 반환(y값은 0)
        Vector3 rotateDirVector = transform.forward;
        rotateDirVector.y = 0;
        return rotateDirVector.normalized;
    }

    public void SetAvatarRotation(Vector3 direction)
    {
        // 들어온 벡터값이 0이면 리턴. 최적화
        if (direction == Vector3.zero) return;
        // 입력받은 Vector3 값을 Quaternion 값으로 된 forward로 변환
        Quaternion targetRotation = Quaternion.LookRotation(direction);
        // 캐릭터의 방향을 새로 만든 forward 방향으로 회전시킨다
        avatar.rotation = Quaternion.Lerp(avatar.rotation, targetRotation, playerStatus.RotateSpeed * Time.deltaTime);
    }

    private Vector2 GetMouseDirection()
    {
        float mouseX = Input.GetAxis("Mouse X") * mouseSensitivity;
        
        // - 부호로 해야된다. 안그러면 반전임. 배열과 같이 Mouse Y 는 아래일 수록 +값이기 때문이다
        float mouseY = -Input.GetAxis("Mouse Y") * mouseSensitivity;

        return new Vector2(mouseX, mouseY);
    }

    public Vector3 GetMoveDirection()
    {

        Vector3 input = GetInputDirection();

        Vector3 direction =
            // 단위벡터 (1,0,0) * input.x { (-1~1,0,0) => -1~1 }
            (transform.right * input.x) +
            // 단위벡터 (0,0,1) + input.z { (0, 0, -1~1) => -1~1 }
            (transform.forward * input.z);
            // 합 그림 그리기

        return direction.normalized;
    }

    public Vector3 GetInputDirection()
    {
        float x = Input.GetAxisRaw("Horizontal");
        float z = Input.GetAxisRaw("Vertical");
        return new Vector3(x, 0, z);
    }
}

실무 팁

  • 구성원 각자 맡은 기능들이 다른 경우에 기능들을 구현하는 방법에 대한 팁
// 참조 생성용 임시 네임스페이스 참조
// 작업물 병합 시 삭제 예정
using PlayerMovement = JYL_Test_B.PlayerMovement;
  • 이런식으로 네임스페이스를 따로 만들어 두고 스크립트를 테스트 한다
  • 기능을 구현할 때, 필드는 어떤 요소들을 가질 지, 어떤 기능들을 가질 지, 기능에는 어떤 변수들을 매개변수로 가질 지, 반환하는 값은 어떤 유형일지 미리 설계를 하고 작업에 들어간다. 그래야 다른 구성원들도 그것을 토대로 기능을 구현할 수 있다
// 테스트를 위한 네임스페이스 따로 설정
namespace JYL_Test
{

    /// <summary>
    /// Movement Test용으로 구현한 클래스.
    /// Controller 구현해야 하는 사람이 Movement 호출 관련 메서드 정리가 끝나면
    /// 해당 파일은 삭제해도 OK
    /// </summary>
    public class JYL_PlayerController : MonoBehaviour
    {
        public JYL_PlayerMovement movement;
        public JYL_PlayerStatus status;

        private void Update()
        {
            MoveTest();
            //IsAiming 변경용 테스트코드
            //GetKey의 반환값이 bool이라서 사용
            status.IsAiming.Value = Input.GetKey(KeyCode.Mouse1);
        }

        /// <summary>
        /// Controller 구현하시는 분, 요런 방식으로 구현 해주세요~
        /// </summary>
        public void MoveTest()
        {
            // 회전 수행 후 카메라 각도 대입
            Vector3 camRotateDir = movement.SetAimRotation();
            
            float moveSpeed;
            if (status.IsAiming.Value) moveSpeed = status.walkSpeed;
            else moveSpeed = status.runSpeed;
            
            // SetMove의 반환값은 이동방향
            Vector3 moveDir = movement.SetMove(moveSpeed);
            // 현재 움직이고 있는지, 아닌지 대입
            status.IsMoving.Value = (moveDir != Vector3.zero);

            // 플레이어 캐릭터 몸체의 회전
            Vector3 avatarDir;
            if (status.IsAiming.Value) avatarDir = camRotateDir;
            else avatarDir = moveDir;

            movement.SetAvatarRotation(avatarDir);
        }
    }
}
  • 이와 같이 네임스페이스를 작업자의 이름으로 따로 두고, 충돌이 일어나지 않도록 스크립트를 관리한다
  • 나중에 해당 기능의 작업자가 어떤식으로 구현을 해야 될 지, 본인이 구현한 기능을 어떤식으로 꺼내 써야 할지를 정해준다

씨네머신 추가 전용

  • 이제 앞서 배운 씨네머신으로 카메라들을 수정해본다

버추얼 카메라 추가

  • 메인 카메라는 다시 원래 위치로 되돌리고, 버추얼 카메라를 두 대 추가해 각각 Idle Camera, Aim Camera로 이름 짓는다. 그리고 미리 맞춰 뒀던 위치로 옮긴다
  • Default Blend 시간을 0.2초로 수정
  • 기존과 동일하게 최상위 빈 오브젝트들에 콜라이더, 리지드바디 추가. 캡슐의 콜라이더는 제거한다

Controller 스크립트 수정

  • 씨네머신 버추얼카메라가 추가되었으니, 해당 부분들을 수정한다
public class JYL_PlayerController : MonoBehaviour
{
    public bool IsControlActivate { get; set; } = true;
    // 퍼블릭으로 두는 변수라도 가급적 자동구현 프로퍼티로라도 두는 편인데,
    // 만약 값이 바뀌는 것으로 인해서 디버깅이 필요할때 setter에 로그 걸고 추적하거나
    // getter로도 쓸데없는데서 참조하고있지는 않은지 추적하고 싶을때 쓰려고 미리 만들어놓는것
    // public변수일때랑, 프로퍼티일때랑 IDE에서 어디서 참조했는지 뜨는 기능들에서 차이를 보인다

    private JYL_PlayerStatus status;
    private JYL_PlayerMovement movement;

    [SerializeField] private CinemachineVirtualCamera aimCamera;

    [SerializeField] private KeyCode aimKey = KeyCode.Mouse1;

    void Awake() => Init();
    private void OnEnable() => SubscribeEvents();
    private void Update() => HandlePlayerControl();
    private void OnDisable() => UnsubscribeEvents();

    private void Init()
    {
        status = GetComponent<JYL_PlayerStatus>();
        movement = GetComponent<JYL_PlayerMovement>();
        // 씨네머신으로 전환
        //mainCamera = Camera.main.gameObject;
    }

    // 이동과 에임을 합쳐서 플레이어 컨트롤로 묶음
    private void HandlePlayerControl()
    {
        // 만약, 컨트롤 할 수 없는 상황이라면 return
        if (!IsControlActivate) return;
        HandleMovement();
        HandleAiming();
    }
    private void HandleMovement()
    {
        // test에서 만들었던 코드 고대로 가져옴

        // 이동속도 선언
        float moveSpeed;
        // 만약 에임 중이라면 걷는속도로 제한한다
        if (status.IsAiming.Value) moveSpeed = status.walkSpeed;
        // 그 외에는 뛰는 속도로 제한을 푼다
        else moveSpeed = status.runSpeed;

        // SetMove의 반환값은 이동방향 단위벡터
        Vector3 moveDir = movement.SetMove(moveSpeed);
        // 현재 움직이고 있는지, 아닌지 판별 결과 대입
        // 값에 변동이 있을 경우, 이벤트 수행 - 현재는 구독중인 함수 없음
        status.IsMoving.Value = (moveDir != Vector3.zero);

        // 플레이어 캐릭터 몸체의 회전
        // 카메라 각도는 캐릭터의 정면방향
        Vector3 camRotateDir = movement.SetAimRotation();
        Vector3 avatarDir;
        // 조준 중이라면, 캐릭터의 정면방향을 대입
        if (status.IsAiming.Value) avatarDir = camRotateDir;
        // Idle 카메라면 이동방향을 대입
        else avatarDir = moveDir;

        // Idle이면, 이동방향으로 캐릭터 회전, Aim이면 카메라 정중앙으로 회전
        movement.SetAvatarRotation(avatarDir);

    }
    private void HandleAiming()
    {
        // GetKey가 bool로 반환되서 IsAiming의 값이 변함
        // 값이 변하면, 자동으로 Notify로 이벤트 수행됨.
        // 요 경우, aimCamera.gameObject.SetActive(IsAiming.Value)가 수행됨
        status.IsAiming.Value = Input.GetKey(aimKey);
    }
    public void SubscribeEvents()
    {
        // 씨네머신 전환으로 불필요
        // status.IsAiming.Subscribe(value => SetActivateAimCamera(value));
        // value => SetAcitvateAimCamera(value) 는 익명 메서드(delegate 또는 람다식) 로 평가되며
        // 결국 Action<bool> 타입에 맞는 콜백으로 전달된다
        // status.IsAiming.Subscribe(SetActivateAimCamera)과 기능적인 차이는 없다
        // 그런데 이와같이 쓰면 더 간결하지만 이해는 어렵다
        // 람다식으로 적은 이유는, 풀어 써서 이해하기 쉽게 하기 위해서다
        // 람다식이 애초에 익명메서드의 종류이기 때문에 쓸 수 있는 방식
        status.IsAiming.Subscribe(aimCamera.gameObject.SetActive);
    }
    public void UnsubscribeEvents()
    {
        // 씨네머신 전환으로 불필요
        // status.IsAiming.UnSubscribe(value => SetActivateAimCamera(value));
        // SetActive가 true, false의 bool형을 매개변수로 가지는 함수라서 들어갈 수 있다
        status.IsAiming.UnSubscribe(aimCamera.gameObject.SetActive);
    }

    //씨네머신 전환으로 불필요
    //private void SetActivateAimCamera(bool value)
    //{
    //    aimCamera.SetActive(value);
    //    mainCamera.SetActive(!value);
    //}
}

컴포넌트 추가

  • 플레이어 캐릭터에 컴포넌트 추가 및 위와 같이 세팅한다
  • 실행 해보면 마우스 이동방향을 기준으로 카메라가 회전하고, 캐릭터는 카메라를 기준으로 이동한다
  • 에임 키를 눌러보면, 캐릭터는 카메라의 정중앙을 바라보고 이동한다
  • 에임 키 안눌렀을 때
  • 에임 키 눌렀을 때
profile
개발 박살내자

0개의 댓글