우선 기본적으로 새로운 팝업이 열렸을 때 이전 팝업의 상호작용은 불가능해야하며,팝업을 닫을 땐 먼저 열린 팝업이 제일 마지막에 닫혀야한다.
이러한 점을 고려했을 때 후입선출의 특징을 가지고 있다는 것을 알 수 있다.
그렇기 때문에 후입선출의 자료구조인 Stack을 활용하여 컨트롤러를 제작해보았다.
우선 한 컨트롤러로 모든 PopUp들을 컨트롤하기 위해선 캡슐화가 필요했다.
public interface IPopUp
{
public void Active();
public void UnActive();
public void ActiveRayTarget();
public void UnActiveRayTarget();
}
Active와 UnActive를 통해 팝업 오브젝트의 Active 상태를 컨트롤 하고 ActiveRayTarget과 UnActiveRayTarget을 통해 팝업 오브젝트의 상호작용을 컨트롤 해주기 위해 이렇게 4가지의 메서드를 캡슐화 해보았다.
그리고 정의된 인터페이스를 통해 팝업 컨트롤이 필요한 클래스에 적절히 인터페이스를 구현해주었다.
public class ExplainCanclePopUp : MonoBehaviour, IPopUp
{
// something...
public void Active()
{
StartCoroutine(FadeIn());
}
public void UnActive()
{
StartCoroutine(FadeOut());
}
public void ActiveRayTarget()
{
img.raycastTarget = true;
}
public void UnActiveRayTarget()
{
img.raycastTarget = false;
}
private IEnumerator FadeIn()
{
// Something...
}
private IEnumerator FadeOut()
{
// Something...
}
}
나는 취소 상호작용을 설명해주는 PopUp UI에 인터페이스를 구현해주었다.
팝업이 열릴 땐 Fade In이 되고, 팝업이 닫힐 땐 Fade Out이 되도록 구현해주었고
raycastTarget의 값을 변경하여 상호작용 컨트롤을 구현해주었다.
다음은 위와 같이 구현된 여러 개의 팝업 UI를 컨트롤 해줄 PopUpController를 구현해볼려고 한다.
public class PopUpController : MonoBehaviour
{
private Stack<IPopUp> popUpStack = new Stack<IPopUp>();
public void PushPopUp(IPopUp popUp) { }
public void PopPopUp() { }
}
앞서 설명했듯이 후입선출의 특징으로 팝업 리스트를 관리해주기 위해 Stack 자료구조로 정의해주었다.
또한 팝업을 삽입, 삭제를 할 수 있는 간단한 메서드도 정의해주었다.
삽입에 대한 구현은 다음과 같다.
public void PushPopUp(IPopUp popUp)
{
if (popUpStack.Count > 0)
{
var _top = popUpStack.Top();
if (_top.Equals(popUp)) return;
_top.UnActiveRayTarget();
}
popUpStack.Push(popUp);
popUp.Active();
popUp.ActiveRayTarget();
}
public static class ExtensionMethods
{
public static T Top<T>(this Stack<T> values)
{
var _top = values.Pop();
values.Push(_top);
return _top;
}
}
Stack에 팝업을 Push하기전 열려있는 팝업이 존재하는지 체크를 해주고 있다면 해당 팝업의 상호작용을 하지 못하도록 제어해준다.
이후 새로 열린 팝업을 리스트에 Push 해주고 해당 팝업을 Active하고 상호작용이 가능하도록 해준다.
+ C# Stack 자료구조는 Pop 메서드를 통해 마지막에 대입된 값을 반환 받을 수 있기 때문에
데이터의 삭제를 원치 않을 경우엔 번거롭게 다시 추가해주는 작업을 거쳐야 된다.
그렇기 때문에 나는 기존 Stack 클래스의 확장 메서드로 Top을 정의해주었다.
삭제에 대한 구현은 다음과 같다.
public void PopPopUp()
{
if (popUpStack.Count.Equals(0))
{
PushPopUp(optionPopUp);
return;
}
var _pop = popUpStack.Pop();
_pop.UnActiveRayTarget();
_pop.UnActive();
popUpStack.Top().ActiveRayTarget();
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Escape)) PopPopUp();
}
삭제 구현도 삽입과 마찬가지로 간단하다.
삭제에선 닫히는 팝업을 제어해주고 닫힌 팝업 이전에 있는 팝업을 활성화 시켜주면 된다.
이 프로젝트에선 팝업 닫기 버튼 이외에도 ESC키를 이용하여 팝업을 닫을 수 있도록 하였기 때문에
Update문에서 Input을 체크해주었고,
아무런 팝업이 열려있지 않을 때 ESC 키를 상호작용하면 옵션 팝업이 열리도록 구현해주었다.
이렇게 하나의 팝업 컨트롤러를 통해 관리하니 비교적 코드도 많이 쓰지 않고 구현한 것 같다.
이전 개인 프로젝트에서 상태 패턴을 구현할 땐,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상에서 좀 더 직관적으로 상태를 관리할 수 있다는 점이 아주 좋은 부분인 것 같다.
처음 시도인만큼 완벽한 설계 방식은 아니지만 문제없이 돌아간다는 점에서 큰 만족감을 가지게 되었다.