Собираем игру целиком

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

Пора увидеть лес за деревьями. За курс мы собрали много деталей: цикл, dt, спрайты, коллизии, гравитацию, звук, счёт, состояния, ИИ. Теперь посмотрим, как они складываются в одну работающую игру — простой аркадный собиратель монет с врагами. Это не новый материал, а карта, показывающая место каждого кусочка.

Структура такая. Сверху — машина состояний: меню, игра, конец. В состоянии "playing" крутится знакомый цикл: события → обновление → отрисовка. Обновление двигает героя по вводу, обновляет группу врагов (с их ИИ) и монеток, проверяет коллизии (собрал монету — плюс очки, коснулся врага — минус жизнь). Отрисовка заливает фон, рисует все группы и HUD со счётом. Звук играет на событиях.

Каждый объект — спрайт в своей группе. Герой управляется клавишами и вектором скорости, враги используют простой ИИ из прошлого урока, монетки просто ждут сбора. Всё это знакомо — мы лишь соединяем провода.

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

Вот карта всей игры: состояние сверху, цикл внутри, группы и подсистемы под ним:

   [МАШИНА СОСТОЯНИЙ]  menu / playing / game_over
           |
           v  (если playing)
   +-------------------------------------------+
   |  ИГРОВОЙ ЦИКЛ                              |
   |  события -> обновление -> отрисовка -> flip|
   +-------------------------------------------+
       |            |               |
       v            v               v
   ввод игрока   группы:         фон + группы
   (клавиши)     player          + HUD (счёт)
                 enemies (ИИ)
                 coins
                 коллизии -> счёт, жизни

Скелет главного файла (читаем):

def update_world(dt):
    player.update(dt)
    enemies.update(dt, player.rect.center)   # ИИ врагов
    got = pygame.sprite.spritecollide(player, coins, True)
    state["score"] += len(got) * 10
    if pygame.sprite.spritecollide(player, enemies, False):
        state["lives"] -= 1

while running:
    dt = clock.tick(60) / 1000.0
    handle_events()
    if game_state == "playing":
        update_world(dt)
    draw(game_state)
    pygame.display.flip()

Соберём «тик» всей игры одним прогоном без графики: обработаем кадр, в котором герой собрал монетку и задел врага. Попробуй сам:

def game_tick(state, events):
    if state["lives"] <= 0:
        state["over"] = True
        return state
    state["score"] += events.get("coins", 0) * 10
    state["lives"] -= events.get("enemy_hits", 0)
    if state["lives"] <= 0:
        state["over"] = True
    return state

state = {"score": 0, "lives": 3, "over": False}
frames = [
    {"coins": 2, "enemy_hits": 0},
    {"coins": 1, "enemy_hits": 1},
    {"coins": 0, "enemy_hits": 0},
]
for i, ev in enumerate(frames, 1):
    state = game_tick(state, ev)
    print(f"кадр {i}: счёт={state['score']} жизни={state['lives']} конец={state['over']}")

Раскладка файлов растущего проекта

Пока игра помещается в один файл — это нормально. Но как только появятся классы игрока, врагов, монеток, сцен и настроек, держать всё в одном main.py станет тяжело. Зрелая раскладка разносит код по смыслу: main.py с игровым циклом, player.py, enemy.py, settings.py с константами, папка assets/ для картинок и звуков. Каждый файл отвечает за свою часть, и найти нужное становится легко даже спустя месяц.

Особенно полезен отдельный settings.py, куда вынесены все «магические числа»: размер окна, скорости, гравитация, цвета, пути к ассетам. Когда все настройки в одном месте, балансировать игру — одно удовольствие: подкрутил пару чисел, и не нужно искать их по всему коду. Это та же идея, что и с константами WIDTH и HEIGHT из первых уроков, только в масштабе всего проекта. Аккуратная структура — не бюрократия, а забота о себе будущем: именно она решает, доведёшь ли ты игру до конца или утонешь в собственном коде на половине пути.

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

  • Свалить всё в один гигантский файл-цикл — раздели на функции: события, обновление, отрисовка.
  • Обновлять мир вне "playing" — враги двигаются в меню.
  • Грузить ассеты в цикле — грузи всё при старте.

Best practices

  • Раздели код на функции handle_events, update_world, draw.
  • Держи объекты в группах по смыслу, коллизии — через spritecollide.
  • Состояние игры держи в одном словаре или классе — легче рестартить.

Итог: готовая игра — это состояния сверху, цикл внутри, спрайт-группы и подсистемы под ним. Все уроки курса встают на свои места в этой структуре.

Проверьте себя
1. Какая структура связывает все части готовой игры?
AОдин большой if
BМашина состояний сверху, игровой цикл внутри, спрайт-группы и подсистемы под ним
CТолько список картинок
DТолько звуковой микшер
2. Как лучше организовать код большой игры?
AВсё в одном цикле без функций
BРазделить на функции handle_events, update_world, draw
CВсё в обработчике QUIT
DКаждый кадр грузить ассеты