6/24 부트캠프 개발 TIL (상태머신 FSM)
오늘은 강의때 배운 상태머신에 대해 이해를 해보자
상태머신이란?
- 유한 상태 기계(Finite State Machine, FSM)
- FSM의 개념
- FSM은 유한한 갯수의 상태들로 구성된 기계 및 패턴을 말합니다.
- 상태와 상태 간의 전환을 기반으로 동작하는 동작 기반 시스템입니다.
- FSM의 구성 요소
- 상태 (State): 시스템이 취할 수 있는 다양한 상태를 나타냅니다.
- 전환 조건 (Transition Condition): 상태 간 전환을 결정하는 조건입니다.
- 동작 (Action): 상태에 따라 수행되는 동작 또는 로직을 나타냅니다.
- FSM의 동작 원리
- 초기 상태에서 시작하여 입력 또는 조건에 따라 상태 전환을 수행합니다.
- 상태 전환은 전환 조건을 충족할 때 발생하며, 전환 조건은 입력, 시간, 조건 등으로 결정됩니다.
- 상태 전환 시 이전 상태의 종료 동작과 새로운 상태의 진입 동작이 수행됩니다.
- FSM의 예시: 플레이어 상태 관리
- 상태: 정지 상태, 이동 상태, 점프 상태
- 전환 조건: 이동 입력, 점프 입력, 충돌 등의 조건
- 동작: 이동 애니메이션 재생, 점프 처리, 이동 속도 조정 등
- Switch-Case 문을 활용한 FSM의 단점
- 상태 기계를 구현하는 가장 간단한 방법은 case switch 문이다.
- 상태가 많아지고 조건이 복잡해 진다면 코드가 지나치게 길어진다. (ex. 어떤 행동 때 특정 키의 입력을 막는 조건이 들어갈 때 등)
- 유지보수에 어려움을 겪을 수 있다.
- 상태가 추가될 때마다 새로운 분기를 작성해야되고, 중복된 코드가 많아진다.
- State Pattern(상태패턴)을 활용한 FSM의 장점
- 기본적으로 객체지향의 다형성을 활용한다.
- 상태를 명확하게 정의하고 상태 간 전환을 일관되게 관리할 수 있습니다.
- 복잡한 동작을 상태와 전환 조건으로 나누어 구현하므로 코드 유지 보수가 용이합니다.
- 다양한 동작을 유기적으로 조합하여 원하는 동작을 구현할 수 있습니다.
위에 정의한 내용에 대해 정확히 알기 위해서 코드로 구현해보자
//StateMachine.cs
//각종 필요한 기능을 추가하기 위한 인터페이스 선언
public interface IState
{
public void Enter();
public void Exit();
public void HandleInput();
public void Update();
public void PhysicsUpdate();
}
//상태머신의 유일한 점은 바로 추상클래스에 MonoBehavior를 상속받지 않는다는 점이다.
//이유는 하이어라키 창에 컴포넌트를 추가하면 나중에 상태를 추가하기 많아지기 때문일 것이다.
public abstract class StateMachine
{
protected IState currentState;
public void ChangeState(IState state)
{
currentState?.Exit();
currentState = state;
currentState?.Enter();
}
public void HandleInput()
{
currentState?.HandleInput();
}
public void Update()
{
currentState?.Update();
}
public void PhysicsUpdate()
{
currentState?.PhysicsUpdate();
}
}
이렇게 기본적으로 상태 머신 틀을 구현하고 이동, 점프, 공격 등의 상태를 추가하기 위해 스크립트로 구현하자
플레이어가 이동할 때와 점프할 때 차이점은 이동할때 땅 위에 있는 상태이고 점프할 때는 공중에 있는 상태를 뜻한다.
그래서 땅에 닿았을 때 상태를 구현해보자
PlayerBaseState가 상속시키는 클래스가 많고 길어져서 맨아래 첨부파일로 열어서 참고하길 바란다.
//PlayerGroundState.cs
//override가 붙인 메서드는 PlayerBaseState에 상속받은 메서드 들이다.
public class PlayerGroundState : PlayerBaseState
{
public PlayerGroundState(PlayerStateMachine stateMachine) : base(stateMachine) { }
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();
if(stateMachine.IsAttacking)
{
OnAttack();
return;
}
}
public override void PhysicsUpdate()
{
base.PhysicsUpdate();
if(!stateMachine.Player.Controller.isGrounded && stateMachine.Player.Controller.velocity.y < Physics.gravity.y * Time.fixedDeltaTime)
stateMachine.ChangeState(stateMachine.FallState);
}
protected override void OnMovementCanceled(InputAction.CallbackContext context) //이동이 끝났을 때
{
if (stateMachine.MovementInput == Vector2.zero) return;
stateMachine.ChangeState(stateMachine.IdleState);
base.OnMovementCanceled(context);
}
protected override void OnJumpStarted(InputAction.CallbackContext context) //점프를 입력했을 때
{
base.OnJumpStarted(context);
stateMachine.ChangeState(stateMachine.JumpState);
}
protected virtual void OnAttack() //공격을 입력했을 때
{
stateMachine.ChangeState(stateMachine.ComboAttackState);
}
}
땅에 닿았을 때는 Idle, Walk , Run 등이 있을 것이다.
구현은 간단하다.
//PlayerIdleState.cs
public class PlayerIdleState : PlayerGroundState
{
public PlayerIdleState(PlayerStateMachine stateMachine) : base(stateMachine) { }
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();
//플레이어입력을 받고 있다면 ChangeState에 WalkState를 호출한다
if (stateMachine.MovementInput != Vector2.zero)
{
stateMachine.ChangeState(stateMachine.WalkState);
return;
}
}
}
//PlayerWalkState.cs
public class PlayerWalkState : PlayerGroundState
{
public PlayerWalkState(PlayerStateMachine stateMachine) : base(stateMachine) { }
public override void Enter()
{
stateMachine.MovementSpeedModifier = groundData.WalkSpeedModifier;
base.Enter();
StartAnimation(stateMachine.Player.AnimationData.WalkParameterHash);
}
public override void Exit()
{
base.Exit();
StopAnimation(stateMachine.Player.AnimationData.WalkParameterHash);
}
protected override void OnRunStarted(InputAction.CallbackContext context)
{
//걷다가 달리기를 입력받으면 달리기 상태를 바꾸어 호출한다.
base.OnRunStarted(context);
stateMachine.ChangeState(stateMachine.RunState);
}
}
//PlayerRunState.cs
public class PlayerRunState : PlayerGroundState
{
public PlayerRunState(PlayerStateMachine stateMachine) : base(stateMachine) { }
public override void Enter()
{
stateMachine.MovementSpeedModifier = groundData.RunSpeedModifier;
base.Enter();
StartAnimation(stateMachine.Player.AnimationData.RunParameterHash);
}
public override void Exit()
{
base.Exit();
StopAnimation(stateMachine.Player.AnimationData.RunParameterHash);
}
}
각 상태에 따라서 필요한 메서드를 받아서 구현하면 되고 여기서 공통점은 Enter와 Exit함수가 포함되어 있다는 것이다.
상태가 들어왔을 때와 빠져 나왔을 때를 확인하기 위해 Enter로 플레이어 상태가 Walk가 되었다는 점과 Exit로 Walk상태를 빠져나간다는 점을 이해하면 될것 같다.
또한 공통된 함수 안에서 StartAnimation과 StopAnimation이 있는 것은 나중에 애니메이션까지 적용할 때 애니메이션을 제어하는 코드를 따로 만들어 적용한다는 것이다.
상태머신을 이용하여 플레이어에게 적용시킨 결과 플레이어의 이동, 점프, 공격이 잘 구현되어있다.
그리고 점프할 때와 공격할 때의 상태를 어떻게 구현하는지 알고 싶으면 아래에 파일을 받고 압축을 풀어 확인해보길 바란다.
여기서 한가지 영감이 떠올린 점은 RPG게임을 만든다고 가정하면 튜토리얼 부분에서 공격을 할 수 없는 부분을 걸어놓고 싶을 때 상태머신에서 튜토리얼 부분이라면 공격을 할 수 없는 로직을 따로 만들어서 구현할 수 있다는 점이다.
(물론 복잡하고 감이 안오겠지만 느낌상으로 할 수 있을것이다.)
by 스파르타 코딩클럽