게임에는 정말 많은 상황이 존재한다. 플레이어나 몬스터와 같은 캐릭터의 공격, 방어, 죽음 등 작은 오브젝트의 상태부터 게임 전체적인 로비, 전투, 결과, 게임오버까지 다양한 상황들이 발생하고 이를 통해 유저의 플레이 경험에 영향을 끼친다.
게임처럼 다양한 상태가 존재하며 서로 상호작용하는 애플리케이션에서 효율적이고 일관된 상태관리 체계를 갖추는 것은 코드의 간결함을 키우고 유지보수를 용이하게 만들기 위해서 반드시 필요하다. 이를 위한 디자인 패턴이 바로 상태관리패턴(State Management Pattern)이다.
상태관리패턴은 간단히 말하면 애플리케이션의 중앙에서 상태의 변화를 감지하고 이에 따라 다른 처리를 해주는 방식이다.
상기한 동작을 가장 쉬운 방식으로 구현해보자.
public class Program
{
// 게임의 기본적인 상태
public enum GameState
{
Init,
Wait,
Play,
GameOver,
Exit
}
static void Main(string[] args)
{
GameState state = GameState.Init;
string? input;
int enemy = 0;
while (true)
{
// 상태에 따라 실행하는 로직이 다름
switch (state)
{
case GameState.Init:
Console.WriteLine("게임을 초기화합니다");
enemy = 5;
state = GameState.Wait;
break;
case GameState.Wait:
Console.WriteLine("게임을 시작하려면 Y를 입력하세요");
input = Console.ReadLine();
if (input != null && input.Equals("Y"))
{
state = GameState.Play;
}
break;
case GameState.Play:
Console.WriteLine("게임을 시작합니다. 아무 숫자를 입력해주세요.");
input = Console.ReadLine();
if (input != null && int.Parse(input) == enemy)
{
Console.WriteLine("정답!");
state = GameState.Play;
}
else
{
state = GameState.GameOver;
}
break;
case GameState.GameOver:
Console.WriteLine("게임 오버");
Console.WriteLine("게임을 다시 시작하려면 Y, 종료하려면 N을 입력하세요");
input = Console.ReadLine();
if (input != null && input.Equals("Y"))
{
state = GameState.Play;
}
else if (input != null && input.Equals("N"))
{
state = GameState.Exit;
}
break;
case GameState.Exit:
return;
break;
}
}
}
}

하지만 이 방법은 엄격하게 관리되는 상태관리패턴은 아니다. Switch문을 통해 게임의 상태를 중앙에서 제어하고 관리하고 있지만 패턴을 사용하면서 얻을 수 있는 확장성과 유지보수성을 최대한으로 활용하지 못하고 있다.
개선을 위해서 우리는 인터페이스를 활용해야한다. 엘레베이터를 예시로 들어보자. 엘레베이터의 동작에는 상승, 하강, 정지의 3가지 상태가 있다. 이 상태들을 하나의 State 인터페이스를 직접 구현하는 형태로 디자인하는 것이다.

먼저 IState 라는 인터페이스를 만들고 여기에 상태에 따른 로직을 실행하는 ExcuteState 메서드를 추가한다.
public interface IGameState
{
void ExcuteState(GameStateManager manager);
}
그리고 GameStateManager는 중앙에서 게임 상태를 관리하고 변경하는 역할을 맡게 될 새로운 클래스이다. currentState라는 상태 클래스를 저장할 멤버와 게임에 필요한 변수들을 가지고 있다.
public class GameStateManager
{
private IGameState currentState;
public int Enemy { get; set; }
public bool IsRunning { get; set; } = true;
public GameStateManager(IGameState initialState)
{
SetState(initialState);
}
public void SetState(IGameState newState)
{
currentState = newState;
}
public void Update()
{
currentState.ExcuteState(this);
}
}
이젠 IState를 구현하는 5개의 인터페이스를 만들자.
public class InitState : IGameState
{
public void ExcuteState(GameStateManager manager)
{
Console.WriteLine("게임을 초기화합니다");
manager.Enemy = 5;
manager.SetState(new WaitState());
}
}
public class WaitState : IGameState
{
public void ExcuteState(GameStateManager manager)
{
Console.WriteLine("게임을 시작하려면 Y를 입력하세요");
string? input = Console.ReadLine();
if (input != null && input.Equals("Y"))
{
manager.SetState(new PlayState());
}
}
}
public class PlayState : IGameState
{
public void ExcuteState(GameStateManager manager)
{
Console.WriteLine("게임을 시작합니다. 아무 숫자를 입력해주세요.");
string? input = Console.ReadLine();
if (input != null && int.TryParse(input, out int number) && number == manager.Enemy)
{
Console.WriteLine("정답!");
manager.SetState(new GameOverState());
}
else
{
manager.SetState(new GameOverState());
}
}
}
public class GameOverState : IGameState
{
public void ExcuteState(GameStateManager manager)
{
Console.WriteLine("게임 오버");
Console.WriteLine("게임을 다시 시작하려면 Y, 종료하려면 N을 입력하세요");
string? input = Console.ReadLine();
if (input != null && input.Equals("Y"))
{
manager.SetState(new InitState());
}
else if (input != null && input.Equals("N"))
{
manager.SetState(new ExitState());
}
}
}
public class ExitState : IGameState
{
public void ExcuteState(GameStateManager manager)
{
Console.WriteLine("게임을 종료합니다.");
manager.IsRunning = false;
}
}
마지막으로 메인에서 이런식으로 gameManager에게 상태관리를 담당하게 하면 정석적인 상태관리패턴이 만들어진다.
class Program
{
static void Main(string[] args)
{
GameStateManager gameManager = new GameStateManager(new InitState());
while (gameManager.IsRunning)
{
gameManager.Update();
}
}
}
이 방식은 새로운 상태를 추가할 때 기존 코드를 수정할 필요가 없다. 예를 들어, 새로운 상태인 PauseState를 추가할 경우, 새로운 클래스를 만들어 IGameState 인터페이스를 구현하면되기에 기존 코드를 변경할 필요가 없어진다.
또한, 각 상태의 책임이 명확하게 구분되면서도 상태 전환을 GameStateManager가 담당하기 때문에, 각 상태는 다른 상태에 대해 알 필요가 없어 의존성을 낮출 수 있다.