
어떤 게임에 한 유닛이 존재한다.
이 유닛은 기본 상태도 있으며, 특정한 방향으로 이동할 수도 있고, 적이 있다면 공격할 수도 있다.
받은 공격에 따라 뒤로 넉백 될 수 있으며, 체력이 전부 소모되면 죽게 된다.
이 유닛은 이러한 여러 상태들을 가지고 있으며, 조건에 따라 자유자재로 변화하게 된다
이를 구현할 수 있는 방법 중 하나가 바로 상태 패턴이다.
상태패턴은 어떤 객체의 상태를 바꿈으로서, 그 상태에 따라 다른 기능을 할 수 있도록 만드는 디자인 패턴이다.
public interface IState
{
public void Enter();
public void Exit();
public void Update();
public void PhisicsUpdate();
}
public abstract class StateMachine
{
public IState currentState;
public void ChangeState(IState state)
{
currentState?.Exit();
currentState = state;
currentState?.Enter();
}
public void Update()
{
currentState?.Update();
}
public void PhisicsUpdate()
{
currentState?.PhisicsUpdate();
}
}
상태 패턴을 구현하기 위해 만든 상태 인터페이스와 상태머신 추상클래스이다.
우선 각 상태들은 공통적으로, 상태에 들어가는 메서드, 나가는 메서드와 Update에 관련된 메서드들이 있다.
상태머신 추상클래스는 상태를 바꿔주는 ChangeState메서드와 상태들의 메서드들을 작동시켜주는 메서드가 포함되어있다.
public class UnitStateMachine : StateMachine
{
public Unit Unit { get; }
// States
public UnitIdleState IdleState { get; private set; }
public UnitMoveState MoveState { get; private set; }
public UnitAttackState AttackState { get; private set; }
public UnitKnockbackState KnockbackState { get; private set; }
public UnitDieState DieState { get; private set; }
public float MovementSpeed { get; protected set; }
public float MovementSpeedModifier { get; set; }
public UnitStateMachine(Unit unit)
{
this.Unit = unit;
IdleState = new UnitIdleState(this);
MoveState = new UnitMoveState(this);
AttackState = new UnitAttackState(this);
KnockbackState = new UnitKnockbackState(this);
DieState = new UnitDieState(this);
MovementSpeed = Unit.unitStat.unitSO.moveSpeed;
}
}
public UnitStateMachine stateMachine;
private void Awake()
{
stateMachine = new UnitStateMachine(this);
}
private void OnEnable()
{
stateMachine.ChangeState(stateMachine.IdleState);
}
private void Update()
{
stateMachine?.Update();
}
private void FixedUpdate()
{
stateMachine?.PhysicsUpdate();
}
위 코드블록은 상태머신을 추상클래스를 상속받은 유닛의 상태머신이며, 아래 코드블록은 Unit 컴포넌트에서 상태머신과 관련된 코드만 추린 것이다.
추상클래스를 상속받은 UnitStateMachine는 당연히 추상클래스에 있는 메서드들도 가지고 있다.
그리고 생성될 때, Unit에게 필요한 모든 상태들을 생성하고, 필요할 때마다 그 상태를 전환할 수 있다.
Unit에서는 Awake할 때 상태머신을 생성하며, OnEnable시 IdleState가 되도록 설정했다.
그리고 상태머신의 Update와 PhysicsUpdate()를 Unit의 Update와 FixedUpdate에서 실행한다.
Unit은 MonoBehaviour를 상속받았기에 Unity 생명주기함수를 이용할 수 있다.
public class UnitBaseState : IState
{
protected UnitStateMachine stateMachine;
// Move
private Vector2 movementDir;
// IsEnemyInRanged
private Vector2 rayPos;
private float attackRange;
private LayerMask layerMask;
protected float stopMoveSpeedModifier;
protected float startMoveSpeedModifier;
protected Unit target;
public UnitBaseState(UnitStateMachine unitStateMachine)
{
stateMachine = unitStateMachine;
attackRange = stateMachine.Unit.unitStat.unitSO.attackRange;
layerMask = stateMachine.Unit.unitStat.unitSO.targetLayerMask;
stopMoveSpeedModifier = 0f;
startMoveSpeedModifier = 10f;
}
public virtual void Enter()
{
}
public virtual void Exit()
{
}
public virtual void Update()
{
// 죽었을 때
if (stateMachine.Unit.unitStat.curHP <= 0 && stateMachine.currentState != stateMachine.DieState)
{
Die();
}
}
public virtual void PhysicsUpdate()
{
Move();
}
private void Move()
{
movementDir = stateMachine.Unit.unitStat.unitSO.unitMoveDirection;
Rotate(movementDir);
// 상태에서 설정하는 stateMachine.MovementSpeedModifier에 따라 속도가 달라짐
float unitMovementSpeed = stateMachine.MovementSpeed * stateMachine.MovementSpeedModifier;
stateMachine.Unit.Move((Time.fixedDeltaTime * unitMovementSpeed) * movementDir);
}
// 오브젝트 회전 메서드
private void Rotate(Vector2 movementDir)
{
if (movementDir.x >= Vector2.zero.x)
{
stateMachine.Unit.transform.rotation = Quaternion.Euler(Vector3.zero);
}
else
{
stateMachine.Unit.transform.rotation = Quaternion.Euler(0f, 180f, 0f);
}
}
// 공격 상태로 가기 위한 bool 반환 메서드
protected bool IsEnemyInRanged()
{
rayPos = (Vector2)stateMachine.Unit.transform.position + new Vector2((0.5f * movementDir.normalized.x), 0.5f);
if (Physics2D.Raycast(rayPos, stateMachine.Unit.transform.right, attackRange, layerMask))
{
RaycastHit2D hit = Physics2D.Raycast(rayPos, stateMachine.Unit.transform.right, attackRange, layerMask);
if (target == null || target.gameObject != hit.collider.gameObject)
{
target = hit.collider.gameObject.GetComponent<Unit>();
}
return true;
}
return false;
}
private void Die()
{
stateMachine.ChangeState(stateMachine.DieState);
}
private void Knockback()
{
stateMachine.ChangeState(stateMachine.KnockbackState);
}
protected void StartAnimation(int animationHash)
{
stateMachine.Unit.unitAnimator.SetBool(animationHash, true);
}
protected void StopAnimation(int animationHash)
{
stateMachine.Unit.unitAnimator.SetBool(animationHash, false);
}
}
위 코드는 IState 인터페이스를 상속받은 UnitBaseState이다.
UnitStateMachine에서 생성하는 Idle, Move, Attack과 같은 상태들은 모두 이 UnitBaseState를 상속받는다.
public class UnitIdleState : UnitBaseState
{
public UnitIdleState(UnitStateMachine unitStateMachine) : base(unitStateMachine)
{
}
public override void Enter()
{
stateMachine.MovementSpeedModifier = stopMoveSpeedModifier;
base.Enter();
}
public override void Exit()
{
}
public override void Update()
{
base.Update();
if (IsEnemyInRanged())
{
stateMachine.ChangeState(stateMachine.AttackState);
}
else
{
stateMachine.ChangeState(stateMachine.MoveState);
}
}
}
public class UnitMoveState : UnitBaseState
{
public UnitMoveState(UnitStateMachine unitStateMachine) : base(unitStateMachine)
{
}
public override void Enter()
{
stateMachine.MovementSpeedModifier = startMoveSpeedModifier;
base.Enter();
StartAnimation(stateMachine.Unit.UnitAnimationData.MoveParameterHash);
}
public override void Exit()
{
base.Exit();
StopAnimation(stateMachine.Unit.UnitAnimationData.MoveParameterHash);
}
public override void Update()
{
base.Update();
if(IsEnemyInRanged())
{
stateMachine.ChangeState(stateMachine.AttackState);
}
}
}
위 두 코드는 Idle상태와 Move상태의 스크립트이다.
눈에 띄는 차이점이라면 다음과 같다
1. MovementSpeedModifier
Idle에서는 MovementSpeedModifier를 stopMoveSpeedModifier으로 설정하고,
Move에서는 startMoveSpeedModifier로 설정한다.
StopMoveSpeedModifier은 0f으로 해당 상태때 움직이지 않게 해주는 기능을 담당한다.
2. Animation관련 메서드
Move에서는 시작하고 끝날 때, 애니메이터를 제어하는 메서드가 호출된다.

3. Update 메서드 내용이 다르다.
Idle에서는 앞에 적이 있는지, 없는지를 판단하고 움직이거나 공격을 하게 하였지만,
이동시에는 따로 멈추는 메서드 없이, 적이 있으면 공격을 하게 하는 기능만 있다.
이 세가지가 달라짐으로서, 상태에 따라 그 상태에 맞는 다른 행동을 구현할 수 있게 되었다.
만약 switch문으로 상태머신을 구현하고자 했었다면, 코드가 매우 길어지고, if문이 지나치게 길어질 수도 있었지만,
상태패턴을 구현함으로서, 상대적으로 더 간편하게 구현할 수 있었다고 생각된다.
'내일배움캠프 TIL' 카테고리의 다른 글
| 내일배움캠프 46일차 TIL "유닛 스탯 적용 트러블 슈팅" (0) | 2024.11.21 |
|---|---|
| 내일배움캠프 45일차 TIL "유니티의 생명주기 함수" (0) | 2024.11.20 |
| 내일배움캠프 43일차 TIL "Sprite Atlas" (0) | 2024.11.18 |
| 내일배움캠프 42일차 TIL "유니티에서 코딩할 때 안 좋은 습관" (0) | 2024.11.15 |
| 내일배움캠프 41일차 TIL "nameof 키워드" (0) | 2024.11.14 |