오늘은 씨네머신으로 탑뷰와 3인칭 시점을 자유롭게 할 수 있는 방법을 남겨본다.

구현 하고자 하는 부분은 요즘 핫한 패스 오브 엑자일2 의 캐릭터 조작 방법과 유사한 방식으로 해 보았다.
거기에 카메라 회전까지 하니 나름 만족스러운 결과물이다.
InputController.cs 도 있지만 생략한다.
하지만 Zoom 설정만 일단 아래 이미지 처럼 설정해주도록 하자


우선은 CinemachineBrain을 제외한 나머지 Cinemachine 는 코드에서 동적으로 생성하여 설정해주었다.
음.. 줌과 회전은 따로 cs를 만들어서 관리 할 수 있지만, 우선 넘어가자.
// CameraConstoller.cs
using Cinemachine;
using UnityEngine;
public class CameraController : MonoBehaviour
{
private bool canRotate;
private CinemachineVirtualCamera virtualCamera;
private CinemachineComposer composer;
private CinemachineTransposer transposer;
private Vector2 mouseDelta;
// body의 시작 FollowOffset
[SerializeField] private Vector3 defaultFollowOffset;
// 카메라 회전 속도
[SerializeField] private float rotationSpeed = 1f;
// 줌 속도
[SerializeField] private float zoomSpeed = 1f;
// composer에서 살짝 기울기
[SerializeField] private Vector3 trackedObjectOffset = new Vector3(0, 0.6f, 0);
// 줌 최솟값, 최댓값
[SerializeField] private float zoomMin = 3f;
[SerializeField] private float zoomMax = 8f;
private void Start()
{
InitCamera();
}
private void InitCamera()
{
// 여기서는 VirtualCamera만 생성해주기 때문에 MainCamera에 CinemachineBrain을 수동으로 달아줘야 한다.
// 동적으로 추가해도 됨..
GameObject virtualCameraGameObject = new GameObject("VirtualCamera");
virtualCamera = virtualCameraGameObject.AddComponent<CinemachineVirtualCamera>();
// 따라다닐 대상
virtualCamera.Follow = transform;
// 주시할 대상
virtualCamera.LookAt = transform;
transposer = virtualCamera.AddCinemachineComponent<CinemachineTransposer>();
transposer.m_FollowOffset = defaultFollowOffset;
// WorldSpace = 카메라가 월드 공간에서 타겟의 위치만 따라가고 회전은 영향을 받지 않습니다.
transposer.m_BindingMode = CinemachineTransposer.BindingMode.WorldSpace;
composer = virtualCamera.AddCinemachineComponent<CinemachineComposer>();
composer.m_TrackedObjectOffset = trackedObjectOffset;
// 밑에 값을 모두 0으로 설정하면 캐릭터가 떨리는 걸 방지할 수 있다.
transposer.m_XDamping = 0f;
transposer.m_YDamping = 0f;
transposer.m_ZDamping = 0f;
composer.m_DeadZoneWidth = 0f;
composer.m_DeadZoneHeight = 0f;
composer.m_HorizontalDamping = 0f;
composer.m_VerticalDamping = 0f;
}
public void MouseRightPressed(bool isPressed)
{
canRotate = isPressed;
Cursor.lockState = isPressed ? CursorLockMode.Locked : CursorLockMode.None;
}
public void Zoom(float delta)
{
if (GameManager.Instance.IsBuildMode) return;
float zoom = transposer.m_FollowOffset.y - (delta * zoomSpeed * Time.deltaTime);
transposer.m_FollowOffset.y = Mathf.Clamp(zoom, zoomMin, zoomMax);
}
public void RotateCamera(Vector2 mouseDelta)
{
if (!canRotate) return;
float horizontalRotation = mouseDelta.x * rotationSpeed * Time.deltaTime;
transposer.m_FollowOffset = Quaternion.AngleAxis(horizontalRotation, Vector3.up) * transposer.m_FollowOffset;
// 카메라가 캐릭터를 계속 바라보도록 설정
virtualCamera.m_LookAt = virtualCamera.Follow;
}
}
LookController.cs를 통해서 캐릭터가 마우스 움직임을 따라다닌다.
정확히는 마우스 포인터가 쏜 Ray가 지면에 닿은 곳을 바라보는 것이다.
// LookController.cs
using UnityEngine;
public class LookController : MonoBehaviour
{
[SerializeField] private LayerMask groundLayer; // 지면 레이어 마스크
private Vector3 mouseDelta;
private bool stopRotation;
private Camera mainCamera;
private float lookSpeed = 5;
private void Awake()
{
mainCamera = Camera.main;
}
private void Update()
{
if (stopRotation) return;
// 화면 좌표를 레이캐스트로 변환
Ray ray = mainCamera.ScreenPointToRay(mouseDelta);
if (Physics.Raycast(ray, out RaycastHit hitInfo, Mathf.Infinity, groundLayer))
{
Vector3 targetPosition = hitInfo.point;
Vector3 direction = targetPosition - transform.position;
direction.y = 0f; // 수직 방향은 무시
if (direction != Vector3.zero)
{
Quaternion targetRotation = Quaternion.LookRotation(direction);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * 10f);
}
}
}
public void Look(Vector2 delta)
{
mouseDelta = delta;
}
public void MouseRightPressed(bool isPressed)
{
stopRotation = isPressed;
}
}
Movement.cs 에서는 캐릭터가 움직이도록 하는데, 움직이는 방식은 카메라가 바라보는 방향으로 캐릭터를 움직이도록 하면서 옆으로 이동중인지 앞으로 이동중인지 뒤로 이동중인지를 판단하며 이동한다.
// MovementController.cs
using Defines;
using UnityEngine;
public class MovementController : MonoBehaviour
{
private Vector3 moveDirection;
private Rigidbody rb;
private CharacterAnimController anim;
private Condition condition;
[Header("이동속도")]
public float defaultMoveSpeed = 3f;
public float acceleration = 0.1f;
public float MoveSpeed => condition == null ? defaultMoveSpeed : condition.CurrentStat.moveSpeed;
[Header("이동 방향에 따른 애니메이션 변경 범위")]
[SerializeField][Range(0, 1)] private float forwardDotRange = 0.5f;
[SerializeField][Range(0, 1)] private float rightDotRange = 0.5f;
[Header("이동 방향에 따른 속도 비율")]
[SerializeField][Range(0, 1)] private float backwardSpeedRatio = 0.5f;
[SerializeField][Range(0, 1)] private float sideSpeedRatio = 0.75f;
private Camera mainCamera;
private void Awake()
{
rb = GetComponent<Rigidbody>();
anim = GetComponent<CharacterAnimController>();
condition = GetComponent<Condition>();
mainCamera = Camera.main;
}
private void Update()
{
GroundCheck();
}
private void FixedUpdate()
{
if (moveDirection == Vector3.zero)
{
rb.velocity = Vector3.zero;
anim.IdleAnim();
}
else
{
ApplyMove(moveDirection);
}
}
private void ApplyMove(Vector3 inputDirection)
{
CharacterMoveStepType type = CharacterMoveStepType.Forward;
Vector3 cameraForward = mainCamera.transform.forward;
cameraForward.y = 0f;
cameraForward.Normalize();
Vector3 cameraRight = mainCamera.transform.right;
cameraRight.y = 0f;
cameraRight.Normalize();
Vector3 direction = cameraForward * inputDirection.z + cameraRight * inputDirection.x;
direction.Normalize();
float forwardDot = Vector3.Dot(transform.forward, direction);
float rightDot = Vector3.Dot(transform.right, direction);
// 화면에서 볼때 캐릭터가 아래로 보면 z축이 -1, 위로 보면 z축이 1, 왼쪽으로 보면 x축이 -1, 오른쪽으로 보면 x축이 1 이다.
// 이동 방향이 위쪽(즉 z 축이 1)이고 캐릭터가 바라보는 방향아 아래쪽일 때, 캐릭터는 뒷걸음질 치는 모션을 취해야하고, 캐릭터가 바라보는 방향으로 이동 할때는 전진하는 모션을 취해야하고, 바라보는 방향에서 좌, 우로 움직일때는 사이드 스텝으로 모션을 취해야한다.
// 양 방향간의 내적으로 캐릭터의 움직임을 정해 애니메이션을 취하도록 한다.
float maxMoveSpeed = MoveSpeed;
if (forwardDot < -forwardDotRange)
{
maxMoveSpeed = maxMoveSpeed * backwardSpeedRatio;
type = CharacterMoveStepType.Backward;
}
else if (rightDot > rightDotRange)
{
maxMoveSpeed = maxMoveSpeed * sideSpeedRatio;
type = CharacterMoveStepType.Right;
}
else if (rightDot < -rightDotRange)
{
maxMoveSpeed = maxMoveSpeed * sideSpeedRatio;
type = CharacterMoveStepType.Left;
}
if (rb.velocity.magnitude < maxMoveSpeed)
{
Vector3 velocity = direction * acceleration;
rb.velocity += velocity;
}
anim.MoveAnim(type);
}
public void Move(Vector3 move)
{
moveDirection = move;
}
}
이런식으로 구현하면 캐릭터를 나름대로 부드럽게 이동할 수 있게 된다.