Unity/연습프로젝트

[Unity]연습 프로젝트 - 1.캐릭터의 부드러운 움직임

myjeongjun 2025. 1. 6. 03:29

 

 

이전 프로젝트에서 플레이어의 움직임을 Blend Tree를 통해 부드럽게 움직일수있도록 구현해보았다.

 

#1. 기본 세팅 (Unity 프로젝트)

(gif로 상세하게 설명하고싶은데 나중에 어떻게 하는지 배워서 수정하겠습니다.)대학교 게임개발 프로젝트 수업에서 RPG장르를 만들어 보기로 결정했습니다. 글 작성을 시작한지 일주일이 지난

myjeongjun.tistory.com

 

하지만 글쓴이가 원하는 정도의 퀄리티가 어째서인지 나오지않았고 어떤점이 부족했는지를 분석해보면

 

1.parameter 조절 방식이 비효율적이다. -> 이 방식으로인해 진행방향의 반대방향으로 움직이기위해 키를누르면, (만약 왼쪽 이동중이라고 가정할때 오른쪽 방향키를 누르면) left walk -> idle -> right walk 불필요하게 idle상태를 거쳐가게된다. 그렇게되면 당연히 움직임의 부자연스러움이 보이게된다.

 

2. 처음부터 카메라의 forward방향으로 시점을 고정하는 방식 :

위 링크에서 구현한 움직임은 항상 전방을 주시하고 움직이는 방식인데,보통 이런 움직임은 많은 게임에서 전투상태일때 만 적용되는 방식이고 평소에는 입력받은 방향을 forward로 설정하고 움직인다.

 

이번에는, 평소에는 다크소울,엘든링 처럼 구현하여 평소에는 입력받은 방향을 forward로 삼으며 움직이고,전투 상태일때는 카메라의 forward가 플레이어의 forward가 되는 방식으로 구현해보고자한다.

 

Sebastian Graves  라는 이름의 유튜버가 필자는 원하는 방식의 구현방법을 제시했다.

캐릭터에 관여하는 스크립트는 매우 다양할것이다 Locomotion,animation,Sound등, 이것들을 많아져서 서로 interaction을 시키다보면 의존성이 매우 높아질테니 characterManager를 하나둬서 이 스크립트를 통해 다른 스크립트가 서로 interaction하도록 설계했다.

 

여기서 playerManager, playerLocomotionManager, playerAnimationManager등 characterManager를 상속 받은 playerManager를 통해 플레이어의 움직임을 구현한다. 이렇게 하는 방식은 당연히 나중에 AI구현할때도 재사용을 위해 characterManager를 부모 클래스로두고 AIManager등을 만들기 위해서이다.

 

 

Input 처리

플레어이 모델을 움직이게 하기 위해선 Input을 어떻게 받아오는지에 대해 설명하자면

기존에는 New Input System에서 이미 만들어진 Action Maps인 Player의 Move를 사용해서 input을 받아왔는데,

 

직접 커스텀하는 방법으로 만들어 보겠다.

우선 Action Maps 옆에있는 + 버튼을눌러 새로운 Map을추가해주고 PlayerMoveMent를 넣어줬다.

Actions에 MoveMent라는 Action을 만들어주고 Action Type을 value로 바꿔준다. control type은 vector2이다.

 

이제 binding할 녀석을 Add up down left right composite를 선택 후 composite type을 2D vector, Mode를 Digital Nomalized를 해준다. 만약 자신이 조이스틱으로 구현하고싶으면 여기서 얼마든지 만들어 쓸 수있다.

아니면 기존에 만들어진 Player Map에가서 Move를 써도 같은 방식으로 구현이 되어있으니 그냥 그것을 사용해도 좋다.

 

이제 이 Input Action set을 스크립트에서 사용하기 위해서는 

 

Generate C# class를해주면 가능하다.

 

player의 Input은 player에서 받는게아닌 platerInputManager를 통해 받는다. 

이것은 게임도중 Input을 받으면 안되는 시점이 존재할수도있으니(컷신,메인화면 등) 따로 비활성화 시키기만하면 조작을 불가능하게 만들수있는 방식이다.

아래 스크립트를 보면 InputManager의 OnEnable함수 즉, 오브젝트가 활성화될때 실행되는 부분이다.

performed += i => movementInput = i .ReadValue<vector2>();는 이벤트 등록으로,  내부적으로는 .Move.performed 이벤트가 발생하면 Unity Input System이 자동으로 InputAction.CallbackContext 객체를 생성한다. 람다 함수를 통해 i에 그 객체를 넘겨준다음 moveMentInput에 i.ReadValue<vector2>()라는 말은 객체는 vector2라는 정보를 가지고있으니 그대로 넘겨준다는 말이다.

 

아래의 cancel은 만약 input이 들어온후 키를 떼면 값이 마지막에 들어온값으로 유지되기 때문에 입력이 들어오지 않은경우는 0,0으로 만들어주는 용도이다.(예를들어 왼쪽 방향키를 누르다가 떼면 입력이 0 , -1인 상태에서 끝나버려 플레이가 계속 왼쪽으로 움직여버리는데  cancel을 통해 입력이 없으면 0 ,0으로 만들어 주므로 멈추게 할 수있다.)

 

    private void HandlePlayerMoveMentInput() 
    {
        verticalnput = movementInput.y;
        horizontalnput = movementInput.x;

        //절대적인 움직임의 크기
        moveAmount = Mathf.Clamp01(Mathf.Abs(verticalnput) + Mathf.Abs(horizontalnput));

        //조이스틱은 세밀한 조작이 가능하기때문에 아래의 if문을 만족하지만 키보드의 경우 1,-1,0중에 하나여서 아래의 if문을 통과하지않음
        if (moveAmount <= 0.5 && moveAmount > 0)
        {
            moveAmount = 0.5f;
        }
        else if (moveAmount > 0.5 && moveAmount <= 1)
        {
            moveAmount = 1f;
        }

        //이거 안해주면 클라이언트로 연결할때 널 참조일어남
        if (player == null)
            return;

        //평상시
        player.playerAnimatorManager.UpdateAnimatorMovement(0, moveAmount,player.playerNetWorkManager.isSprinting.Value);

        //락온시

    }

받아온  input을 적절히 처리해서 넘겨주는데 이때 맨마지막의  player.playerAnimatorManager.UpdateAnimatorMovement(0, moveAmount,player.playerNetWorkManager.isSprinting.Value);을 추적해보자면

 

    public void UpdateAnimatorMovement(float horizontalValue, float verticalValue,bool isSprinting) 
    {
        float horizontalAmount = horizontalValue;
        float verticalAmount = verticalValue;

        if (isSprinting) 
        {
            verticalAmount = 2;
        }

        character.animator.SetFloat(horizontal, horizontalAmount, 0.1f, Time.deltaTime);
        character.animator.SetFloat(vertical, verticalAmount, 0.1f, Time.deltaTime);

    }

애니메이터의 parameter를 천천히 올리고 내리도록 dampTime을 설정해놓았다.

 

Blend Tree방식도 저번과는큰 차이가 없다.

움직임 처리(Character Controller사용)

    private void HandleGroundedMovement() 
    {
        GetMovementValues();

        if (!player.canMove)
            return;
        moveDirection = PlayerCamera.instance.transform.forward * verticalMovement;
        moveDirection = moveDirection + PlayerCamera.instance.transform.right * horizontalMovement;
        moveDirection.Normalize();
        moveDirection.y = 0;

        if (player.playerNetWorkManager.isSprinting.Value) 
        {
            player.characterController.Move(moveDirection * runningSpeed * Time.deltaTime);
        }
        else 
        {
            player.characterController.Move(moveDirection * walkingSpeed * Time.deltaTime);
                //WALKING SPEED
            
        }

    }
    private void GetMovementValues()
    {
        verticalMovement = PlayerInputManager.Instance.verticalnput;
        horizontalMovement = PlayerInputManager.Instance.horizontalnput;
        moveAmount = PlayerInputManager.Instance.moveAmount;
    }

 

charactorController를 통해 캐릭터를 움직이기 때문에 Move함수로 방향 * 속도 * Time.deltaTime을 사용한다.

 

회전처리 함수

마찬가지로 slerp시키면 부드럽게 회전하는것을 구현할 수있다.

  void HandleRotation() 
  {
      if(!player.canRotate) return;

      targetRotationDirection = Vector3.zero;

      targetRotationDirection = PlayerCamera.instance.cameraobject.transform.forward * verticalMovement;
      targetRotationDirection = targetRotationDirection + PlayerCamera.instance.cameraobject.transform.right * horizontalMovement;
      targetRotationDirection.Normalize();
      targetRotationDirection.y = 0;

      if (targetRotationDirection == Vector3.zero) 
      {
          targetRotationDirection = transform.forward;
      }

      Quaternion newRotation = Quaternion.LookRotation(targetRotationDirection);
      Quaternion targetRotation = Quaternion.Slerp(transform.rotation,newRotation, rotationSpeed * Time.deltaTime); 
      transform.rotation = targetRotation;
  }

 

결과물

 

움직임 구현을 작성하면서 카메라 구현방식도 함께 서술해야했는데 글이 너무 길어지니 나중에 글을 작성하겠습니다.