2024.01.17,18, 19 최종프로젝트 (FSM)

2024. 1. 17. 21:41카테고리 없음

유한 상태 기계(Finite State Machine, FSM)

FSM을 활용하여 플레이어의 이동과 동작을 구현해보았다.

  • FSM의 개념
    • FSM은 유한 상태 기계를 나타내는 디자인 패턴입니다.
    • 상태와 상태 간의 전환을 기반으로 동작하는 동작 기반 시스템입니다.
  • FSM의 구성 요소
    • 상태 (State): 시스템이 취할 수 있는 다양한 상태를 나타냅니다.
    • 전환 조건 (Transition Condition): 상태 간 전환을 결정하는 조건입니다.
    • 동작 (Action): 상태에 따라 수행되는 동작 또는 로직을 나타냅니다.
  • FSM의 동작 원리
    • 초기 상태에서 시작하여 입력 또는 조건에 따라 상태 전환을 수행합니다.
    • 상태 전환은 전환 조건을 충족할 때 발생하며, 전환 조건은 입력, 시간, 조건 등으로 결정됩니다.
    • 상태 전환 시 이전 상태의 종료 동작과 새로운 상태의 진입 동작이 수행됩니다.
  • FSM의 장점
    • 상태를 명확하게 정의하고 상태 간 전환을 일관되게 관리할 수 있습니다.
    • 복잡한 동작을 상태와 전환 조건으로 나누어 구현하므로 코드 유지 보수가 용이합니다.
    • 다양한 동작을 유기적으로 조합하여 원하는 동작을 구현할 수 있습니다.
  • FSM의 예시: 플레이어 상태 관리
    • 상태: 정지 상태, 이동 상태, 점프 상태
    • 전환 조건: 이동 입력, 점프 입력, 충돌 등의 조건
    • 동작: 이동 애니메이션 재생, 점프 처리, 이동 속도 조정 등

플레이어 오브젝트를 생성 후 Player 모델이 되는 Asset 파일을 추가해주었다.

 

Input System을 활용하여 Input Action을 설정해주었고, Generate C# Class 파일을 Apply 하여 만들어주었다.

 

만들어진 PlayerInputActions 스크립트를 호출하여 사용하기 위해 만들어준다.

 

StateMachine 준비하기

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerInput : MonoBehaviour //플레이어 액션을 스크립트로 처리
{
    public PlayerInputActions InputActions { get; private set; } //인풋시스템으로 만든 클래스
    public PlayerInputActions.PlayerActions PlayerActions { get; private set; } //인풋시스템을 활용하여 동작구현

    private void Awake()//인풋액션을 생성
    {
        InputActions = new PlayerInputActions();

        PlayerActions = InputActions.Player;
    }
    private void OnEnable()
    {
        InputActions.Enable(); //인풋액션 실행
    }

    private void OnDisable()
    {
        InputActions.Disable(); //인풋액션 실행취소
    }

}

플레이어의 애니메이션의 상태들을 정보를 만들어준다.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[Serializable]
public class PlayerAnimationData //애니메이션에서 사용할 파라미터들을 생성
{
    [SerializeField] private string groundParameterName = "@Ground";
    [SerializeField] private string idleParameterName = "Idle";
    [SerializeField] private string walkParameterName = "Walk";
    [SerializeField] private string runParameterName = "Run";

    [SerializeField] private string airParameterName = "@Air";
    [SerializeField] private string jumpParameterName = "Jump";
    [SerializeField] private string fallParameterName = "Fall";

    [SerializeField] private string attackParameterName = "@Attack";
    [SerializeField] private string comboAttackParameterName = "ComboAttack";


    public int GroundParameterHash { get; private set; }
    public int IdleParameterHash { get; private set; }
    public int WalkParameterHash { get; private set; }
    public int RunParameterHash { get; private set; }

    public int AirParameterHash { get; private set; }
    public int JumpParameterHash { get; private set; }
    public int fallParameterHash { get; private set; }

    public int AttackParameterHash { get; private set; }
    public int ComboAttackParameterHash { get; private set; }

    public void Initialize() //플레이어 애니메이션을 생성해서 바로 사용가능
    {
        GroundParameterHash = Animator.StringToHash(groundParameterName);
        IdleParameterHash = Animator.StringToHash(idleParameterName);
        WalkParameterHash = Animator.StringToHash(walkParameterName);
        RunParameterHash = Animator.StringToHash(runParameterName);
//Ground 상태에서 동작되는 애니메이션
        AirParameterHash = Animator.StringToHash(airParameterName);
        JumpParameterHash = Animator.StringToHash(jumpParameterName);
        fallParameterHash = Animator.StringToHash(fallParameterName);
//Air 상태에서 동작되는 애니메이션
        AttackParameterHash = Animator.StringToHash(attackParameterName);
        ComboAttackParameterHash = Animator.StringToHash(comboAttackParameterName);
//Attack 관련 동작 애니메이션        
        
    }
}

Player 오브젝트에 적용해줄 Player 스크립트를 만들어준다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour //플레이어에 필요한 총 데이터, 컴포넌트 총집합
{
    [field: Header("Animations")]
    [field: SerializeField] public PlayerAnimationData AnimationData { get; private set; }

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

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

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

    private void Start()
    {
        Cursor.lockState = CursorLockMode.Locked; //커서를 사라지도록 설정
    }

}

다중상속을 시키기 위해 인터페이스를 사용하여 IState를 만든다.

public interface IState //인터페이스 내부구현X 실행할 함수들만 작성한다.
{
    public void Enter();
    public void Exit();
    public void HandleInput();
    public void Update();
    public void PhysicsUpdate();

}

StateMachine 도 만들어준다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Experimental.AI;

public abstract class StateMachine //IState를 활용한다. 추상클래스로 만들어 상속을 받아 사용한다.
{
    protected IState currentState; //현재의 상태를 표시

    public void ChangeState(IState newState)
    {
        currentState?.Exit(); //전에 하고 있는 상태를 종료한다.

        currentState = newState; //새로운 동작을 받아온다.

        currentState?.Enter(); //새로운 동작을 실행한다.
    }

    public void HandleInput()
    {
        currentState?.HandleInput();
    }

    public void Update()
    {
        currentState?.Update();
    }

    public void PhysicsUpdate()
    {
        currentState?.PhysicsUpdate();
    }
}

플레이어가 Ground에 위치했을 때 초기 설정값을 만들어준다.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[Serializable] //PlayerSO에서 사용하기 때문에 인스펙터창에 보일 수 있도록 설정
public class PlayerGroundData
{
    [field: SerializeField][field: Range(0f, 25f)] public float BaseSpeed { get; private set; } = 5f; //이동속도
    [field: SerializeField][field: Range(0f, 25f)] public float BaseRotationDamping { get; private set; } = 1f; //기본감쇠

    [field: Header("IdleData")]

    [field: Header("WalkData")]
    [field: SerializeField][field: Range(0f, 2f)] public float WalkSpeedModifier { get; private set; } = 0.225f;

    [field: Header("RunData")]
    [field: SerializeField][field: Range(0f, 2f)] public float RunSpeedModifier { get; private set; } = 1f;
}

플레이어가 점프를 했을 때 정보들을 PlayerAirData에 기입한다. (JumpForce 프로퍼티)

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[Serializable]
public class PlayerAirData
{
    [field: Header("JumpData")]
    [field: SerializeField][field: Range(0f, 25f)] public float JumpForce { get; private set; } = 4f; //점프에 필요한 힘

}

스크립터블오브젝트 PlayerSO

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName ="Player",menuName = "Characters/Player")]

public class PlayerSO : ScriptableObject
{
    [field: SerializeField] public PlayerGroundData GroundedData { get; private set; }
    [field: SerializeField] public PlayerAirData AirData { get; private set; }
}

PlayerBaseState 를 통해 기본상태에 실행되어야 할 함수들을 작성한다.

using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UIElements;

public class PlayerBaseState : IState //플레이어의 각 State 에서 오버라이드하여 사용된다.
{
    protected PlayerStateMachine stateMachine;
    protected readonly PlayerGroundData groundData;

    public PlayerBaseState(PlayerStateMachine playerStateMachine)
    {
        stateMachine = playerStateMachine;
        groundData = stateMachine.Player.Data.GroundedData; //PlayerSO에 있는 GroundedData를 사용
    }

    public virtual void Enter() //함수 재정의를 위해 virtual 사용
    {
        AddInputActionsCallbacks();
    }

    public virtual void Exit()
    {
        RemoveInputActionsCallbacks();
    }

    public virtual void HandleInput()
    {
        ReadMovementInput();
    }

    public virtual void PhysicsUpdate()
    {

    }

    public virtual void Update()
    {
        Move();
    }

    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; //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 = GetMovementSpeed();
        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 GetMovementSpeed()
    {
        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);
    }
    protected virtual void AddInputActionsCallbacks()
    {
    
    }
    protected virtual void RemoveInputActionsCallbacks()
    {
    
    }
    

}

Ground 애니메이션에서 각각의 행동들이 구현될 함수를 작성한다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerGroundedState : PlayerBaseState
{
    public PlayerGroundedState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
    {
    }

    public override void Enter()
    {
        base.Enter();
        StartAnimation(stateMachine.Player.AnimationData.GroundParameterHash); //Ground Hash 값을 bool값을 켠다.
    }

    public override void Exit()
    {
        base.Exit();
        StopAnimation(stateMachine.Player.AnimationData.GroundParameterHash);
    }

    public override void Update()
    {
        base.Update();
    }

    public override void PhysicsUpdate()
    {
        base.PhysicsUpdate();
    }

    protected override void OnMovementCanceled(InputAction.CallbackContext context)
    {
        if(stateMachine.MovementInput == Vector2.zero)
        {
            return;
        }

        stateMachine.ChangeState(stateMachine.IdleState);

        base.OnMovementCanceled(context);
    }

    protected virtual void OnMove()
    {
        stateMachine.ChangeState(stateMachine.WalkState);
    }

}

IdleState 스크립트 작성

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerIdleState : PlayerGroundedState //땅에 있을 때 IdleState 상태
{
    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();
    }
}

PlayerStateMachine 을 통해 모든 State를 관리한다.

using System.Collections;
using System.Collections.Generic;
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) //player 생성자를 만들어준다.
    {
        this.Player = player;

        IdleState = new PlayerIdleState(this);

        MainCameraTransform = Camera.main.transform;

        MovementSpeed = player.Data.GroundedData.BaseSpeed;
        RotationDamping = player.Data.GroundedData.BaseRotationDamping;
    }
}

추가된 State를 Player 스크립트에도 저장한다.

''' 생략
[field: Header("References")]
[field: SerializeField] public PlayerSO Data { get; private set; }

''' 생략
private PlayerStateMachine stateMachine;

private void Awake()
{
    AnimationData.Initialize();
		Animator = GetComponentInChildren<Animator>();
    Input = GetComponent<PlayerInput>();
    Controller = GetComponent<CharacterController>();

    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();
}
''' 생략

캐릭터 컨트롤러(Character Controller)

캐릭터 컨트롤러(Character Controller)는 Unity에서 캐릭터나 플레이어의 움직임과 충돌을 관리하기 위해 사용되는 컴포넌트입니다. 이 컴포넌트는 물리 엔진이 아닌 캐릭터의 움직임을 프레임 기반으로 처리하므로, 주로 3D 캐릭터를 제어하는 데 사용됩니다.

캐릭터 컨트롤러의 주요 기능과 특징은 다음과 같습니다:

  1. 캐릭터 이동: 캐릭터 컨트롤러는 단순한 이동을 쉽게 구현할 수 있도록 메서드를 제공합니다. 주로 이동 방향과 이동 속력을 설정하여 캐릭터를 움직이게 합니다.
  2. 중력 적용: 캐릭터 컨트롤러는 중력을 적용하여 점프나 떨어짐을 자연스럽게 처리할 수 있습니다.
  3. 충돌 처리: 캐릭터 컨트롤러는 물리 엔진을 사용하지 않고, 캐릭터의 충돌을 감지하고 처리할 수 있습니다. 다른 콜리더와의 충돌을 통제하고, 경사로와의 상호작용 등을 지원합니다.
  4. 바닥 검출: 캐릭터 컨트롤러는 캐릭터가 바닥 위에 놓이도록 바닥 검출을 처리합니다. 바닥과의 거리, 표면 노멀 등을 고려하여 캐릭터의 높이를 조절하거나 점프를 가능하게 합니다.
  5. 움직임 제한: 캐릭터 컨트롤러는 움직임을 제한하는 기능도 제공합니다. 지정된 영역 내에서만 움직이도록 하거나, 지형의 경사를 따라 이동할 수 있도록 설정할 수 있습니다.

 

Bool로 파라미터를 생성하고, 에쎗에 준비해둔 애니메이션들을 가져온다.

GroundData 에서는 Idle, Walk, Run만 관여하고 있어 세가지 애니메이션을 추가하고 파라미터를 설정한다.

Jump의 경우 AirData에 있으므로 Ground 처럼 Air를 추가하여 Jump, Fall 애니메이션을 추가해준다.