Состояния игры: меню, пауза, конец

Игра — это не только геймплей, но и меню, пауза, экран поражения. Машина состояний наводит порядок в этих экранах.
Суть: игра живёт в одном из состояний — меню, игра, пауза, конец. Машина состояний хранит текущий экран и переключает их по событиям, не смешивая логику разных экранов.

Запусти любую игру: сначала меню, потом сам геймплей, по Esc — пауза, при проигрыше — экран Game Over с предложением сыграть снова. Это разные экраны, у каждого свой ввод и своя отрисовка. Если свалить всё в один игровой цикл с грудой if, код быстро превратится в нечитаемую кашу. Решение — машина состояний.

Идея простая: в любой момент игра находится ровно в одном состоянии. Заводим переменную state со значением вроде "menu", "playing", "paused", "game_over". В цикле смотрим на текущее состояние и делаем только то, что нужно ему: в меню рисуем кнопки и ждём «Старт», в игре крутим геймплей, в паузе показываем «Пауза» и не обновляем мир. Переходы между состояниями происходят по событиям: нажал «Старт» — state = "playing", проиграл — state = "game_over".

Так логика каждого экрана живёт отдельно и не мешает другим. Добавить новый экран (настройки, выбор уровня) — значит просто добавить ещё одно состояние. Это масштабируется и читается как описание игры.

Как работает под капотом

Состояние — это узел графа. События — это стрелки переходов между узлами:

        +--------+   Старт    +---------+
        |  MENU  |----------->| PLAYING |
        +--------+            +---------+
             ^                 |   ^  |
      Заново |          Esc(пауза) |  | урон, lives=0
             |                 v   |  v
        +-----------+      +--------+
        | GAME_OVER |<-----| PAUSED |
        +-----------+      +--------+

В pygame (читаем):

state = "menu"
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT: running = False
        if state == "menu" and event.type == pygame.KEYDOWN:
            if event.key == pygame.K_RETURN: state = "playing"
        elif state == "playing" and event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE: state = "paused"

    if state == "playing":
        update_world(dt)        # мир обновляется только в игре
    draw_screen(state)          # рисуем нужный экран

Саму машину состояний — какие переходы разрешены — можно описать и проверить без графики. Попробуй сам:

transitions = {
    ("menu", "start"): "playing",
    ("playing", "pause"): "paused",
    ("paused", "resume"): "playing",
    ("playing", "die"): "game_over",
    ("game_over", "restart"): "playing",
}

def step(state, action):
    return transitions.get((state, action), state)  # нет перехода -> остаёмся

state = "menu"
for action in ["start", "pause", "resume", "die", "restart", "die"]:
    new = step(state, action)
    print(f"{state:9} + {action:8} -> {new}")
    state = new

Сцены как классы

Когда экранов становится много, ветвление по строковой переменной state разрастается в длинные цепочки if. Следующий шаг зрелости — оформить каждый экран отдельным классом-сценой с методами handle_events, update и draw. Тогда главный цикл становится крошечным: он просто вызывает эти три метода у текущей сцены, кто бы она ни была. Меню, игра, пауза — каждая отвечает за себя целиком, и добавить новую сцену значит написать новый класс, не трогая остальные.

Этот подход называется паттерном «состояние» (State) и встречается во всех серьёзных движках. Он же решает вопрос перехода с передачей данных: уходя с экрана игры на экран Game Over, сцена может передать туда финальный счёт. Начинать стоит с простой строковой переменной, как в этом уроке, — её достаточно для маленькой игры. Но знать, куда расти, полезно: когда твоя игра обзаведётся настройками, выбором уровня и таблицей рекордов, сцены-классы спасут код от превращения в неуправляемый клубок условий.

Частые ошибки

  • Обновлять мир во всех состояниях — в паузе и меню враги продолжают двигаться.
  • Смешивать ввод разных экранов — Enter в игре вдруг делает то же, что в меню.
  • Забыть сбросить состояние при рестарте — новая игра начинается со старым счётом.

Best practices

  • Держи одну переменную state и ветви по ней и в обновлении, и в отрисовке.
  • Обновляй игровой мир только в состоянии "playing".
  • При старте новой игры явно сбрасывай счёт, жизни и позиции.

Итог: машина состояний — это карта экранов игры. Одна переменная state, чёткие переходы по событиям — и меню, пауза, геймовер перестают мешать друг другу.

Проверьте себя
1. Что описывает машина состояний игры?
AЦвета спрайтов
BВ каком экране сейчас игра (меню/игра/пауза/конец) и как между ними переходить
CСкорость врагов
DРазмер окна
2. В каком состоянии обычно обновляют игровой мир (двигают врагов)?
AВо всех сразу
BТолько в состоянии playing
CТолько в паузе
DТолько в меню