본문 바로가기
스파르타 내배캠

스파르타 내배캠 Unity 3기 - 46

by LemongO 2024. 3. 7.

 

시작해 볼까?

 

 

 


최종 프로젝트 2일 차 그리고 Player 로직

 

 

 

이번 최종 프로젝트에서 맡게 된 파트 중 메인급은 Player 로직 부분이다.

주황 표시 되어있는 모듈화가 튜터님께서 잘 다듬으면 좋은 포트폴리오 내용으로 가져갈 수 있다고 하셨다.

 

하지만 모듈화를 하기 전 가장 먼저 작업해야 하는 부분이 있으니 바로 플레이어의 이동과 공격이다.


그리고 이동과 공격을 생각하면서 고려해야 할 사항이 몇 개 있었다.

 

RigidBody? CharacterController? 어느 것으로 움직임을 구현할까였는데

 

우리 게임의 특징에 가장 적합한 것을 골라야 했다.

 

대표적인 컴포넌트별 차이점은

 

RigidBody

  • 오브젝트 끼리의 상호작용이 가능하다. (서로가 서로에게 물리적 힘을 가할 수 있다.)
  • RigidBody 컴포넌트를 가진 오브젝트의 Rotation 변경 시 콜라이더 역시 따라 움직인다.
  • 중력계산을 따로 하지 않아도 알아서 된다. 다만 Ground체크는 별도로 해줘야 한다.

CharacterController

  • 오브젝트 끼리의 상호작용이 불가능하다. (콜라이더 충돌은 가능하나 밀치거나 할 수 없다.)
  • Rotation 변경 시에도 CharacterController의 Collider는 항상 제자리이다.
  • 중력계산을 따로 해줘야 y축 하향이동이 가능하다.

 

일단 RigidBody 자체만 보면 분명 섬세한 조정이 가능하다. 뭔가 다채로운 것을 만들고자 한다면

예를 들어 경사면에 따라 캐릭터가 같이 기울어야 한다던가 폴가이즈 느낌의 물리적 상호작용이 있어야 한다던가

등의 작업이 필요하다면 RigidBody 사용이 필수가 되겠지만, 우리 게임에 그런 작용은 존재하지 않는다.

 

그렇기 때문에 사용하기 간편하지만 기능은 게임 기획에 충분한 CharacterController를 채택해서 만들기로 했다.

 

 

 

이동에 필요한 컴포넌트는 골랐고, 이제 입력값을 받아야 하니 그 부분은 강의에서도 배웠고 쓰다 보니 편리했던 

InputSystem을 사용, 매핑하고 입력값을 받기 위해 제공되는 Behavior 방식 중 

 

코드로 처리하기 편한 'Invoke C Sharp Events'를 사용하기로 했다.

UnityEvent 방식은 드래그드롭으로 하는 걸 좀 싫어하는 나로선 기피대상이다. 그 외의 두 개는 성능상의 문제로 기각.

 

Camera는 그냥 눈에 띄어서 넣어줬고 별다른 의미는 없다.

 

이 정도쯤 되면 심화강의에서 알려준 대로 FSM까지 쭉쭉하면 될 것 같지만 그건 코드를 베낀 거나 마찬가지.

 

게다가 나는 CharacterController에 익숙하지 않았고

실제 플레이어는 상체, 하체로 분리되어 움직이기 때문에

곧바로 FSM을 만들기보다 (애초에 담당하시는 팀원분이 따로 계신다.)

일단 컨트롤러 내부에서 이동처리를 만들고 그다음을 하자는 마인드로 시작했다.

 

 

public class PlayerController : MonoBehaviour
{
    [Header("# Test")]
    [Range(2f, 10f)][SerializeField] float _movementSpeed;
    [Range(5f, 15f)][SerializeField] float _smoothRotateValue;
    [Range(0.1f, 0.5f)][SerializeField] float _minDownForceValue; // 접지 중일 때 최소 중력배율
    [Range(1f, 100f)][SerializeField] float _jumpPower;

    [field: Header("# Animation")]
    [field: SerializeField] public PlayerAnimationData AnimationData { get; private set; }

    public PlayerInput Input { get; private set; }
    public CharacterController Controller { get; private set; }
    public Animator Anim { get; private set; }

    private Vector2 _currentMovementInput;
    private Vector3 _currentMovementDirection;
    private bool _isMoveInputPressed;
    private bool _isJumpInputPressed;

    private float _initialGravity;
    private bool _isJumping = false;

    private void Awake()
    {
        AnimationData.Init();

        Input = GetComponent<PlayerInput>();
        Controller = GetComponent<CharacterController>();
        Anim = GetComponentInChildren<Animator>();

        AddInputCallBacks();

        _initialGravity = Physics.gravity.y * 2;
    }

    private void Update()
    {
        HandleRotation();
        HandleAnimation();
        Move(); // Q. 물리적 이동은 FixedUpdate로 쓰이는 줄 알았는데 왜 Update에서 쓰는 걸까?
        HandleGravity();
        HandleJump();
    }

    private void AddInputCallBacks()
    {
        Input.Actions.Move.started += OnMovementInput;
        Input.Actions.Move.performed += OnMovementInput;
        Input.Actions.Move.canceled += OnMovementInput;
        Input.Actions.Jump.started += OnJump;
        Input.Actions.Jump.canceled += OnJump;
    }

    // Temp - 실제 이동에 쓰일 함수
    private void Move()
    {
        Controller.Move(_currentMovementDirection * _movementSpeed * Time.deltaTime);
    }

    // Temp - InputAction 에 콜백 함수로 등록하여 입력값 받아옴. (이동관련)
    private void OnMovementInput(InputAction.CallbackContext context)
    {
        _currentMovementInput = context.ReadValue<Vector2>();
        _currentMovementDirection.x = _currentMovementInput.x;
        _currentMovementDirection.z = _currentMovementInput.y;
        _isMoveInputPressed = _currentMovementInput.x != 0 || _currentMovementInput.y != 0;
    }

    // Temp - InputAction 에 콜백 함수로 등록하여 입력값 받아옴. (점프관련)
    private void OnJump(InputAction.CallbackContext context)
    {
        _isJumpInputPressed = context.ReadValueAsButton();
    }

    // Temp - 점프
    private void HandleJump()
    {
        if (_isJumpInputPressed && Controller.isGrounded && !_isJumping)
        {
            _isJumping = true;
            _currentMovementDirection.y = _jumpPower;
        }
        else if (!_isJumpInputPressed && !Controller.isGrounded && _isJumping)
            _isJumping = false;
    }

    // Temp - CharacterController 컴포넌트는 중력에 대해 연산 안 하기 때문에 직접 조정 해줘야 함.
    private void HandleGravity()
    {
        if (Controller.isGrounded)
        {
            _currentMovementDirection.y = -_minDownForceValue;
        }
        else
        {
            _currentMovementDirection.y += _initialGravity * Time.deltaTime;
        }
    }

    // Temp - 회전 제어
    private void HandleRotation()
    {
        Vector3 positionToLookAt;

        positionToLookAt.x = _currentMovementDirection.x;
        positionToLookAt.y = 0f;
        positionToLookAt.z = _currentMovementDirection.z;

        Quaternion currentRotation = transform.rotation;

        if (_isMoveInputPressed)
        {
            Quaternion targetRotation = Quaternion.LookRotation(positionToLookAt);
            transform.rotation = Quaternion.Slerp(currentRotation, targetRotation, _smoothRotateValue * Time.deltaTime);
        }
    }

    // Temp - 애니메이션 제어 : 현재 Walk만 가능
    private void HandleAnimation()
    {
        bool isWalking = Anim.GetBool(AnimationData.WalkParameterHash);

        if (_isMoveInputPressed && !isWalking)
        {
            Anim.SetBool(AnimationData.WalkParameterHash, true);
        }
        else if (!_isMoveInputPressed && isWalking)
        {
            Anim.SetBool(AnimationData.WalkParameterHash, false);
        }
    }

 

현재까지 구현된 부분은 Update 문에서 이동 / 회전 / 점프 / 중력 / 애니메이션을 담당한다.

 

 

C# Class로 만든 InputActions를 생성하고 PlayerController 클래스로 가져와 실제 이동에 쓰일 콜백함수들을 등록해 주고

Input.Actions.Move.started += OnMovementInput;
Input.Actions.Move.performed += OnMovementInput;
Input.Actions.Move.canceled += OnMovementInput;

 

입력 값인 ReadValue <Vector2>의 x, y 값에 따라 현재 이동방향과 실제 이동, 회전, Walk Animation 실행 및 종료를
처리해 준다.

// Temp - 실제 이동에 쓰일 함수
    private void Move()
    {
        Controller.Move(_currentMovementDirection * _movementSpeed * Time.deltaTime);
    }

// Temp - InputAction 에 콜백 함수로 등록하여 입력값 받아옴. (이동관련)
    private void OnMovementInput(InputAction.CallbackContext context)
    {
        _currentMovementInput = context.ReadValue<Vector2>();
        _currentMovementDirection.x = _currentMovementInput.x;
        _currentMovementDirection.z = _currentMovementInput.y;
        _isMoveInputPressed = _currentMovementInput.x != 0 || _currentMovementInput.y != 0;
    }

    // Temp - 회전 제어
    private void HandleRotation()
    {
        Vector3 positionToLookAt;

        positionToLookAt.x = _currentMovementDirection.x;
        positionToLookAt.y = 0f;
        positionToLookAt.z = _currentMovementDirection.z;

        Quaternion currentRotation = transform.rotation;

        if (_isMoveInputPressed)
        {
            Quaternion targetRotation = Quaternion.LookRotation(positionToLookAt);
            transform.rotation = Quaternion.Slerp(currentRotation, targetRotation, _smoothRotateValue * Time.deltaTime);
        }
    }

    // Temp - 애니메이션 제어 : 현재 Walk만 가능
    private void HandleAnimation()
    {
        bool isWalking = Anim.GetBool(AnimationData.WalkParameterHash);

        if (_isMoveInputPressed && !isWalking)
        {
            Anim.SetBool(AnimationData.WalkParameterHash, true);
        }
        else if (!_isMoveInputPressed && isWalking)
        {
            Anim.SetBool(AnimationData.WalkParameterHash, false);
        }
    }

 

 

 

점프는 ReadValueAsButton() 함수로 바인드 된 버튼을 Started Canceled 체크하여 점프를 처리해 준다.

 // Temp - InputAction 에 콜백 함수로 등록하여 입력값 받아옴. (점프관련)
    private void OnJump(InputAction.CallbackContext context)
    {
        _isJumpInputPressed = context.ReadValueAsButton();
    }

    // Temp - 점프
    private void HandleJump()
    {
        if (_isJumpInputPressed && Controller.isGrounded && !_isJumping)
        {
            _isJumping = true;
            _currentMovementDirection.y = _jumpPower;
        }
        else if (!_isJumpInputPressed && !Controller.isGrounded && _isJumping)
            _isJumping = false;
    }

 

 

중력 부분은 현재 땅에 닿은 상태이면 하향 최솟값을 따로 정해 계속해서 y축 아래 방향으로 속도를 주고

땅에 닿지 않았다면 공중에 있기 때문에 중력값 * 배율을 계속해서 += 하는 방식으로 가속을 준다.

// Temp - CharacterController 컴포넌트는 중력에 대해 연산 안 하기 때문에 직접 조정 해줘야 함.
    private void HandleGravity()
    {
        if (Controller.isGrounded)
        {
            _currentMovementDirection.y = -_minDownForceValue;
        }
        else
        {
            _currentMovementDirection.y += _initialGravity * Time.deltaTime;
        }
    }

 

 

그리고 이 모든 걸 Update에서 처리를 하는데...

 private void Update()
    {
        HandleRotation();
        HandleAnimation();
        Move(); // Q. 물리적 이동은 FixedUpdate로 쓰이는 줄 알았는데 왜 Update에서 쓰는 걸까?
        HandleGravity();
        HandleJump();
    }

 

모든 것을 다 Update로 처리하기에는 너무 낭비인 부분이 많이 보인다.

일단 이동과 점프를 처리했으니 내일 FSM을 이용해서 상태별 처리를 하도록 하자.

 

 

시작은 보잘 것 없지만 그 끝은...[더보기]