[TIL] Cinemachine 3인칭, 탑뷰 만들기

Dreamer·2024년 11월 7일

1. 오늘 주제

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

구현 하고자 하는 부분은 요즘 핫한 패스 오브 엑자일2 의 캐릭터 조작 방법과 유사한 방식으로 해 보았다.
거기에 카메라 회전까지 하니 나름 만족스러운 결과물이다.

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

이런식으로 구현하면 캐릭터를 나름대로 부드럽게 이동할 수 있게 된다.

profile
새로운 시작

0개의 댓글