오늘은 프로젝트 C에서 오브젝트의 상태 패턴을 구현해 볼 것이다.
이전 개인 프로젝트에서 상태 패턴을 구현할 땐,Player와 Montser 오브젝트가 있을 때 각기 다른 상태를 정의하고 같은 오브젝트 끼리는 해당 상태 인스턴스를 공유한 방식이었다.같은 상태를 가지는 오브젝트가 개별로 상태 인스턴스를 가지고 있는건 메모리적으로 낭비가 되는 부분이기 때문에각 상태를 CashData로 저장하여 공유가 되도록 했다.
이번에는 이전 방식과 로직은 비슷하되 Scriptable Object를 활용하여 제작해보기로 결정했다.
스크립트는 크게 세 분류로 나누었으며,각 상태를 컨트롤 해주는 State Machine과 해당 상태로 변경할 수 있는 지에 대한 여부를 판단하는 Decision,해당 상태의 Action이 정의된 State로 나누었다.
public abstract class StateDecision : ScriptableObject
{
public abstract bool Decision(MyObject obj);
}
Decision은 bool 반환형 메서드로 정의하여 해당 상태로 변경이 가능한지에 대한 여부를 판단할 수 있도록 정의해주었다.
public abstract class State : ScriptableObject
{
public StateDecision decision;
public virtual void Enter(MyObject obj) { }
public virtual void Action(MyObject obj) { }
public virtual void Exit(MyObject obj) { }
}
State는 StateDecision 필드 변수 하나와 메서드 3가지로 정의하였다.
decision은 앞서 정의한 StateDecision을 가지는 필드 변수이다. 각 상태에 대응되는 Decision을 대입할 것 이다.
Enter는 해당 상태로 변했을 때 초기화가 이루어지는 부분이고, Action은 해당 상태에서 매 프레임마다 Update되는 메서드이다.
Exit는 해당 상태가 종료되었을 때 한번 호출되는 메서드이다.
public class GeneralStateMachine : ScriptableObject
{
[SerializeField] private List<State> states;
public void TransState(MyObject obj, ref State curState)
{
foreach (var _state in states)
{
if (_state.decision == null || _state.decision.Decision(obj))
{
ChangeState(obj, ref curState, _state);
break;
}
}
}
public void UpdateState(MyObject obj, State curState)
{
curState.Action(obj);
}
private void ChangeState(MyObject obj, ref State curState, State newState)
{
if (curState.Equals(newState)) return;
curState.Exit(obj);
newState.Enter(obj);
curState = newState;
}
}
StateMachine은 매 프레임마다 호출되어 상태를 바꾸어주는 TransState와 매 프레임마다 현재 상태의 Action을 호출해주는 UpdateState로 나누었다.
각 상태는 List를 통해 저장하며 순서가 젤 앞설수록 상태 전환의 우선 순위를 가질 수 있도록 설계했다.
현재 상태는 각 오브젝트 별로 가지고 있어(현재 상태는 고유의 값이므로) 매개변수를 통해 현재 상태를 전달해준다.
이때 현재 상태를 매개 변수로 전달할 때 값 형식으로 전달되기 때문에 반드시 ref 키워드를 붙여 참조 형식으로 전달해주어야 현재 상태의 값이 반영된다.
이렇게 뼈대는 완성이 되었고 이제 살만 붙이는 작업만 해준다면 멋진 상태 패턴을 만들 수 있다.
이전 방식에선 Controller에서 상태에 대한 행동을 정의하지 않고 상태 클래스 내에서 행동을 모두 정의했다.
Controller의 코드 다이어트가 되어 조금 더 깔끔해 보이지 않을까 하는 생각에 해당 방식으로 작성한 것이었다.
하지만 이번엔 조금 더 상태를 범용성있게 사용하기 위해 각 Controller에 상태에 대한 행동을 정의하고 해당 메서드를 각 상태에서 호출할 수 있게끔 설계하였고 먼저 캡슐화 작업을 해주었다.
#region State
public interface IIdleState
{
public bool Idleable { get; }
}
public interface IMoveState
{
public bool Moveable { get; }
}
public interface IAttackState
{
public bool Attackable { get; }
}
public interface IHitState
{
public bool Hitable { get; }
}
public interface IDeadState
{
public bool IsDead { get; }
}
#endregion
#region Action
public interface IIdleAction
{
public void Idle();
}
public interface IMoveAction
{
public void Move();
}
public interface IAttackAction
{
public void Attack();
}
public interface IHitAction
{
public void Hit();
}
public interface IDeadAction
{
public void Dead();
}
#endregion
캡슐화는 Decision에 필요한 것과 Action을 위해 필요한 것으로 나누어 정의했다.이후 필요한게 있다면 추가적으로 캡슐화를 할 예정이다.
이렇게 정의된 interface를 바탕으로 각 State와 Decision을 정의해주었고, 아래는 Move 상태에 대한 코드이다.
public class MoveDecision : StateDecision
{
public override bool Decision(MyObject obj)
{
return (obj as IMoveState).Moveable;
}
}
public class MoveState : State
{
public override void Enter(MyObject obj)
{
}
public override void Action(MyObject obj)
{
(obj as IMoveAction).Move();
}
public override void Exit(MyObject obj)
{
}
}
매개 변수를 통해 전달 받은 오브젝트를 캐스팅하여 메서드를 호출하는 간단한 코드이다.
이렇게 정의된 스크립트를 바탕으로 Decision과 State를 만들어주었고, 해당 State에 대응되는 Decision을 대입해주었다.
다른 상태들 역시 필요하다면 이렇게 추가하여 관리할 수 있다.
이렇게 몬스터에 필요한 각 상태들을 정의하고 우선순위에 맞게 각 상태들을 List에 대입한 모습이다.
이 방식은 Editor상에서 좀 더 직관적으로 상태를 관리할 수 있다는 점이 아주 좋은 부분인 것 같다.
처음 시도인만큼 완벽한 설계 방식은 아니지만 문제없이 돌아간다는 점에서 큰 만족감을 가지게 되었다.
'Unity' 카테고리의 다른 글
유니티 프로젝트 개발 일지 - 3 (렌더링) (0) | 2024.12.20 |
---|---|
유니티 프로젝트 개발 일지 - 2 (팝업 UI 컨트롤러) (1) | 2024.12.19 |
유니티 프로젝트 개발 일지 - 0 (0) | 2024.12.17 |
[Unity] Asset Management(에셋 관리) - Resources, AssetBundle, Addressable (3) | 2024.09.23 |
[Unity] 유니티 생명 주기(Unity Life Cycle) (3) | 2024.09.06 |