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


Unity Registry를 선택한다
Cinemachine 을 검색하고, 설치 해준다Sample을 Import한다TIP
- 프로그래밍에 익숙해지면, 직접 기능들을 구현해서 사용하는 것이 빠를 수도 있다

하이라키 창에서 위와 같이 선택하면 추가된다
메인 카메라에는 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 이다

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

블렌드 리스트 하위로 버추얼 카메라 3대를 추가한다. 추가한 다음, 각 카메라들의 위치를 설정해준다Loop : 카메라 전환을 루프 시킨다Blend in : 블렌드 방법, 전환 시간, 유지 시간을 정해줄 수 있다주시대상이 다른 오브젝트(벽, 지형)등에 가려졌을 때 주시대상을 볼 수 있는 카메라로 자동으로 전환한다
하이라키 창에서 위와 같이 추가
Priority 설정으로 더 높은 값을 가진 카메라 순으로 먼저 비춰진다
Walls 레이어로 설정해준다
Cinemachine Collider 컴포넌트가 있다Collide Against 에서 레이어를 지정해주면, 해당 레이어에 주시대상이 가려지면 자동으로 카메라가 전환된다

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


위와 같이 추가하면 된다

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


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

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

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



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

TIP
Dolly Track은 일반적인게임 오브젝트에도 적용할 수 있다. 지정된 경로를 반복적으로 움직이게 해야할 때 쓸 수 있다.
주시 대상에 대한 자유로운 시점 변환이 가능. Top, Middle, Bottom 시점을 각자 다른 위치를 조절할 수 있어, 캐릭터를 클로즈업 하는 TPS 시점을 구현하기 좋다TPS의 시점을 구현해 볼 것이다
하이라키 창에서 위와 같이 추가한다

Follow로 플레이어 캐릭터를 넣어준다
Orbits는 위와 같이 설정 해준다


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


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

하이라키 창에서 위와같이 추가한다
Target에 캐릭터들을 참조 시킨다Weight를 설정하면 가중치를 넣어 줄 수 있다Radius는 공처럼 생각하면 된다. 대상을 기준으로 카메라가 잡는 범위를 설정한다
Impulse Source : 충격을 발생시키는 원인이 된다. Impulse Listener : 충격 원인으로 인해 충격을 받는 것을 구현할 수 있다
핸드헬드 효과를 적용해보자. 가상카메라를 추가하고, Add Extension에서 CinemachineImpulseListener를 추가한다
Noise에서 사진과 같이 설정해준다. Noise Profile에서 Handheld 중 아무거나 선택하면 된다
Amplitude Gain , Frequency Gain을 바꿀 경우를 보여준다.Amplitude는 강도, 식에서는 에 해당한다. 커질 수록 진동이 강해진다Frequency gain은 주기, 식에서는 에 해당한다. 커질수록 반복 주기가 짧아진다 = 더 빠르게 진동한다
Sphere를 배치하고, Rigidbody 컴포넌트를 추가해서 재생 시 땅으로 떨어지게 한다. Impulse Source 컴포넌트를 추가해준다. 땅에 충돌 시 Impulse Source가 수행된다
가상 카메라를 추가하고 Impulse Listener 컴포넌트를 추가한다
Cube 들도 배치해서 Impulse Listener를 추가한다
Shpere가 땅에 충돌할 때마다 카메라 뿐만 아니라 Cube들 또한 진동이 일어난다TIP
- 게임의 진동처럼 만들고 싶으면
Reaction Settings를 조절 해보자

Status, Movement, Weapon을 관리한다

muzzlePoint도 넣을 수 있다(총게임)


Idle Camera로, 카메라를 추가해서 Aim Camera로 이름을 바꾸고 플레이어 캐릭터의 하위로 넣는다Idle Camera는 TPS 시점, Aim Camera는 총을 조준했을 때처럼 배치한다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);
}
}
}
}
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();
}
}
}
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으로 두는 변수라도 가급적 자동구현 프로퍼티라도 작성하자
만약 값이 바뀌는 것으로 인해서 디버깅이 필요할 때 setter에 Debug.Log를 걸고 추적하거나, getter로도 어떤 곳 들에서 참조하고 있는지 추적하고 싶을 때 쓸 수 있다
public변수의 경우와 프로퍼티의 경우 각각 IDE에서 어디서 참조했는지 뜨는 기능들에서 차이를 보인다
getter, setter에 각각 Shift +F12 키를 누르면 어디서 참조하고 있는지 확인할 수 있다. public 변수에서는 불가능하다



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

이런 식으로 getter에 디버그 로그를 작성하여 호출되는 타이밍을 디버깅 할 수도 있다
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();
}
컨트롤러가 담당한다
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초로 수정
콜라이더, 리지드바디 추가. 캡슐의 콜라이더는 제거한다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);
//}
}

에임 키를 눌러보면, 캐릭터는 카메라의 정중앙을 바라보고 이동한다
에임 키 안눌렀을 때
에임 키 눌렀을 때