본문 바로가기
개발/Unity 내일배움캠프 TIL

Unity3D FSM으로 플레이어 캐릭터 조작 구현하기 (2)

by 석시 2023. 10. 6.



저번 글에서는 Input Action을 스크립트로 접근하는 법과 Player Animation을 FSM으로 구현하기 위한 기본 세팅을 마쳤었다.

Unity3D FSM으로 플레이어 캐릭터 조작 구현하기 (1)
프로젝트 세팅Input System으로 기본적인 이동 구현하기Input Actions와 오브젝트 연결하기플레이어에 애니메이션을 받도록 하는 스크립트 작성FSM 구성하기플레이어에 대한 상태 정보 구현하기 플레이어 이동 구현 방식에는 여러 가지가 있지만, 구현해야 하는 이동 상황이 많아지면 주로 사용하는 것이 바로 FSM(Finite State Machine)이다. FSM은 상태 패턴을 구현하는 방법 중 하나로 자세한 내용은 나중에 게시글로 따로 다루도록 하겠다. 이번 글에서는 FSM을 이용하여 플레이어 이동 구현하는 방식을 정리해보고자 한다. 프로젝트 세팅 사용된 애셋은 다음과 같다.ProjectAssets.unitypackage 프로젝트 패키지는 Input System을 사용할 것이기 때문에 Packag..
https://seoksii.tistory.com/56

이제 본격적으로 State Machine을 만들어보자.


State Machine 만들기

제일 먼저 전체적인 State를 관리해줄 기능을 위해 PlayerStateMachine.cs 스크립트를 작성하고, 모든 State들의 형태가 비슷하기 때문에 상속을 받아 만들 기본 State인 PlayerBaseState.cs를 작성하도록 하겠다.

  • PlayerStateMachine.cs
    using UnityEngine;
    
    public class PlayerStateMachine : StateMachine
    {
        public Player Player { get; }
    
        // States
        public PlayerIdleState IdleState { get; }
    
        // 
        public Vector2 MovementInput { get; set; }
        public float MovementSpeed { get; private set; }
        public float RotationDamping { get; private set; }
        public float MovementSpeedModifier { get; set; } = 1f;
    
        public float JumpForce { get; set; }
    
        public Transform MainCameraTransform { get; set; }
    
        public PlayerStateMachine(Player player)
        {
            this.Player = player;
    
            IdleState = new PlayerIdleState(this);
    
            MainCameraTransform = Camera.main.transform;
    
            MovementSpeed = player.Data.GroundedData.BaseSpeed;
            RotationDamping = player.Data.GroundedData.BaseRotationDamping;
        }
    }

    해당 코드에는 이동이 이루어질 때 필요한 데이터들이 들어가게 되고, State Machine을 이루기 위한 정보와 기능들이 들어가 있다.

Player.cs에도 PlayerStateMachine을 필드와 상태 업데이트 부분을 추가해주자.

  • Player.cs
    public class Player : MonoBehaviour
    {
        private PlayerStateMachine stateMachine;
        
        private void Awake()
        {
            stateMachine = new PlayerStateMachine(this);
        }
    
        private void Start()
        {
            Cursor.lockState = CursorLockMode.Locked;
            stateMachine.ChangeState(stateMachine.IdleState);
        }
    
        private void Update()
        {
            stateMachine.HandleInput();
            stateMachine.Update();
        }
    
        private void FixedUpdate()
        {
            stateMachine.PhysicsUpdate();
        }
    }

  • PlayerBaseState.cs
    using UnityEngine;
    
    public class PlayerBaseState : IState
    {
        protected PlayerStateMachine stateMachine;
        protected readonly PlayerGroundData groundData;
    
        public PlayerBaseState(PlayerStateMachine playerStateMachine)
        {
            stateMachine = playerStateMachine;
            groundData = stateMachine.Player.Data.GroundedData;
        }
    
        public virtual void Enter()
        {
            AddInputActionsCallbacks();
        }
    
        public virtual void Exit()
        {
            RemoveInputActionsCallbacks();
        }
    
        public virtual void HandleInput()
        {
            ReadMovementInput();
        }
    
        public virtual void PhysicsUpdate()
        {
    
        }
    
        public virtual void Update()
        {
            Move();
        }
    
        protected virtual void AddInputActionsCallbacks()
        {
            
        }
    
        protected virtual void RemoveInputActionsCallbacks()
        {
            
        }
        
        private void ReadMovementInput()
        {
            stateMachine.MovementInput = stateMachine.Player.Input.PlayerActions.Movement.ReadValue<Vector2>();
        }
    
        private void Move()
        {
            Vector3 movementDirection = GetMovementDirection();
    
            Rotate(movementDirection);
    
            Move(movementDirection);
        }
    
        private Vector3 GetMovementDirection()
        {
            Vector3 forward = stateMachine.MainCameraTransform.forward;
            Vector3 right = stateMachine.MainCameraTransform.right;
    
            forward.y = 0;
            right.y = 0;
    
            forward.Normalize();
            right.Normalize();
    
            return forward * stateMachine.MovementInput.y + right * stateMachine.MovementInput.x; 
        }
    
        private void Move(Vector3 movementDirection)
        {
            float movementSpeed = GetMovemenetSpeed();
            stateMachine.Player.Controller.Move(
                (movementDirection * movementSpeed) * Time.deltaTime
                );
        }
    
        private void Rotate(Vector3 movementDirection)
        {
            if(movementDirection != Vector3.zero)
            {
                Transform playerTransform = stateMachine.Player.transform;
                Quaternion targetRotation = Quaternion.LookRotation(movementDirection);
                playerTransform.rotation = Quaternion.Slerp(playerTransform.rotation, targetRotation, stateMachine.RotationDamping * Time.deltaTime);
            }
        }
    
        private float GetMovemenetSpeed()
        {
            float movementSpeed = stateMachine.MovementSpeed * stateMachine.MovementSpeedModifier;
            return movementSpeed;
        }
    
        protected void StartAnimation(int animationHash)
        {
            stateMachine.Player.Animator.SetBool(animationHash, true);
        }
    
        protected void StopAnimation(int animationHash)
        {
            stateMachine.Player.Animator.SetBool(animationHash, false);
        }
    
    
    }

기본적으로 모든 State는 State Machine과 역참조를 할 것이다.

생성된 State들이 속한 StateMachine을 항상 참조하고 있다는 뜻이다.

나머지 virtual로 되어 있는 부분들은 이제 각각의 State를 만들 때 구현해야하는 공통적인 기능들이다.

PlayerBaseState를 상속받는 각각의 State들을 만들어보자.


땅에 붙어 있을 때 상태 구현하기

먼저 플레이어가 땅에 닿아있는 상태인 PlayerGroundedState이다.

  • PlayerGroundedState.cs
    public class PlayerGroundedState : PlayerBaseState
    {
        public PlayerGroundedState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
        {
        }
    
        public override void Enter()
        {
            base.Enter();
            StartAnimation(stateMachine.Player.AnimationData.GroundParameterHash);
        }
    
        public override void Exit()
        {
            base.Exit();
            StopAnimation(stateMachine.Player.AnimationData.GroundParameterHash);
        }
    
        public override void Update()
        {
            base.Update();
        }
    
        public override void PhysicsUpdate()
        {
            base.PhysicsUpdate();
        }
    
    }

이제 땅에 있을 때 가장 기본 상태인 PlayerIdleState를 작성하자.

  • PlayerIdleState.cs
    public class PlayerIdleState : PlayerGroundedState
    {
        public PlayerIdleState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
        {
        }
    
        public override void Enter()
        {
            stateMachine.MovementSpeedModifier = 0f;
            base.Enter();
            StartAnimation(stateMachine.Player.AnimationData.IdleParameterHash);
        }
    
        public override void Exit()
        {
            base.Exit();
            StopAnimation(stateMachine.Player.AnimationData.IdleParameterHash);
        }
    
        public override void Update()
        {
            base.Update();
        }
    }

PlayerSO 만들기

이제 Player에 정보를 넣어주자.

이전 게시글에서 만들었던 PlayerSO 스크립트를 기반으로 스크립터블 오브젝트를 하나 생성해서 다음과 같이 값을 설정해주자.

Player 오브젝트 설정

만들어놓은 Player Object에 Player Input 스크립트와 Character Controller를 추가해주자.

주의할 것은 Player Input이 두 개 뜰 것인데, Input Actions를 넣어주는 것이 아니라 우리가 스크립트화 시켜준 것을 넣어야한다.

Character Controller라는 것이 뭐냐면 보통 Rigidbody를 사용하지 않고 움직이는 오브젝트들을 구현할 때 많이 사용하는 것이다.

Animator 설정

이제 모델에 Animation을 걸어주자.

오브젝트 하위에 있는 모델을 클릭해서 Animator를 추가해주자.

Animator Controller도 만들어서 Animator에 넣어주자.

이제 Animation Controller를 더블클릭해서 Animator 창에 들어가주자.

배경에 우클릭을 해서 Create Sub-State Machine을 해줄 것이다.

그러고 이름을 Ground로 바꿔주자.

Sub-State Machine은 하나의 State들을 담는 것이 아닌 여러 스테이트로 구성된 집합이라고 생각하면 된다.

Ground를 클릭해서 들어가보면 다음과 같이 또다른 State Diagram들이 나온다.

다음과 같이 추가하고, 애니메이션들을 세팅해주자.

여기까지 구현을 마치고 실행해보면 다음과 같이 IDLE 상태의 애니메이션이 적용된 것을 볼 수 있다.

지금까지 상속을 받을 수 있는 기본적인 형태의 State와 그 State를 받는 State Machine을 만드는 과정에 대해 알아보았다.

추가적인 State들을 만드는 것은 다음 글에서 다루도록 하겠다.

Unity3D FSM으로 플레이어 캐릭터 조작 구현하기 (3)
기존 코드 수정Walk, Run 상태 추가 저번 게시글까지 FSM으로 플레이어 캐릭터 조작을 구현하기 위한 기본 State 클래스를 구성하는 부분까지는 완성을 했었다. Unity3D FSM으로 플레이어 캐릭터 조작 구현하기 (2)State Machine 만들기땅에 붙어 있을 때 상태 구현하기PlayerSO 만들기Player 오브젝트 설정Animator 설정 저번 글에서는 Input Action을 스크립트로 접근하는 법과 Player Animation을 FSM으로 구현하기 위한 기본 세팅을 마쳤었다. Unity3D FSM으로 플레이어 캐릭터 조작 구현하기 (1)프로젝트 세팅Input System으로 기본적인 이동 구현하기Input Actions와 오브젝트 연결하기플레이어에 애니메이션을 받도록 하는 스크립트..
https://seoksii.tistory.com/58

Uploaded by N2T