Глава 21 · Проект: космический шутер

Как появляются враги

Таймер появления в секундах — с тем же while-накопителем, что и таймер анимации в главе 20.

В разделе 21.3 мы уже измеряли время до следующего врага в секундах: таймер уменьшался на dt каждый кадр, а при срабатывании прибавлял интервал заново, а не присваивал его — так остаток не терялся. Теперь перенесём тот же принцип в финальную архитектуру игры и сделаем таймер пригодным для интервала, который меняется вместе со сложностью.

fragment_spawn_timer.py
self.spawn_timer -= dt
while self.spawn_timer <= 0.0:
    self._spawn_enemy()
    self.spawn_timer += interval_poyavleniya_vraga(self.score)
Два независимых механизма, а не один
self.spawn_timer += interval_poyavleniya_vraga(self.score) — то, что нужно на каждом кадре: остаток времени сверх интервала не выбрасывается, а переносится на следующего врага, точно так же, как в разделе 21.3 и как таймер анимации в разделе 20.23. Это сохранение остатка работает и с обычным if — оно не требует while. Сам while нужен для другого: если бы один обрабатываемый шаг длился дольше сразу нескольких интервалов появления врагов, он честно создал бы несколько врагов подряд за один вызов, а if продвинул бы появление только одного врага и молча потерял остальное накопленное время.
В этой конкретной игре второй случай пока не наступает
Финальная версия игры ограничивает dt значением MAX_DT = 0.05 секунды перед каждым обновлением (защита от скачка после паузы отладчика или зависания ОС), а нижняя граница интервала появления врагов — MIN_SPAWN_INTERVAL = 0.35 секунды: даже самый долгий допустимый кадр короче любого возможного интервала между врагами. Значит, тело while в этой игре сейчас никогда не выполняется больше одного раза за кадр — и написанный через if код вёл бы себя здесь ровно так же. while оставлен как более общий и надёжный приём: он не сломается, если позже поднять MAX_DT или уменьшить интервал появления врагов сильнее, чем сейчас, а if в этом случае пришлось бы менять.

Где именно появляется враг

Координата X должна гарантировать, что враг полностью помещается внутри игрового поля — без частичного «обрезания» по краю:

fragment_spawn_x.py
def x_poyavleniya_vraga(rng, shirina_vraga, pole):
    levaya, pravaya = pole.left, pole.right - shirina_vraga
    if pravaya <= levaya:
        return float(levaya)
    return rng.uniform(levaya, pravaya)

Это отдельная, чистая функция — принимает rng явным аргументом (раздел 21.24 объясняет, зачем), а не обращается к глобальному random напрямую, поэтому её удобно тестировать без запуска всей игры.

Два типа врагов

Реальное окно: несколько врагов разных размеров и цветов спускаются по игровому полю группой
Реальное окно — волна врагов: маленькие быстрые разведчики и более крупные истребители появляются вперемешку.
Scout (разведчик)Fighter (истребитель)
СкоростьВышеНиже
РазмерМеньшеБольше
Очки за уничтожение100200
Вероятность появленияВыше в начале игрыРастёт вместе со счётом (раздел 21.17)
fragment_enemy_spec.py
@dataclass(frozen=True)
class EnemySpec:
    name: str
    base_speed: float
    points: int
    image_key: str

ENEMY_SPECS = {
    "scout": EnemySpec("scout", base_speed=150.0, points=100, image_key="enemy_scout"),
    "fighter": EnemySpec("fighter", base_speed=85.0, points=200, image_key="enemy_fighter"),
}
EnemySpec описывает тип, а не отдельного врага
@dataclass из главы 14 автоматически создаёт конструктор для четырёх полей. Параметр frozen=True запрещает менять эти поля после создания объекта: общая спецификация scout или fighter остаётся неизменной, а координаты и текущая скорость хранятся уже в каждом экземпляре Enemy. Чтобы добавить тип врага в упражнении 21.25, создайте новый EnemySpec, а не изменяйте существующий.
Практика: таймер появления врагов
Проверяется прямо в браузере — установка Pygame не требуется
Практика выполняется локально
Открыть практику →