Уровни и процедурная генерация
Бесконечно рисовать уровни руками невозможно. Процедурная генерация создаёт платформы, врагов и монетки по правилам — и каждый запуск становится новым.
Суть: уровень можно описать данными (сеткой или списком объектов) и строить его кодом. Процедурная генерация с случайностью даёт бесконечные непохожие уровни из простых правил.
Маленькую игру можно собрать, расставив платформы руками. Но как только хочется десять уровней или бесконечный раннер — рисовать каждый вручную нереально. Решение — описывать уровень данными и строить его кодом. Это отделяет «что в уровне» от «как он рисуется», и открывает дорогу к процедурной генерации.
Самый простой способ — текстовая карта. Уровень это список строк, где каждый символ что-то значит: # — стена, . — пусто, C — монетка, E — враг. Код пробегает по сетке и для каждого символа создаёт нужный спрайт в нужной клетке. Менять уровень теперь так же легко, как редактировать текст.
Следующий шаг — генерировать карту автоматически. С модулем random мы по правилам расставляем платформы и врагов: «платформа каждые 3-6 клеток», «на платформе с шансом 30% монетка». Каждый запуск даёт новый уровень. Это и есть процедурная генерация — то, что делает Minecraft и роглайки бесконечно реиграбельными, и снова это чистая логика без графики.
Как работает под капотом
Карта-сетка превращается в объекты: индекс символа даёт координату клетки, символ — тип спрайта. Координата = индекс × размер клетки:
текстовая карта -> объекты в мире "..C..." ряд 0 монетка в клетке (2,0) "..###." ряд 1 3 стены в ряду 1 "E....." ряд 2 враг в клетке (0,2) x_пикс = столбец * TILE y_пикс = ряд * TILE
Построение уровня из карты на pygame (читаем):
TILE = 40
level = ["..C...", "..###.", "E....."]
for row, line in enumerate(level):
for col, ch in enumerate(line):
x, y = col * TILE, row * TILE
if ch == "#": walls.add(Wall(x, y))
elif ch == "C": coins.add(Coin(x, y))
elif ch == "E": enemies.add(Enemy(x, y))И парсинг карты, и процедурную генерацию можно полностью проверить без графики. Сгенерируем простой уровень случайно и распарсим его в объекты. Попробуй сам:
import random
random.seed(42) # чтобы результат был воспроизводимым
def generate_level(width):
row = []
for col in range(width):
r = random.random()
if r < 0.15: row.append("#") # стена
elif r < 0.30: row.append("C") # монетка
elif r < 0.38: row.append("E") # враг
else: row.append(".") # пусто
return "".join(row)
TILE = 40
line = generate_level(12)
print("карта:", line)
for col, ch in enumerate(line):
if ch != ".":
kind = {"#":"стена","C":"монетка","E":"враг"}[ch]
print(f" {kind} в позиции x={col*TILE}")Сиды и честная случайность
Слово «процедурная» пугает, но за ним стоит простая идея: вместо готового уровня мы храним рецепт и зерно (seed) — число, задающее последовательность случайных значений. Один и тот же seed всегда даёт один и тот же уровень. Это даёт сразу два бонуса: при отладке баг воспроизводится, а игроки могут делиться «кодом уровня» (тем самым seed), как делают в Minecraft. Случайность, которой можно управлять, — мощный инструмент, а не хаос.
Но чистый рандом часто рождает несправедливые или скучные уровни: стена прямо на старте, непроходимая яма, монотонная пустота. Поэтому к генерации добавляют правила и проверки: гарантировать стартовую безопасную зону, не ставить две ямы подряд, держать долю стен в разумных пределах, а после генерации прогнать проверку проходимости. Получается «управляемая случайность» — каждый уровень новый, но всегда играбельный. Этот баланс между разнообразием и честностью — главное искусство процедурной генерации, и осваивается оно экспериментами с правилами, которые ты запускал выше.
Частые ошибки
- Жёстко прописывать координаты каждого объекта — невозможно поддерживать. Описывай уровень данными.
- Генерировать «непроходимые» уровни — слишком много стен или враг на старте. Добавляй правила-ограничения.
- Не фиксировать seed при отладке — баг исчезает при следующем запуске, не воспроизвести.
Best practices
- Описывай уровни данными (карты, списки) отдельно от кода отрисовки.
- При генерации задавай разумные правила и проверяй проходимость.
- Фиксируй
random.seedпри отладке, отпускай в релизе для разнообразия.
Итог: уровень — это данные, превращаемые в объекты кодом. Добавь случайность с правилами — и получишь бесконечные непохожие уровни.