발전하는 춘배

[원고, C++/OOP] 로그라이크 게임 만들어보기 10 - 상태 머신 본문

.원고

[원고, C++/OOP] 로그라이크 게임 만들어보기 10 - 상태 머신

춘배0 2026. 2. 21. 14:57

지금 Game.run은 이렇다.

void Game::run() {
  isRunning = true;
  world->populateMap("map1", 50, 50);
  player= world->getPlayer();
  camera->follow(player->getX(), player->getY(), world->getCurrentMap()->getWidth(), world->getCurrentMap()->getHeight());
  while (isRunning) {
    render();
    handleInput();
    if (world->isPlayerDead()) {
      std::cout << "You died!\n";
      isRunning = false;
      break;
    }
    camera->follow(player->getX(), player->getY(), world->getCurrentMap()->getWidth(), world->getCurrentMap()->getHeight());
  }
}

뭔가 마음에 안 든다.

일단 코드가 잘 안 읽힌다.

그리고 전에 테스트 하면서 느낀 건데

Enter command: /inv
-----------Inventory-----------
Inventory (0/32):
-------------------------------
-----------Equipment-----------
-------------------------------
# # # # # . # # # # # # # # # . . . . . . . . # # # # # . . 
# # # # # . # # # # # # # # # . . . . . . . . # # # # # # # 
# # # # # . # # # # # # # # # . . . . . . . . # # # # # # # 
# # # # # . # # # # # # # # # . . . . . . . . # # # # # # # 
# # # # # . # # # # # # # # # . . . . . . . . # # # # # # # 
# # # # # . # # # # # # # # # . . . . . . . . # # # # # # # 
# # # # # . # # # # # # # # # . # # # . # # # # # # # # # # 
# # # # # . # # # # # # # # # . # # # . # # # # # # # # # # 
# # # # # . # # # # # # # # # . # # # . # # # # # # # # # # 
# # # # # . # # # # # # # # # . # # # . # # # # # # # # # # 
# # # # # . # # # # # # # # # . # # # . # # # # # # # # # # 
# # # . . . . . # # # # # # # . # # # . . . . . . # # # # # 
# # # . . . . . # # # # # # # . # # # # # # # # . # # # # # 
# # # . . . . M # # # # # # # . # # # # # # # # . # # # # # 
# # # . . . . . # # # # # # # . # # # # # # # # . # # # # # 
# # # @ . . . . # # # # # # # . # # # # # # # # . # # # # # 
# # # M . . . . # # # # # # # . # # # # # # # # . # # # # # 
# # # # # . # # # # # # # # # . # # # # # # # # . # # # # # 
# # # # # . # # # # # # # # # . # # # # # # # # . # # # # # 
# # # # # . # # # # # # # # # . # # # . . . . . . . . . . . 
# # # # # . . . . . . # # # # . # # # . . . . . . . . . . . 
# # # # # # . . . . . . . . . . # # # . . . . . . . . . . . 
# # # # # # . . . . . . . . . . # # # . . . . . . . . . . . 
# # # # # # . . . . . . . . . . # # # . . . . . . . . . . . 
# # # # # # . . . . . . . . . . # # # . . . . . . . . . . . 
# # # # # # . . . . . . . . . # # # # . . . . . . . . . . . 
# # # # # # . . . . . . . . . # # # # . . . . . . . . . . . 
# # # # # # . . . . . . . . . # # # # . . . . . . . . . . . 
# # # # # # # # # # . # # # # # # # # . . . . . . . . . . . 
# # # # # # # # # # . # # # # # # # # . . . . . . . . . . . 
Enter command: /getTestItem
# # # # # . # # # # # # # # # . . . . . . . . # # # # # . . 
# # # # # . # # # # # # # # # . . . . . . . . # # # # # # # 
# # # # # . # # # # # # # # # . . . . . . . . # # # # # # # 
# # # # # . # # # # # # # # # . . . . . . . . # # # # # # # 
# # # # # . # # # # # # # # # . . . . . . . . # # # # # # # 
# # # # # . # # # # # # # # # . . . . . . . . # # # # # # # 
# # # # # . # # # # # # # # # . # # # . # # # # # # # # # # 
# # # # # . # # # # # # # # # . # # # . # # # # # # # # # # 
# # # # # . # # # # # # # # # . # # # . # # # # # # # # # # 
# # # # # . # # # # # # # # # . # # # . # # # # # # # # # # 
# # # # # . # # # # # # # # # . # # # . # # # # # # # # # # 
# # # . . . . . # # # # # # # . # # # . . . . . . # # # # # 
# # # . . . . . # # # # # # # . # # # # # # # # . # # # # # 
# # # . . . . M # # # # # # # . # # # # # # # # . # # # # # 
# # # . . . . . # # # # # # # . # # # # # # # # . # # # # # 
# # # @ . . . . # # # # # # # . # # # # # # # # . # # # # # 
# # # M . . . . # # # # # # # . # # # # # # # # . # # # # # 
# # # # # . # # # # # # # # # . # # # # # # # # . # # # # # 
# # # # # . # # # # # # # # # . # # # # # # # # . # # # # # 
# # # # # . # # # # # # # # # . # # # . . . . . . . . . . . 
# # # # # . . . . . . # # # # . # # # . . . . . . . . . . . 
# # # # # # . . . . . . . . . . # # # . . . . . . . . . . . 
# # # # # # . . . . . . . . . . # # # . . . . . . . . . . . 
# # # # # # . . . . . . . . . . # # # . . . . . . . . . . . 
# # # # # # . . . . . . . . . . # # # . . . . . . . . . . . 
# # # # # # . . . . . . . . . # # # # . . . . . . . . . . . 
# # # # # # . . . . . . . . . # # # # . . . . . . . . . . . 
# # # # # # . . . . . . . . . # # # # . . . . . . . . . . . 
# # # # # # # # # # . # # # # # # # # . . . . . . . . . . . 
# # # # # # # # # # . # # # # # # # # . . . . . . . . . . . 
Enter command: /inv
-----------Inventory-----------
Inventory (3/32):
1. Small Potion x3
   Heals +20 health to the player
2. Iron Sword x1
   A basic iron sword that increases attack power
3. Iron Armor x1
   Heavy armor that greatly increases defense
-------------------------------
-----------Equipment-----------
-------------------------------
# # # # # . # # # # # # # # # . . . . . . . . # # # # # . . 
# # # # # . # # # # # # # # # . . . . . . . . # # # # # # # 
# # # # # . # # # # # # # # # . . . . . . . . # # # # # # # 
# # # # # . # # # # # # # # # . . . . . . . . # # # # # # # 
# # # # # . # # # # # # # # # . . . . . . . . # # # # # # # 
# # # # # . # # # # # # # # # . . . . . . . . # # # # # # # 
# # # # # . # # # # # # # # # . # # # . # # # # # # # # # # 
# # # # # . # # # # # # # # # . # # # . # # # # # # # # # # 
# # # # # . # # # # # # # # # . # # # . # # # # # # # # # # 
# # # # # . # # # # # # # # # . # # # . # # # # # # # # # # 
# # # # # . # # # # # # # # # . # # # . # # # # # # # # # # 
# # # . . . . . # # # # # # # . # # # . . . . . . # # # # # 
# # # . . . . . # # # # # # # . # # # # # # # # . # # # # # 
# # # . . . . M # # # # # # # . # # # # # # # # . # # # # # 
# # # . . . . . # # # # # # # . # # # # # # # # . # # # # # 
# # # @ . . . . # # # # # # # . # # # # # # # # . # # # # # 
# # # M . . . . # # # # # # # . # # # # # # # # . # # # # # 
# # # # # . # # # # # # # # # . # # # # # # # # . # # # # # 
# # # # # . # # # # # # # # # . # # # # # # # # . # # # # # 
# # # # # . # # # # # # # # # . # # # . . . . . . . . . . . 
# # # # # . . . . . . # # # # . # # # . . . . . . . . . . . 
# # # # # # . . . . . . . . . . # # # . . . . . . . . . . . 
# # # # # # . . . . . . . . . . # # # . . . . . . . . . . . 
# # # # # # . . . . . . . . . . # # # . . . . . . . . . . . 
# # # # # # . . . . . . . . . . # # # . . . . . . . . . . . 
# # # # # # . . . . . . . . . # # # # . . . . . . . . . . . 
# # # # # # . . . . . . . . . # # # # . . . . . . . . . . . 
# # # # # # . . . . . . . . . # # # # . . . . . . . . . . . 
# # # # # # # # # # . # # # # # # # # . . . . . . . . . . . 
# # # # # # # # # # . # # # # # # # # . . . . . . . . . . . 
Enter command: /use
Enter item slot: 2

맵을 그냥 매번 출력해버리니 영 불편하다.

 

맵을 매번 출력하는 게 아니라, 인벤토리나 아이템 장착, 사용 등 커맨드를 입력했을 때는 맵을 출력하지 않고, 게임을 실제로 '진행' 할 때만 맵을 출력하도록 해 보는 게 목표이다.

이를 위해 매번 if문을 돌리고 싶지는 않으니, 상태 머신 개념을 도입해본다.

 

1. 게임 상태 머신

그림을 잘 그려보자.

오 수업 때 FSM 배웠던 기억이 새록새록 난다.

이렇게 써먹는구나

그림으로 다 나타냈으니 구현은 어렵지 않다.

enum class GameState {
  MAIN_MENU,
  PLAYING,
  PAUSED,
  INVENTORY,
  GAME_OVER
};

상태들을 선언해주고

void Game::update() {
  switch(state) {
    case GameState::MAIN_MENU:
      std::cout << "Main Menu\n";
      std::cout << "1. /start\n";
      std::cout << "2. /quit\n";
      updateMainMenu();
      break;
    case GameState::PLAYING:
      render();
      std::cout << "Playing\n";
      std::cout << "1. /menu\n";
      std::cout << "2. /quit\n";
      std::cout << "3. wasd to move\n";
      updatePlaying();
      break;
    case GameState::PAUSED:
      std::cout << "Paused\n";
      std::cout << "1. /resume\n";
      std::cout << "2. /inv\n";
      std::cout << "3. /stat\n";
      std::cout << "4. /quit\n";
      updatePaused();
      break;
    case GameState::INVENTORY:
      std::cout << "Inventory\n";
      player->printInventory();
      std::cout << "1. /use\n";
      std::cout << "2. /equip\n";
      std::cout << "3. /unequip\n";
      std::cout << "4. /back\n";
      updateInventory();
      break;
    case GameState::GAME_OVER:
      std::cout << "Game Over\n";
      std::cout << "1. /restart\n";
      std::cout << "2. /quit\n";
      updateGameOver();
      break;
  }
}

각 상태별로 가능한 커맨드들을 플레이어한테 출력해준다.

그리고 각 update상태()에서는 커맨드별로 로직을 수행하고 상태를 전이시킨다.

예를 들자면 이런 느낌

void Game::updatePlaying() {
  std::string command;
  std::cout << "Enter command: ";
  std::cin >> command;

  if (command.length() > 1) {
    if (command == "/menu") {
      state = GameState::PAUSED;
      return;
    }
    if (command == "/quit") {
      exit(0);
    }
    std::cout << "Invalid command.\n";
    return;
  }
  switch (command[0]) {
  case 's':
    world->tryMoveEntity(player, 0, 1);
    break;
  case 'd':
    world->tryMoveEntity(player, 1, 0);
    break;
  case 'w':
    world->tryMoveEntity(player, 0, -1);
    break;
  case 'a':
    world->tryMoveEntity(player, -1, 0);
    break;
  default:
    std::cout << "Invalid command.\n";
    break;
  }
  if (world->isPlayerDead()) {
    std::cout << "You died!\n";
    state = GameState::GAME_OVER;
    return;
  }
  camera->follow(player->getX(), player->getY(), world->getCurrentMap()->getWidth(), world->getCurrentMap()->getHeight());
}

 

그러면 지저분했던 Game.run()은 이렇게 바뀐다.

void Game::run() {
  while (true) {
    update();
  }
}

 

테스트하니 의도대로 잘 동작한다.

 

2. 느낀점

느낀 점이 있다면 상태 머신은 그림만 잘 그려놓으면 구현은 직관적이고 쉽다는 것.

반응형