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