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 캐릭터를 제어하는 데 사용됩니다.
캐릭터 컨트롤러의 주요 기능과 특징은 다음과 같습니다:
- 캐릭터 이동: 캐릭터 컨트롤러는 단순한 이동을 쉽게 구현할 수 있도록 메서드를 제공합니다. 주로 이동 방향과 이동 속력을 설정하여 캐릭터를 움직이게 합니다.
- 중력 적용: 캐릭터 컨트롤러는 중력을 적용하여 점프나 떨어짐을 자연스럽게 처리할 수 있습니다.
- 충돌 처리: 캐릭터 컨트롤러는 물리 엔진을 사용하지 않고, 캐릭터의 충돌을 감지하고 처리할 수 있습니다. 다른 콜리더와의 충돌을 통제하고, 경사로와의 상호작용 등을 지원합니다.
- 바닥 검출: 캐릭터 컨트롤러는 캐릭터가 바닥 위에 놓이도록 바닥 검출을 처리합니다. 바닥과의 거리, 표면 노멀 등을 고려하여 캐릭터의 높이를 조절하거나 점프를 가능하게 합니다.
- 움직임 제한: 캐릭터 컨트롤러는 움직임을 제한하는 기능도 제공합니다. 지정된 영역 내에서만 움직이도록 하거나, 지형의 경사를 따라 이동할 수 있도록 설정할 수 있습니다.
GroundData 에서는 Idle, Walk, Run만 관여하고 있어 세가지 애니메이션을 추가하고 파라미터를 설정한다.
Jump의 경우 AirData에 있으므로 Ground 처럼 Air를 추가하여 Jump, Fall 애니메이션을 추가해준다.