Python — Racing
Разработчику Начальный уровень
Материалы трека приводятся к единому формату: полные листинги для копирования на каждом этапе, блок "Разбор" и раздел "Полная ревизия" в конце статьи.
- Гарантированно запускаемые эталоны для сверки: Battle City, Match3, Ping Pong, Tetris, Racing (
#full-revision). - В этой статье полная ревизия (
#full-revision) покрывает этапы 0–14 — овал, круги, таймер, два соперника, меню, отсчёт, пауза и финиш. Этапы 15–16 (мини-карта, nitro) — по желанию.
Как проходить практикум
- Копируйте целиком файлы из блоков кода каждого этапа.
- После каждого этапа запускайте проект (команда указана в главе) и пройдите чек-лист самопроверки.
- Если застряли — методология в разделе Практикум разработки игр — о разделе; для сверки — полная ревизия в конце этой статьи и эталоны Battle City, Match3, Ping Pong.
О практикуме
Соберём аркадные гонки сверху (top-down) на Python 3 и Pygame — овальная трасса, машина с ускорением и поворотом, столкновения с бордюром, контрольные точки, круги, таймер заезда и простые соперники по waypoints. Графика — цветные фигуры (без внешних спрайтов), зато с полным разбором физики, коллизий и состояний гонки.
Жанр top-down racing — вид "с камеры над трассой", как в ранних Micro Machines или Hot Wheels. Альтернатива для отдельного проекта — вертикальный скроллер (машина внизу, дорога едет на игрока); здесь выбран овал, потому что он наглядно учит непрерывную физику, секторный подсчёт кругов и ИИ по точкам маршрута.
Нужны базовые Python (классы, списки, math) и знакомство с Pygame из статьи Разработка игр на Python. Каждый этап — запускаемый код: после шага проект можно запустить и увидеть новую механику.
Управление в финальной версии
| Клавиша | Действие |
|---|---|
W или ↑ |
Газ |
S или ↓ |
Тормоз / задний ход |
A или ← |
Поворот влево (при движении) |
D или → |
Поворот вправо (при движении) |
P |
Пауза |
R |
Перезапуск заезда |
Enter |
Старт из меню |
Shift |
Nitro (этап 16) |
F1 |
Режим отладки — секторы и waypoints |
Маршрут чтения
- Архитектура — как устроен проект до первой строки кода.
- Зависимости и структура папок — окружение и файлы.
- Этап 0 — минимальный запуск — окно и игровой цикл.
- Этапы 1–14 — базовый прототип, по одной механике за шаг.
- Полная ревизия файлов — эталон после этапа 14 для построчной сверки.
- Этапы 15–16 — позиция в гонке, nitro и полировка (опционально).
- Настройка "руления" — таблица параметров физики.
- Отладка на трассе — визуализация секторов и waypoints.
- Итоговая структура и самопроверка.
Содержание этапов
| № | Тема | Новая механика |
|---|---|---|
| 0 | Минимальный запуск | Окно, цикл, clock.tick |
| 1 | Конфиг и фон | config.py, палитра |
| 2 | Овальная трасса | Эллипсы, линия разметки |
| 3 | Машина игрока | Класс Car, поворот спрайта |
| 4 | Газ и трение | speed, FRICTION |
| 5 | Движение и поворот | move, steer |
| 6 | Бордюр | Track.clamp_car |
| 7 | Секторы | RaceProgress, круги |
| 8 | Таймер круга | perf_counter, best lap |
| 9 | Старт/финиш и сброс | Черта, клавиша R |
| 10 | ИИ соперник | Waypoints по овалу |
| 11 | Столкновения | Несколько машин, отталкивание |
| 12 | HUD | Скорость, круг, время |
| 13 | Состояния заезда | Меню, отсчёт, пауза |
| 14 | Класс Game |
Модули, тонкий main.py |
| 15 | Позиция и мини-карта | Место в гонке, radar |
| 16 | Nitro и следы шин | Буст, полировка |
Что должно получиться
| Механика | Описание |
|---|---|
| Трасса | Овальное кольцо — асфальт между внутренним и внешним эллипсом |
| Машина | Ускорение, трение, поворот зависит от скорости |
| Бордюр | Выезд за асфальт — отскок и потеря скорости |
| Круги | Четыре сектора-триггера; полный круг только при проходе по порядку |
| Таймер | Время текущего круга и лучший круг |
| Соперники | 1–3 машины по замкнутому маршруту waypoints |
| Заезд | Обратный отсчёт 3–2–1–GO, меню, пауза, финиш после N кругов |
| Позиция | Место в гонке по прогрессу круга и сектора |
| Nitro | Кратковременный буст по Shift с перезарядкой |
Сравнение подходов к гонкам в Pygame
| Подход | Камера | Сложность | Чему учит |
|---|---|---|---|
| Top-down (этот практикум) | Статичная сверху | Средняя | Угол, скорость, секторы, waypoints |
| Вертикальный скроллер | Дорога движется вниз | Ниже | Спавн препятствий, скорость мира |
| Псевдо-3D (OutRun) | Перспектива по линиям дороги | Выше | Проекция, сегменты трассы |
| Tilemap-трек | Сверху или изометрия | Средняя | Тайлы, A*, сетка |
Архитектура
Прежде чем писать код, зафиксируем из чего состоит гонка и как данные текут по кадру.
Игровой цикл
flowchart TD
A[Старт] --> B[Инициализация Pygame]
B --> C{running?}
C -->|да| D[Обработка событий]
D --> E[Обновление состояния]
E --> F[Отрисовка]
F --> G[clock.tick FPS]
G --> C
C -->|нет| H[pygame.quit]
На каждом кадре внутри обновления (когда состояние RACING):
- Прочитать удерживаемые клавиши (
get_pressed). - Применить газ/тормоз и трение к скорости игрока.
- Повернуть машину, если скорость выше порога.
- Сдвинуть позицию по углу и скорости.
- Проверить границы трассы — при выезде вернуть на асфальт и урезать скорость.
- Обновить прогресс по секторам и счётчик кругов.
- Обновить соперников по waypoints.
- Проверить столкновения машин (упрощённо — отталкивание).
- Обновить таймеры и проверить финиш.
Слои приложения
| Слой | Ответственность | Примеры |
|---|---|---|
| Ввод | Клавиатура, пауза, меню | KEYDOWN, get_pressed |
| Трасса | Геометрия, коллизии с бордюром | Track, эллипсы |
| Физика | Скорость, угол, трение | Car.update |
| Прогресс | Секторы, круги, финиш | RaceProgress |
| ИИ | Движение соперников | WaypointFollower |
| Правила | Состояния заезда, таймер | GameState |
| Представление | Трасса, машины, HUD | draw_track, draw_hud |
Слой правил меняет состояние; слой представления только читает его и рисует кадр.
Координатная система
Вид сверху, ось Y направлена вниз (как в Pygame). Угол машины — в градусах, 0° — вправо, рост угла — по часовой стрелке (стандарт pygame.transform.rotate).
Экран (пиксели)
┌────────────────────────────────────────┐
│ трава (фон) │
│ ╭────────────────────────╮ │
│ │ внутренний газон │ │
│ ╰────────────────────────╯ │
│ асфальт (кольцо) │
│ старт / финиш — нижняя дуга │
└────────────────────────────────────────┘
Рекомендуемые константы (все модули берут размеры из config.py):
| Константа | Значение | Смысл |
|---|---|---|
SCREEN_W, SCREEN_H |
960, 540 |
Окно 16:9 |
FPS |
60 |
Кадров в секунду |
TRACK_CX, TRACK_CY |
центр экрана | Центр овала |
OUTER_RX, OUTER_RY |
420, 220 |
Внешний эллипс |
INNER_RX, INNER_RY |
220, 110 |
Внутренний эллипс |
CAR_W, CAR_H |
44, 22 |
Габарит машины |
MAX_SPEED |
8.0 |
Пикселей за кадр |
TOTAL_LAPS |
3 |
Кругов до финиша |
Проверка "машина на асфальте" — через нормализованное расстояние до эллипса:
def ellipse_norm(x, y, cx, cy, rx, ry):
dx, dy = x - cx, y - cy
return (dx / rx) ** 2 + (dy / ry) ** 2
Точка на кольце, если inner_norm >= 1.0 и outer_norm <= 1.0 (с небольшим запасом EPS).
Модель физики машины
Машина — material point + heading — храним (x, y, angle, speed). Это упрощение без бокового сноса; для аркады его достаточно.
| Переменная | Единица | Роль |
|---|---|---|
speed |
px/кадр | Скalar скорости вдоль angle; отрицательная — задний ход |
angle |
градусы | Курс; 0° — вправо, 90° — вниз |
ACCELERATION |
px/кадр² | Прирост при газе |
FRICTION |
0…1 | Множитель каждый кадр; 0.96 ≈ лёгкое сопротивление |
Обновление за кадр (состояние RACING):
speed += gas * ACCELERATION - brake * BRAKE
speed *= FRICTION
speed = clamp(speed, MIN_SPEED, MAX_SPEED)
if |speed| > STEER_MIN_SPEED:
angle += turn_input * TURN_SPEED * sign(speed)
x += cos(radians(angle)) * speed
y += sin(radians(angle)) * speed
Поворот зависит от знака скорости — при заднем ходе руль "инвертируется", как у настоящего автомобиля.
В HUD скорость показываем как abs(speed) 10 — условные km/h для красоты. Реальная физика завязана на px/кадр и FPS; при смене FPS умножайте ускорение на dt 60, если переходите на обновление через dt.
Порядок обновления кадра
Порядок вызовов фиксирован — иначе круги и столкновения "дрожат":
| Шаг | Действие | Почему именно здесь |
|---|---|---|
| 1 | Ввод → apply_input, steer |
Решаем, куда едем |
| 2 | move |
Меняем позицию |
| 3 | track.clamp_car |
Не даём уехать с асфальта |
| 4 | progress.update |
Секторы после финальной позиции |
| 5 | ИИ соперников + clamp_car |
Соперники в тех же правилах |
| 6 | collide_with |
Разводим пересечения |
| 7 | Проверка finished |
Финиш после логики |
Секторы и круги
Трассу делим на 4 сектора по углу от центра (atan2). Игрок должен пройти секторы 0 → 1 → 2 → 3 → 0 подряд; только тогда засчитывается полный круг. Так нельзя "накрутить" круг, проехав туда-сюда на одном участке.
flowchart LR
S0["Сектор 0\n(справа)"] --> S1["Сектор 1\n(низ)"]
S1 --> S2["Сектор 2\n(слева)"]
S2 --> S3["Сектор 3\n(верх)"]
S3 --> S0
S0 -->|"после S3"| LAP["+1 круг"]
Схема секторов на овале (вид сверху, центр — TRACK_CX, TRACK_CY):
сектор 3 (верх)
│
сектор 2 ─────┼───── сектор 0 (право)
(лево) │
сектор 1 (низ, старт)
Угол считается через atan2(dy, dx) в градусах [0, 360); границы секторов — кратные 90°. Старт на нижней дуге попадает в сектор 1, поэтому RaceProgress инициализируется с next_sector = 2.
Конечный автомат состояний
stateDiagram-v2
[*] --> MENU
MENU --> COUNTDOWN : Enter
COUNTDOWN --> RACING : GO
RACING --> PAUSED : P
PAUSED --> RACING : P
RACING --> FINISHED : все круги пройдены
RACING --> FINISHED : время вышло (опционально)
FINISHED --> COUNTDOWN : R
MENU --> [*] : Esc
RACING --> [*] : Esc
Состояния MENU, COUNTDOWN, RACING, PAUSED, FINISHED — отдельные ветки в update() и draw().
Структура файлов (целевая)
До этапа 5 достаточно main.py и config.py. Дальше код раскладываем по модулям.
racing/
├── main.py # точка входа, цикл while
├── config.py # константы, цвета, FPS
├── assets/ # позже — звуки и спрайты
├── game/
│ ├── __init__.py
│ ├── car.py # Car — физика и отрисовка
│ ├── track.py # Track — эллипсы, коллизии, рисование
│ ├── progress.py # RaceProgress — секторы и круги
│ ├── ai.py # WaypointFollower для соперников
│ ├── hud.py # скорость, круг, таймер
│ └── states.py # Game — состояния заезда
└── requirements.txt
Диаграмма объектов
classDiagram
class Game {
+state
+player Car
+rivals Car[]
+track Track
+progress RaceProgress
+update()
+draw()
}
class Car {
+x y angle speed
+update(keys, track)
+draw(surface)
}
class Track {
+is_on_track(x, y)
+clamp_to_track(car)
+draw(surface)
}
class RaceProgress {
+sector lap
+on_car_moved(x, y)
}
Game --> Car
Game --> Track
Game --> RaceProgress
Car --> Track : коллизии
Для гонок удобна непрерывная траектория: скорость и угол меняются плавно. Овальное кольцо задаётся двумя эллипсами — минимум геометрии, максимум ощущения "трассы". Позже ту же архитектуру можно перенести на полигональную трассу из точек.
ИИ соперников — waypoints
Соперник не "знает" физику — каждый кадр тянется к следующей точке маршрута:
flowchart LR
W0[waypoint i] --> W1[waypoint i+1]
W1 --> W2[...]
W2 --> W0
Car[Car x,y] -->|вектор к W[i]| W0
Точки строятся по параметрическому овалу (cos/sin с радиусом между inner и outer). Когда расстояние до точки < порога — индекс увеличивается. Скорость ИИ константа; разнообразие — разный speed и сдвиг стартового index, чтобы машины не ехали в хвост.
Словарь терминов
| Термин | Значение в проекте |
|---|---|
| Waypoint | Точка (x, y) на маршруте; цель для ИИ |
| Сектор | Четверть овала по углу; триггер прогресса |
| Круг (lap) | Полный проход секторов 0→1→2→3→0 |
| Clamp | Возврат машины на допустимый эллипс |
| HUD | Наложенный UI — скорость, круг, таймер |
| dt | Длительность кадра в секундах (tick / 1000) |
Зависимости и подготовка окружения
Требования
- Python 3.10+
- Pygame 2.5+ — единственная внешняя библиотека
Установка
mkdir racing && cd racing
python -m venv .venv
Активация виртуального окружения:
- Windows (PowerShell):
.venv\Scripts\Activate.ps1 - Linux / macOS:
source .venv/bin/activate
pip install pygame
python -c "import pygame; print('Pygame', pygame.version.ver)"
Файл requirements.txt:
pygame>=2.5.0
Первичная структура
На этапе 0 создайте только main.py. Файл config.py появится на этапе 1, папку game/ — ближе к финалу.
Если Pygame не ставится
| Сообщение | Что сделать |
|---|---|
Microsoft Visual C++ 14.0 is required (старые версии) |
Обновите pip и Pygame до 2.5+, либо поставьте Build Tools |
No module named 'pygame' |
Активируйте .venv — в приглашении должно быть (.venv) |
| Окно сразу закрывается | Запускайте из терминала python main.py, читайте traceback |
| Чёрный экран на Linux/WSL | Нужен графический сервер (WSLg или X11); в headless CI Pygame не откроет окно |
Создайте папку racing/, копируйте код после каждого этапа, запускайте python main.py. Если что-то сломалось — сверьтесь с блоком "Самопроверка" в конце этапа.
Этап 0 — минимальный запускаемый код
Цель — окно, цикл событий, выход по крестику и Esc, стабильные 60 FPS.
Создайте main.py:
Запуск:
python main.py
Самопроверка этапа 0
- Окно открывается без traceback.
- Фон зелёный (трава), без мерцания.
-
Escи крестик закрывают программу.
Зачем clock.tick(FPS)
clock.tick(60) ограничивает частоту кадров и возвращает миллисекунды с прошлого кадра. Без него цикл крутится на максимальной скорости CPU — окно мерцает, а физика на слабом и мощном ПК ведёт себя по-разному. С этапа 13 используем dt = tick / 1000.0 для отсчёта 3–2–1.
На следующих этапах не удаляем цикл — только расширяем тело while.
Этап 1 — конфиг и фон
Цель — вынести настройки в config.py, зафиксировать палитру и размер окна.
config.py:
Обновите main.py:
Самопроверка
- Импорт
configбез ошибок. - Размер окна 960×540.
Этап 2 — овальная трасса
Цель — нарисовать кольцо асфальта между двумя эллипсами и стартовую разметку в шахматном стиле.
Добавьте в main.py функции отрисовки (позже перенесём в track.py):
В цикле вместо одного fill:
screen.fill(C.COLOR_GRASS)
draw_track(screen)
Самопроверка
- Видно серое кольцо и зелёный "остров" внутри.
- Трасса по центру экрана.
- На нижней дуге — бело-чёрная черта и жёлтая линия финиша.
Этап 3 — машина игрока (статичная)
Цель — класс Car, отрисовка повёрнутого прямоугольника, стартовая позиция на нижней дуге.
Добавьте в main.py (или создайте game/car.py и импортируйте):
Стартовая точка — снаружи внутреннего эллипса, снизу по центру:
start_x = C.TRACK_CX
start_y = C.TRACK_CY + (C.INNER_RY + C.OUTER_RY) // 2
player = Car(start_x, start_y, -90, (220, 50, 50))
В цикле после draw_track(screen):
player.draw(screen)
Самопроверка
- Красная машина стоит на асфальте внизу овала.
- "Лобовое стекло" смотрит по направлению движения (вверх при
angle = -90).
Этап 4 — газ, тормоз и трение
Цель — изменять speed по клавишам W/S, каждый кадр умножать скорость на коэффициент трения.
В config.py добавьте:
MAX_SPEED = 8.0
MIN_SPEED = -3.0
ACCELERATION = 0.18
BRAKE = 0.28
FRICTION = 0.96
В класс Car добавьте метод:
def apply_input(self, keys):
if keys[pygame.K_w] or keys[pygame.K_UP]:
self.speed += C.ACCELERATION
if keys[pygame.K_s] or keys[pygame.K_DOWN]:
self.speed -= C.BRAKE
self.speed *= C.FRICTION
self.speed = max(C.MIN_SPEED, min(C.MAX_SPEED, self.speed))
В цикле перед отрисовкой:
keys = pygame.key.get_pressed()
player.apply_input(keys)
Пока не двигаем (x, y) — только меняется скорость (на следующем этапе поедем).
Для отладки выведите скорость на экран (временно):
font = pygame.font.SysFont("consolas", 20)
# после apply_input:
txt = font.render(f"speed={player.speed:.2f}", True, (255, 255, 255))
screen.blit(txt, (12, 12))
Самопроверка
- При зажатом
Wв заголовке окна или временномprint(player.speed)скорость растёт доMAX_SPEED. - После отпускания клавиш скорость падает к нулю.
Этап 5 — движение и поворот
Цель — сдвиг по углу, поворот A/D только при достаточной скорости.
В config.py:
TURN_SPEED = 3.2
STEER_MIN_SPEED = 0.4
Методы Car:
def steer(self, keys):
if abs(self.speed) < C.STEER_MIN_SPEED:
return
direction = 1 if self.speed >= 0 else -1
if keys[pygame.K_a] or keys[pygame.K_LEFT]:
self.angle -= C.TURN_SPEED * direction
if keys[pygame.K_d] or keys[pygame.K_RIGHT]:
self.angle += C.TURN_SPEED * direction
def move(self):
rad = math.radians(self.angle)
self.x += math.cos(rad) * self.speed
self.y += math.sin(rad) * self.speed
В цикле:
player.apply_input(keys)
player.steer(keys)
player.move()
Самопроверка
- Машина ездит по овалу при удержании
Wи лёгкой коррекцииA/D. - На месте (
speed ≈ 0) поворот не работает.
Сводный main.py после этапа 5
Если вы собирали файл по частям, сверьте его с этим вариантом:
Скорость — скаляр (пикс/кадр), угол — курс в градусах. Каждый кадр: speed меняется от газа/тормоза и умножается на FRICTION; позиция сдвигается по формулам cos/sin. Это упрощённая модель "танка" — без бокового сноса; для аркады этого достаточно.
Этап 6 — коллизии с бордюром
Цель — не давать машине уехать на газон; при выезде — вернуть на асфальт и урезать скорость.
Создайте game/track.py:
Создайте пустой game/__init__.py. В main.py импортируйте Track, создайте track = Track() и в цикле после player.steer(keys) вызывайте player.move(), затем track.clamp_car(player); перед pygame.display.flip() — track.draw(screen) (вместо прежней draw_track в main.py).
Как работает clamp_car
Идея — проецировать точку на ближайший допустимый эллипс. Если outer_norm > 1, координаты сжимают к внешней границе; если outer_norm < 1 для внутреннего газона — выталкивают наружу от inner. Множитель 0.55 к скорости имитирует удар о бордюр.
В этом учебном прототипе угол при столкновении с бордюром не отражается — только позиция и скорость. Для реализма добавьте отражение компоненты скорости относительно нормали эллипса (домашнее задание в конце статьи).
Самопроверка
- При резком выезде машина "отскакивает" на асфальт.
- Скорость заметно падает после удара о бордюр.
Этап 7 — секторы трассы
Цель — определять, в каком секторе (0–3) находится машина, для будущего подсчёта кругов.
game/progress.py:
В config.py:
TOTAL_LAPS = 3
В main.py после clamp_car:
from game.progress import RaceProgress
progress = RaceProgress(player.x, player.y)
# в цикле:
progress.update(player.x, player.y)
Для отладки выведите сектор шрифтом:
font = pygame.font.SysFont("consolas", 20)
label = font.render(f"sector {progress.sector} lap {progress.lap}", True, (255, 255, 255))
screen.blit(label, (12, 12))
Самопроверка
- При полном обороте по часовой стрелке
lapувеличивается на 1. - Срезание через центр не даёт лишний круг без прохода всех секторов.
Частая причина — езда против часовой или пропуск сектора (срезали внутренний газон и "перепрыгнули" с 1 на 3). Смотрите отладку секторов по F1 в разделе Отладка на трассе.
Этап 8 — таймер круга и лучший круг
Цель — засекать время текущего круга и запоминать лучший результат.
Расширьте RaceProgress:
Функция форматирования в main.py или game/hud.py:
def format_time(seconds):
ms = int((seconds % 1) * 1000)
s = int(seconds) % 60
m = int(seconds) // 60
return f"{m:02d}:{s:02d}.{ms:03d}"
Самопроверка
- Таймер текущего круга растёт каждый кадр.
- После финиша сектора 3→0 время фиксируется как
last_lap_time.
Этап 9 — линия старта/финиша и сброс заезда
Цель — нарисовать стартовую черту; по R сбрасывать машину и прогресс.
В Track.draw добавьте черту на нижней дуге:
finish_x = C.TRACK_CX
finish_y = C.TRACK_CY + (C.INNER_RY + C.OUTER_RY) // 2
pygame.draw.line(
surface, (255, 255, 0),
(finish_x - 30, finish_y),
(finish_x + 30, finish_y), 4,
)
Функция сброса в main.py:
def reset_race():
player.x = C.TRACK_CX
player.y = C.TRACK_CY + (C.INNER_RY + C.OUTER_RY) // 2
player.angle = -90
player.speed = 0
progress.__init__(player.x, player.y)
Обработка KEYDOWN:
elif event.type == pygame.KEYDOWN and event.key == pygame.K_r:
reset_race()
Самопроверка
- Жёлтая линия видна на старте.
-
Rвозвращает машину на линию и обнуляет круги.
Этап 10 — соперник по waypoints
Цель — одна машина ИИ, едущая по заранее заданным точкам по овалу.
game/ai.py:
Look-ahead (опционально) — смотреть на точку дальше по маршруту, чтобы траектория была плавнее:
def _target_index(self, offset=3):
return (self.index + offset) % len(self.waypoints)
def update(self):
tx, ty = self.waypoints[self._target_index()]
# ... остальное как выше, tx/ty от look-ahead ...
В main.py:
from game.ai import build_oval_waypoints, WaypointFollower
from game.car import Car
rival = Car(C.TRACK_CX + 120, C.TRACK_CY, 180, (50, 120, 220))
ai = WaypointFollower(rival, build_oval_waypoints(), speed=4.2)
# в цикле:
ai.update()
rival.draw(screen)
Самопроверка
- Синяя машина стабильно кружит по овалу без выездов.
- Скорость соперника постоянная (можно менять в конструкторе).
Настройка ИИ
| Параметр | Эффект | Рекомендация |
|---|---|---|
speed |
Средняя скорость соперника | 3.8–4.8 px/кадр |
n в build_oval_waypoints(n) |
Гладкость траектории | 32–48 точек |
Порог dist < 16 |
Когда переключать waypoint | 12–20 px |
rival.index = i * 8 |
Разводка машин на старте | Сдвиг 6–10 точек |
Этап 11 — несколько соперников и простое столкновение
Цель — список соперников; при пересечении прямоугольников — лёгкое отталкивание.
В Car добавьте (метод rect() здесь не нужен — используем axis-aligned Rect по центру):
Создайте список:
rivals = [
WaypointFollower(Car(...), build_oval_waypoints(), speed=4.0),
WaypointFollower(Car(...), build_oval_waypoints(), speed=4.6),
]
После движения игрока проверяйте столкновения с каждым rival.car.
Самопроверка
- При таране машины разъезжаются.
- Скорость игрока падает после контакта.
Этап 12 — HUD (скорость, круг, таймер)
Цель — панель статуса поверх трассы с полупрозрачным фоном и подсказкой управления.
game/hud.py:
Параметры position и total_racers заполним на этапе 15.
Самопроверка
- HUD читается на фоне трассы.
- Номер круга совпадает с логикой
RaceProgress.
Этап 13 — меню, отсчёт и пауза
Цель — состояния MENU, COUNTDOWN, RACING, PAUSED, FINISHED.
game/states.py (скелет):
В main.py используйте dt = clock.tick(C.FPS) / 1000.0. Пока state != RACING, не вызывайте player.apply_input (или обнуляйте скорость).
Отрисовка оверлеев:
def draw_menu(surface, font):
title = font.render("Racing — Enter to start", True, (255, 255, 255))
surface.blit(title, (C.SCREEN_W // 2 - title.get_width() // 2, C.SCREEN_H // 2))
def draw_countdown(surface, font, value):
text = font.render(str(max(1, int(value + 0.99))), True, (255, 220, 0))
rect = text.get_rect(center=(C.SCREEN_W // 2, C.SCREEN_H // 2))
surface.blit(text, rect)
При progress.finished переключайте state = FINISHED и показывайте итоговое время.
Самопроверка
- Enter запускает 3–2–1, затем гонка.
-
Pставит паузу и снимает её. - После 3 кругов — экран финиша.
Этап 14 — класс Game и чистый main.py
Цель — собрать логику в Game, оставить в main.py только инициализацию и цикл.
Перенесите класс Car в game/car.py (добавьте import config as C и метод collide_with из этапа 11):
Пример финального main.py:
Класс Game внутри game/states.py (или game/game.py) хранит player, track, progress, rivals, hud, методы reset, update, draw.
Пример полной реализации Game (можно скопировать в game/states.py после переноса Car, Track, RaceProgress, HUD, WaypointFollower):
Финальный config.py (все константы в одном месте):
Этапы 15–16 добавляют позицию, nitro и следы. Полный список идей — в разделе Дальнейшее развитие в конце статьи.
Самопроверка этапа 14
-
main.pyкороче ~40 строк. - Поведение совпадает с этапом 13.
- Все модули в
game/импортируются без циклических ошибок.
Этап 15 — позиция в гонке и мини-карта
Цель — показывать место игрока (P1/3) и мини-схему овала с точками машин.
Подсчёт позиции
Сравниваем progress_score() игрока и каждого соперника. У соперника "виртуальный" прогресс — индекс waypoint, приведённый к той же шкале:
В Game.update после движения сохраняйте self.position = race_position(...).
Мини-карта (radar)
game/minimap.py:
В Game.draw после HUD: self.minimap.draw(surface, self.player, self.rivals).
Самопроверка
- При обгоне соперника цифра
P1,P2меняется. - На мини-карте видны все машины относительно овала.
Этап 16 — nitro и следы от шин
Цель — кратковременный буст по Shift и визуальные следы при заносе.
Nitro
В config.py:
NITRO_BOOST = 0.35
NITRO_MAX = 100.0
NITRO_DRAIN = 2.5
NITRO_RECHARGE = 0.8
MAX_SPEED_NITRO = 11.0
В Car:
В HUD добавьте полоску nitro — pygame.draw.rect шириной пропорционально player.nitro / NITRO_MAX.
Следы шин
Список последних позиций при высокой скорости и резком повороте:
В Game.update после steer вызывайте skids.add(..., turn_delta=...).
Самопроверка
-
Shift+Wдаёт заметный разгон; полоска nitro опустошается. - Без Shift nitro постепенно восстанавливается.
- На резких поворотах остаются тёмные следы.
Настройка "руления"
Все ощущения от вождения — в нескольких константах config.py. Меняйте по одной и перезапускайте игру.
| Константа | "Слишком" | Симптом | Куда крутить |
|---|---|---|---|
ACCELERATION |
высокая | Машина "стреляет" | ↓ до 0.12–0.15 |
FRICTION |
низкая (0.90) | Долгое скольжение | ↑ до 0.96–0.98 |
FRICTION |
высокая (0.99) | Тормозит слишком резко | ↓ |
TURN_SPEED |
высокая | Крутится на месте | ↓ до 2.5 |
MAX_SPEED |
высокая | Слетает с овала | ↓ или ужесточите clamp |
STEER_MIN_SPEED |
высокая | Не поворачивает на малой скорости | ↓ до 0.2 |
BRAKE |
низкая | Плохо тормозит | ↑ |
Пресеты
| Стиль | ACCEL | FRICTION | TURN | MAX_SPEED |
|---|---|---|---|---|
| Аркада (по умолчанию) | 0.18 | 0.96 | 3.2 | 8.0 |
| Симуляция-lite | 0.10 | 0.98 | 2.2 | 6.5 |
| Хардкор | 0.22 | 0.94 | 3.8 | 9.5 |
Отладка на трассе
Добавьте флаг DEBUG = False в config.py и переключение по F1.
game/debug_draw.py:
В Game.handle_event по F1 переключайте C.DEBUG. В draw при DEBUG вызывайте функции выше.
На время отладки кругов пишите print(sector, next_sector, lap) только при смене сектора — иначе консоль завалится за секунду.
Полная ревизия файлов
Эталонный проект после этапа 14 (проверен импортом from game.states import Game). Скопируйте дерево в папку racing/ и запустите python main.py — овальная трасса, круги, таймер, два соперника, меню, отсчёт 3–2–1, пауза и финиш после трёх кругов.
Дерево проекта
racing/
├── main.py
├── config.py
├── requirements.txt
└── game/
├── __init__.py
├── car.py
├── track.py
├── progress.py
├── ai.py
├── hud.py
└── states.py # class Game
requirements.txt
Разбор. Единственная внешняя зависимость — Pygame 2.5+.
pygame>=2.5.0
config.py
Разбор. Размеры окна, геометрия овала, физика машины, палитра и число кругов — все модули импортируют config as C.
main.py
Разбор. Точка входа — цикл событий, dt, вызовы Game.handle_event, update и draw; импорт from game.states import Game.
game/__init__.py
Разбор. Пустой файл — Python считает game пакетом; без него импорты из game.* не сработают.
game/car.py
Разбор. Физика игрока и соперников — газ, трение, поворот, сдвиг, отталкивание и отрисовка прямоугольника.
game/track.py
Разбор. Овальная трасса — нормализованные эллипсы, clamp_car, линия финиша и draw.
game/progress.py
Разбор. Секторы по atan2, счётчик кругов и таймеры круга (perf_counter).
game/ai.py
Разбор. Маршрут по waypoints на среднем радиусе овала; WaypointFollower двигает машину соперника.
game/hud.py
Разбор. Панель скорости, круга и таймеров поверх трассы; подсказка управления внизу экрана.
game/states.py
Разбор. Класс Game — FSM (State), сборка заезда, обновление кадра, оверлеи меню/отсчёта/паузы/финиша.
Разбор финальной архитектуры
- Track, ellipse clamp —
ellipse_normпроверяет кольцо между inner и outer;clamp_carпроецирует машину на границу и режет скорость при ударе о бордюр. - RaceProgress, sectors — четыре сектора по углу от центра; круг засчитывается только при проходе 0→1→2→3→0 подряд.
- WaypointFollower AI — соперники тянутся к точкам овала; разный
speedи сдвигindexразводят машины на старте. - State enum flow —
MENU→COUNTDOWN(Enter) →RACING→PAUSED(P) →FINISHEDпослеTOTAL_LAPS;Rперезапускает заезд. - HUD overlay — полупрозрачная панель со скоростью, кругом и таймерами; позиция
P{n}/3считается вrace_positionпоprogress_scoreи индексу waypoint.
Чек-лист эталона
-
Enterзапускает отсчёт 3–2–1, затем гонку. -
WASDведут машину, она остаётся на овале. - Счётчик круга растёт после прохода четырёх секторов по порядку.
- Два соперника стабильно едут по трассе.
-
P— пауза,R— перезапуск, финиш после трёх кругов.
Итоговая структура и самопроверка
Дерево проекта
racing/
├── main.py
├── config.py
├── requirements.txt
└── game/
├── __init__.py
├── car.py
├── track.py
├── progress.py
├── ai.py
├── hud.py
├── minimap.py # этап 15
├── debug_draw.py # отладка F1
└── states.py
Чек-лист готового прототипа (этап 14)
Сверьте поведение с полной ревизией или пройдите пункты ниже.
-
Enterзапускает отсчёт 3–2–1, затем гонку. -
WASDведут машину, она остаётся на овале. - Счётчик круга растёт после прохода четырёх секторов по порядку.
- Два соперника стабильно едут по трассе.
-
P— пауза,R— перезапуск, финиш после трёх кругов.
После этапов 15–16 дополнительно:
- HUD с позицией;
- мини-карта;
- nitro по
Shift; - следы при заносе.
Типичные ошибки
| Симптом | Причина | Решение |
|---|---|---|
| Машина "скользит" боком | Поворот при нулевой скорости | Проверьте STEER_MIN_SPEED |
| Круги не растут | Секторы проходятся не по порядку | Езжайте по периметру овала |
| ИИ уезжает с трассы | Слишком высокая speed у follower |
Уменьшите до 3.5–4.5 |
| Окно "мигает" | Забыли clock.tick |
Вызов в конце цикла |
ImportError: game |
Нет game/__init__.py |
Создайте пустой файл |
| Nitro не кончается | Нет NITRO_DRAIN в цикле |
Списывайте только при Shift |
| Позиция всегда P1 | race_position не вызывается |
Обновляйте после progress.update |
| Мини-карта пустая | Забыли Minimap.draw |
Вызов после HUD |
| Отладка не видна | DEBUG не переключается |
Обработайте K_F1 в handle_event |
Дальнейшее развитие
Идеи после прохождения 16 этапов — каждая опирается на уже готовую архитектуру.
| Идея | Сложность | С чего начать |
|---|---|---|
| Полигональная трасса из JSON | Средняя | Заменить ellipse_norm на "точка внутри полигона" |
| Два игрока на одной клавиатуре | Низкая | Второй Car, WASD + стрелки |
Звук мотора по speed |
Низкая | pygame.mixer, pitch через set_volume |
| Таблица рекордов | Низкая | best_lap_time в records.json |
| Отражение от бордюра | Средняя | Нормаль эллипса + отражение скорости |
| Камера следует за машиной | Средняя | Сдвиг всех draw на -camera_x, -camera_y |
| Штрафной таймер за off-track | Низкая | Счётчик кадров вне асфальта → +1 сек штрафа |
Два игрока — эскиз
player1 = Car(sx - 20, sy, -90, (220, 50, 50))
player2 = Car(sx + 20, sy, -90, (50, 220, 100))
# player1: WASD, player2: стрелки — разные ветки apply_input по keys
Запись лучшего круга
import json
from pathlib import Path
RECORDS = Path("records.json")
def save_best(name, seconds):
data = json.loads(RECORDS.read_text()) if RECORDS.exists() else {}
if name not in data or seconds < data[name]:
data[name] = seconds
RECORDS.write_text(json.dumps(data, indent=2))
Связанные материалы
- Разработка игр на Python — игровой цикл, события, спрайты.
- Battle City на GitHub — похожая поэтапная структура с коллизиями.
- Практикум разработки игр — о разделе — остальные треки.