본문 바로가기
Development/C#

멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 89일차

by Mobics 2025. 4. 4.

 

목차


    게임에 필요한 상식

    25.04.04

    Character Animation

    >> Hierarchy에 Ellen Prefab 추가

     

    >> PlayerController.cs 생성

    : Ellen에 추가

    --> Input Manager를 통해 입력값 받는 방식으로 구현

     

    >> Ellen에 'Character Controller' Component 추가

    : 캐릭터를 이동시킬 뿐만 아니라 경사로나 계단도 오르도록 도와준다.

    • Slope Limit : 오를 수 있는 경사로 각도
    • Skin Width : 충돌감지영역 크기
    • Min Move Distance : 움직임의 최소 거리
    • Step Offset : 오를 수 있는 계단의 높이

    --> Character에 맞게 Center 조정

     

    ※ Layer는 32Bit 비트 마스크를 사용하여 Layer를 구분한다.

     

    >> 이동 테스트 할 때, 캐릭터가 살짝 뜨는 문제 해결

    : Character Controller의 Collider 부분이 땅과 닿아서 생기는 문제 --> Skin Width 값을 줄여서 해결

     

    >> Animation Controller 생성

    : 이름은 'Ellen', 이후 Hierarchy에 있는 Ellen에 바인딩

     

    ※ Animation을 미리 보고 다운 받을 수 있는 사이트

    https://www.mixamo.com/#/?page=1&type=Character

     

    Mixamo

     

    www.mixamo.com

     

    >> Animator

    : 'Ellen' Animation Controller

    --> EllenIdle은 Idle로 이름을 변경

     

    >> Walk 추가

     

    >> Create State Empty로 'Jump', 'Attack', 'Hit' 추가한 뒤, Has Exit Time 체크 해제

    : Walk -> Idle도 Has Exit Time 체크 해제

    --> Jump, Attack, Hit은 나중에 구현하려고 임시로 만든 것

     

    >> Parameters에서 Bool 타입으로 'Move' 생성 후 바인딩

    : Walk -> Idle은 반대로 Move가 False일 때로 설정

     

    >> Apply Root Motion 해제

     

    >> Jump 구현

    : (개인적으로 구현하도록)

     

    >> Run 구현

    : Blend Tree 활용 --> 기존의 Walk는 삭제

     

    1. Blend Tree 생성

     

    2. Blend Tree 내부로 들어가서 Motion Field 생성

    : 2개 생성

     

    3. Ellen의 Walk와 Run 바인딩 및 Parameter 수정

     

    4. Blend Tree의 이름을 Move로 변경하고 Conditions 추가 및 Has Exit Time 체크 해제

    : Move -> Idle도 마찬가지로 Has Exit Time 체크 해제하고 Condition의 Move를 false로 설정

     

    >> Shift를 눌렀을 때, 서서히 속도가 증가해서 Animation을 자연스럽게 만들기

    : 개인적으로 구현하도록

     

    └ Apply Root Motion을 Script에서 조절하여 구현

    ※ Update Mode

    : Animation의 계산을 언제 시행할지 결정하는 것

    • Normal : Update()와 같은 횟수로 호출 --> 타임 스케일의 영향을 받는다.
    • Animate Physics : Fixed Update()와 같은 횟수로 호출 --> Unity가 다루는 물리 계산과 Animation을 동기화할 수 있다.
    • Unscaled Time : Update()와 같은 횟수로 호출되지만 타임스케일의 영향을 받지 않는다 --> 게임이 슬로우 모션 중일 때에도 정상적으로 Animation 시키고자 할 때 도움이 된다.

     

    >> Jump 구현

    : Sub-State Machine을 활용하여 구현

     

    1. Sub-State Machine 생성

    : 이름은 'Jump'

     

    2. Jump 내부로 들어가서 Blend Tree 생성 및 'EllenIdleLand' 추가 후, 'Make Transition'

    : Blend Tree의 이름은 'OnJump'

     

    3. Base Layer에서 Idle과 Jump 연결

    : StateMachine - Jump로 연결

     

    ※ 아직 구현이 완료되지 않은 상태로 수업이 마무리 됨

     

    Camera

    >> 구면 좌표계

    • r: 원점에서 점까지의 거리 (반지름)
    • θ: 방위각 (azimuthal angle, x축 기준 회전각)
    • φ: 고도각 (polar angle, y축 방향으로 올라가는 각)
    • b: xz 평면에 투영된 거리 (r의 수평 성분)

     

    >> CameraController.cs 생성

    : 이후 Main Camera에 추가

     

    >> Ellen의 자식으로 빈 게임 오브젝트 'Camera Target' 생성

    : 이후 CameraController.cs에 바인딩

    --> 그냥 Ellen을 Camera의 Target으로 넣으면 Pivot이 발에 있기 때문에 발을 기준으로 Camera가 움직이게 되므로 이를 방지하기 위해서

     

    최종 코드

    >> PlayerController.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    [RequireComponent(typeof(CharacterController))]
    [RequireComponent(typeof(Animator))]
    public class PlayerController : MonoBehaviour
    {
        private static readonly int Move = Animator.StringToHash("Move");
        private static readonly int Speed = Animator.StringToHash("Speed");
    
        [SerializeField] private float rotateSpeed = 100f;
        [SerializeField] private float jumpForce = 5f;
        
        private CharacterController _characterController;
        private Animator _animator;
        
        private float _gravity = -9.81f;
        private Vector3 _velocity;
        private float _groundDistance;
        
        // Root Motion On
        private float _groundedMinDistance = 0.1f;
        private float _speed = 0f;
    
        private bool IsGrounded
        {
            get
            {
                var distance = GetDistanceToGround();
                return distance < _groundedMinDistance;
            }
        }
        
        private void Awake()
        {
            _characterController = GetComponent<CharacterController>();
            _animator = GetComponent<Animator>();
        }
    
        private void Start()
        {
            // 커서 설정
            Cursor.visible = false;
            Cursor.lockState = CursorLockMode.Locked;
        }
    
        private void Update()
        {
            // 커서 Rock 해제
            if (Input.GetKeyDown(KeyCode.Escape))
            {
                Cursor.visible = true;
                Cursor.lockState = CursorLockMode.None;
            }
            
            HandleMovement();
            // ApplyGravity();  // Root Motion Off
            CheckRun();
        }
    
        // 사용자 입력 처리 함수
        private void HandleMovement()
        {
            float horizontal = Input.GetAxis("Horizontal");
            float vertical = Input.GetAxis("Vertical");
    
            if (vertical > 0)
            {
                _animator.SetBool(Move, true);
            }
            else
            {
                _animator.SetBool(Move, false);
            }
            _animator.SetFloat(Speed, _speed);
    
            #region Root Motion Off
    
            // // 달리기
            // float speed = 0;
            // if (Input.GetKey(KeyCode.LeftShift))
            // {
            //     speed = 1;
            // }
            //
            // _animator.SetFloat("Speed", speed);
    
            #endregion
    
            Vector3 movement = transform.forward * vertical;
            transform.Rotate(0, horizontal * rotateSpeed * Time.deltaTime, 0);
            
            #region Root Motion Off
    
            // _characterController.Move(movement * Time.deltaTime);
    
            // _groundDistance = GetDistanceToGround();
    
            // if (Input.GetButtonDown("Jump"))
            // {
            //     _velocity.y = Mathf.Sqrt(jumpForce * -2f * _gravity);
            // }
    
            #endregion
        }
    
        #region Root Motion Off
    
        // 중력 적용 함수
        private void ApplyGravity()
        {
            _velocity.y += _gravity * Time.deltaTime;
            _characterController.Move(_velocity * Time.deltaTime);
        }
    
        #endregion
        
        // 달리기 처리
        private void CheckRun()
        {
            if (Input.GetKey(KeyCode.LeftShift))
            {
                _speed += Time.deltaTime;
                _speed = Mathf.Clamp01(_speed); //Mathf.Clamp(_speed, 0f, 1f);와 동일
            }
            else
            {
                _speed -= Time.deltaTime;
                _speed = Mathf.Clamp01(_speed);
            }
        }
        
        // 바닥과 거리를 계산하는 함수
        private float GetDistanceToGround()
        {
            float maxDistance = 10f;
            if (Physics.Raycast(transform.position, Vector3.down, out RaycastHit hit, maxDistance))
            {
                return hit.distance;
            }
            else
            {
                return maxDistance;
            }
        }
    
        #region Animator Method
    
        private void OnAnimatorMove()
        {
            Vector3 movePosition;
    
            movePosition = _animator.deltaPosition;
            
            // 중력 적용
            _velocity.y += _gravity * Time.deltaTime;
            movePosition.y = _velocity.y;
            
            _characterController.Move(movePosition);
        }
    
        #endregion
    
        private void OnDrawGizmos()
        {
            Gizmos.color = Color.red;
            Gizmos.DrawLine(transform.position, transform.position + Vector3.down * _groundDistance);
        }
    }

     

    >> CameraController.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    [RequireComponent(typeof(Camera))]
    public class CameraController : MonoBehaviour
    {
        [SerializeField] private Transform target;
        [SerializeField] private float rotationSpeed = 1000f;
        [SerializeField] private float distance = 5f;   // target과 camera의 거리
    
        private float _azimuthAngel;
        private float _polarAngle = 45f;
    
        private void Start()
        {
            var cartesianPosition = GetCameraPosition(distance, _polarAngle, _azimuthAngel);
            var cameraPosition = target.position - cartesianPosition;
    
            transform.position = cameraPosition;
            transform.LookAt(target);
        }
    
        private void LateUpdate()
        {
            float mouseX = Input.GetAxis("Mouse X");
            float mouseY = Input.GetAxis("Mouse Y");
            
            Debug.Log($"## X : {mouseX} Y : {mouseY}");
            
            _azimuthAngel += mouseX * rotationSpeed * Time.deltaTime;
            _polarAngle -= mouseY * rotationSpeed * Time.deltaTime;
    
            _polarAngle = Mathf.Clamp(_polarAngle, 10f, 45f);
    
            var cartesianPosition = GetCameraPosition(distance, _polarAngle, _azimuthAngel);
            var cameraPosition = target.position - cartesianPosition;
    
            transform.position = cameraPosition;
            transform.LookAt(target);
        }
    
        Vector3 GetCameraPosition(float r, float polarAngle, float azimuthAngle)
        {
            float b = r * Mathf.Cos(polarAngle * Mathf.Deg2Rad);
            float z = b * Mathf.Cos(azimuthAngle * Mathf.Deg2Rad);
            float y = r * Mathf.Sin(polarAngle * Mathf.Deg2Rad) * -1;
            float x = b * Mathf.Sin(azimuthAngle * Mathf.Deg2Rad);
    
            return new Vector3(x, y, z);
        }
    }