AI-процедурна генерація рівнів для ігор
Ваш ігровий данжон генерується з нелогічними переходами, а крива складності скаче? Гравці застрягають у глухих кутах, а рідкісні проходи ведуть у порожні кімнати. Ми вирішуємо ці проблеми за допомогою AI-процедурної генерації рівнів (PCG-levels) — алгоритмічного створення ігрових просторів: данжонів, платформених рівнів, відкритих світів, головоломок. AI-підходи додають семантичне розуміння: система знає, де має бути перший контакт із ворогом, де захований секрет, яка оптимальна крива складності. Маємо понад 5 років досвіду та реалізували більше 15 проектів для інді-студій і великих видавців.
Одна з типових ситуацій: замовник хоче 100 рівнів для roguelike, але ручна розробка кожного займає 2 дні. Без автоматизації бюджет на дизайн зростає до значної суми. З нашою AI-системою генерація одного рівня займає 2 секунди, а підсумкова економія часу — 85%. Нейромережі для геймдизайну тут працюють як фільтр якості: LLM оцінює кожен згенерований рівень за 10 метриками та відбраковує 30% невдалих варіантів.
Чому стандартна генерація рівнів перестає працювати?
Ручне проєктування тисяч рівнів — дорого й довго. Хаотична рандомна генерація дає незбалансований досвід: гравці або нудьгують, або помирають на кожному кроці. Проблема ускладнюється, коли потрібно підтримувати інтерес протягом 50+ годин геймплею. AI-генерація вирішує це через:
- Контрольовану складність — параметр difficulty впливає на щільність ворогів, пасток і нагород.
- Детермінованість за seed — один seed дає ідентичний рівень, що важливо для реплеїв і тестування.
- Семантичну оцінку — LLM аналізує карту як геймдизайнер, виявляючи проблеми потоку та темпу.
Ми комбінуємо класичні алгоритми (BSP, WFC) з машинним навчанням, щоб отримати найкраще від обох світів: швидкість і передбачуваність BSP + гнучкість і адаптивність ML.
Як AI-генерація рівнів вирішує проблему одноманітності?
Одноманітність — головний ворог процедурної генерації. Гравці швидко помічають патерни, якщо алгоритм не вміє варіювати структуру та наповнення. Наш підхід включає три рівні адаптації:
- Варіативність структур — BSP-розбиття з різною глибиною та співвідношенням кімнат/коридорів. WFC використовує зразки з реальних рівнів-еталонів.
- Інтерактивне наповнення — на основі difficulty розставляються пастки, скрині, боси. LLM коригує розміщення, щоб уникнути кластеризації.
- Тематичні біоми — набір параметрів (текстури, тайли, вороги) змінюється залежно від біому: класичний данжон, підземелля, руїни.
В одному з кейсів ми генерували рівні для roguelike-гри: після впровадження AI-оцінки показник exploration_score зріс із 4.2 до 8.7, а retention гравців на 15-й годині збільшився на 23%. Це дозволило скоротити бюджет на левел-дизайн на 40% — зазначив головний геймдизайнер студії.
Алгоритми генерації данжонів
BSP-генератор
from dataclasses import dataclass, field
from enum import Enum
import random
import numpy as np
from collections import deque
class TileType(Enum):
WALL = 0
FLOOR = 1
DOOR = 2
CHEST = 3
SPAWN = 4
EXIT = 5
TRAP = 6
BOSS_ROOM = 7
@dataclass
class DungeonConfig:
width: int = 64
height: int = 64
min_rooms: int = 8
max_rooms: int = 15
min_room_size: tuple = (4, 4)
max_room_size: tuple = (12, 10)
corridor_width: int = 1
difficulty: float = 0.5 # 0.0 – 1.0, впливає на ворогів і пастки
dungeon_type: str = "classic" # classic, cave, maze, ruins
class BSPDungeonGenerator:
"""Binary Space Partitioning — класичний алгоритм для данжонів"""
def __init__(self, config: DungeonConfig):
self.config = config
self.grid = np.full((config.height, config.width), TileType.WALL.value)
self.rooms = []
self.rng = random.Random()
def generate(self, seed: int = None) -> np.ndarray:
if seed:
self.rng.seed(seed)
np.random.seed(seed)
# 1. BSP-розбиття
root = {"x": 1, "y": 1, "w": self.config.width - 2, "h": self.config.height - 2}
leaves = self._split_bsp(root, depth=0, max_depth=4)
# 2. Створення кімнат у листках
for leaf in leaves:
room = self._create_room_in_leaf(leaf)
if room:
self.rooms.append(room)
self._carve_room(room)
# 3. З'єднання кімнат коридорами
for i in range(len(self.rooms) - 1):
self._connect_rooms(self.rooms[i], self.rooms[i + 1])
# 4. Розміщення спеціальних тайлів
self._place_spawn_and_exit()
self._place_interactive_elements()
return self.grid
def _split_bsp(self, node: dict, depth: int, max_depth: int) -> list:
if depth >= max_depth or (node["w"] < 14 and node["h"] < 14):
return [node]
split_horizontal = self.rng.random() > 0.5
if node["w"] > node["h"] * 1.25:
split_horizontal = False
elif node["h"] > node["w"] * 1.25:
split_horizontal = True
leaves = []
if split_horizontal:
split_pos = self.rng.randint(node["y"] + 6, node["y"] + node["h"] - 6)
child_a = {"x": node["x"], "y": node["y"], "w": node["w"], "h": split_pos - node["y"]}
child_b = {"x": node["x"], "y": split_pos, "w": node["w"], "h": node["y"] + node["h"] - split_pos}
else:
split_pos = self.rng.randint(node["x"] + 6, node["x"] + node["w"] - 6)
child_a = {"x": node["x"], "y": node["y"], "w": split_pos - node["x"], "h": node["h"]}
child_b = {"x": split_pos, "y": node["y"], "w": node["x"] + node["w"] - split_pos, "h": node["h"]}
leaves.extend(self._split_bsp(child_a, depth + 1, max_depth))
leaves.extend(self._split_bsp(child_b, depth + 1, max_depth))
return leaves
def _create_room_in_leaf(self, leaf: dict) -> dict | None:
max_w = min(self.config.max_room_size[0], leaf["w"] - 2)
max_h = min(self.config.max_room_size[1], leaf["h"] - 2)
if max_w < self.config.min_room_size[0] or max_h < self.config.min_room_size[1]:
return None
w = self.rng.randint(self.config.min_room_size[0], max_w)
h = self.rng.randint(self.config.min_room_size[1], max_h)
x = leaf["x"] + self.rng.randint(1, leaf["w"] - w - 1)
y = leaf["y"] + self.rng.randint(1, leaf["h"] - h - 1)
return {"x": x, "y": y, "w": w, "h": h}
def _carve_room(self, room: dict) -> None:
for y in range(room["y"], room["y"] + room["h"]):
for x in range(room["x"], room["x"] + room["w"]):
self.grid[y][x] = TileType.FLOOR.value
def _connect_rooms(self, room_a: dict, room_b: dict) -> None:
"""L-подібний коридор між центрами кімнат"""
cx_a = room_a["x"] + room_a["w"] // 2
cy_a = room_a["y"] + room_a["h"] // 2
cx_b = room_b["x"] + room_b["w"] // 2
cy_b = room_b["y"] + room_b["h"] // 2
if self.rng.random() > 0.5:
self._carve_horizontal(cy_a, min(cx_a, cx_b), max(cx_a, cx_b))
self._carve_vertical(cx_b, min(cy_a, cy_b), max(cy_a, cy_b))
else:
self._carve_vertical(cx_a, min(cy_a, cy_b), max(cy_a, cy_b))
self._carve_horizontal(cy_b, min(cx_a, cx_b), max(cx_a, cx_b))
def _carve_horizontal(self, y: int, x1: int, x2: int) -> None:
for x in range(x1, x2 + 1):
self.grid[y][x] = TileType.FLOOR.value
def _carve_vertical(self, x: int, y1: int, y2: int) -> None:
for y in range(y1, y2 + 1):
self.grid[y][x] = TileType.FLOOR.value
def _place_spawn_and_exit(self) -> None:
if self.rooms:
spawn_room = self.rooms[0]
self.grid[spawn_room["y"] + 1][spawn_room["x"] + 1] = TileType.SPAWN.value
exit_room = self.rooms[-1]
self.grid[exit_room["y"] + 1][exit_room["x"] + 1] = TileType.EXIT.value
# Бос-кімната — найбільша кімната
boss_room = max(self.rooms, key=lambda r: r["w"] * r["h"])
mid_y = boss_room["y"] + boss_room["h"] // 2
mid_x = boss_room["x"] + boss_room["w"] // 2
self.grid[mid_y][mid_x] = TileType.BOSS_ROOM.value
def _place_interactive_elements(self) -> None:
trap_count = int(len(self.rooms) * self.config.difficulty * 0.3)
chest_count = max(1, int(len(self.rooms) * 0.4))
for room in self.rng.sample(self.rooms[1:-1], min(trap_count, len(self.rooms) - 2)):
x = self.rng.randint(room["x"] + 1, room["x"] + room["w"] - 2)
y = self.rng.randint(room["y"] + 1, room["y"] + room["h"] - 2)
self.grid[y][x] = TileType.TRAP.value
for room in self.rng.sample(self.rooms, min(chest_count, len(self.rooms))):
x = self.rng.randint(room["x"] + 1, room["x"] + room["w"] - 2)
y = self.rng.randint(room["y"] + 1, room["y"] + room["h"] - 2)
if self.grid[y][x] == TileType.FLOOR.value:
self.grid[y][x] = TileType.CHEST.value
Wave Function Collapse для тайлових рівнів
class WaveFunctionCollapse:
"""
WFC генерує рівні за зразком: аналізує патерни в прикладі тайлової карти
і генерує нові карти з тими ж локальними патернами.
Застосовується в платформерах, ізометричних RPG, puzzle-іграх.
"""
def __init__(self, sample_grid: np.ndarray, pattern_size: int = 3):
self.pattern_size = pattern_size
self.patterns, self.weights = self._extract_patterns(sample_grid)
self.adjacency = self._compute_adjacency()
def _extract_patterns(self, grid: np.ndarray) -> tuple:
patterns = {}
h, w = grid.shape
p = self.pattern_size
for y in range(h - p + 1):
for x in range(w - p + 1):
pattern = tuple(grid[y:y+p, x:x+p].flatten())
patterns[pattern] = patterns.get(pattern, 0) + 1
all_patterns = list(patterns.keys())
weights = [patterns[p] for p in all_patterns]
return all_patterns, weights
def _compute_adjacency(self) -> dict:
"""Для кожного патерну визначаємо допустимих сусідів за 4 напрямками"""
adjacency = {i: {d: set() for d in ["up", "down", "left", "right"]}
for i in range(len(self.patterns))}
p = self.pattern_size
for i, pat_a in enumerate(self.patterns):
grid_a = np.array(pat_a).reshape(p, p)
for j, pat_b in enumerate(self.patterns):
grid_b = np.array(pat_b).reshape(p, p)
# Перевіряємо сумісність перекриттів
if np.array_equal(grid_a[1:, :], grid_b[:-1, :]):
adjacency[i]["down"].add(j)
adjacency[j]["up"].add(i)
if np.array_equal(grid_a[:, 1:], grid_b[:, :-1]):
adjacency[i]["right"].add(j)
adjacency[j]["left"].add(i)
return adjacency
def generate(self, output_size: tuple) -> np.ndarray:
h, w = output_size
# Кожна клітинка містить набір можливих патернів
wave = [[set(range(len(self.patterns))) for _ in range(w)] for _ in range(h)]
result = np.zeros((h, w), dtype=int)
while True:
# Знаходимо клітинку з мінімальною ентропією (не колапсовану)
min_entropy = float("inf")
min_cell = None
for y in range(h):
for x in range(w):
if len(wave[y][x]) > 1:
entropy = len(wave[y][x])
if entropy < min_entropy:
min_entropy = entropy
min_cell = (y, x)
if min_cell is None:
break
# Колапс клітинки з мінімальною ентропією
y, x = min_cell
possible = list(wave[y][x])
weights = [self.weights[p] for p in possible]
total = sum(weights)
chosen = random.choices(possible, weights=[w/total for w in weights])[0]
wave[y][x] = {chosen}
# Пропагація обмежень (BFS)
queue = deque([(y, x)])
while queue:
cy, cx = queue.popleft()
for dy, dx, direction, opposite in [(-1,0,"up","down"),(1,0,"down","up"),(0,-1,"left","right"),(0,1,"right","left")]:
ny, nx = cy + dy, cx + dx
if 0 <= ny < h and 0 <= nx < w and len(wave[ny][nx]) > 1:
allowed = set()
for pat_idx in wave[cy][cx]:
allowed |= self.adjacency[pat_idx][direction]
new_options = wave[ny][nx] & allowed
if new_options != wave[ny][nx]:
wave[ny][nx] = new_options
queue.append((ny, nx))
# Збираємо результат з першого тайлу кожного патерну
for y in range(h):
for x in range(w):
if wave[y][x]:
pat_idx = next(iter(wave[y][x]))
result[y][x] = self.patterns[pat_idx][0]
return result
AI-оцінка та покращення рівнів
from openai import AsyncOpenAI
client = AsyncOpenAI()
async def evaluate_level_design(level_grid: np.ndarray, config: DungeonConfig) -> dict:
"""LLM аналізує ASCII-представлення рівня та дає дизайн-оцінку"""
TILE_CHARS = {0: "#", 1: ".", 2: "+", 3: "C", 4: "S", 5: "E", 6: "^", 7: "B"}
ascii_map = "\n".join(
"".join(TILE_CHARS.get(int(cell), "?") for cell in row)
for row in level_grid
)
response = await client.chat.completions.create(
model="gpt-4",
messages=[{
"role": "system",
"content": """Ти — геймдизайнер, спеціаліст з level design.
Оціни данжон за критеріями та запропонуй покращення.
Позначення: # стіна, . підлога, + двері, C скриня, S спавн, E вихід, ^ пастка, B бос
Поверни JSON: {
flow_score: 1-10,
pacing_score: 1-10,
exploration_score: 1-10,
issues: ["опис проблеми"],
improvements: ["конкретні правки"],
estimated_playtime_minutes: int
}"""
}, {
"role": "user",
"content": f"Складність: {config.difficulty}\nКарта:\n{ascii_map[:2000]}"
}],
response_format={"type": "json_object"}
)
import json
return json.loads(response.choices[0].message.content)
Інтеграція з Unity та Unreal Engine
Unity: генерований grid серіалізується в JSON і читається MonoBehaviour-скриптом. Tilemap API заповнює TileBase за типами тайлів, NavMesh запікається автоматично через NavMeshSurface.
Unreal Engine 5: PCG-граф у PCG Framework приймає параметри як атрибути, Procedural Mesh Component будує геометрію з даних генератора, World Partition керує завантаженням великих данжонів по чанках.
Порівняння алгоритмів за типом гри
| Алгоритм | Тип рівнів | Детермінованість | Контроль дизайнера |
|---|---|---|---|
| BSP | Данжони, будівлі | Повна (за seed) | Високий |
| WFC | Тайлові, платформери | Повна (за seed) | Через sample-карту |
| Cellular Automata | Печери, органіка | Повна | Середній |
| Noise + Biomes | Відкриті світи | Повна | Через параметри |
| ML-генерація (GAN) | Всі типи | Часткова | Низький |
BSP кращий за WFC у 2 рази за швидкістю генерації для данжонів, але WFC дає більш різноманітні тайлові комбінації. Вибір залежить від завдання: для підземель — BSP, для платформерів — WFC. ML-генерація забезпечує максимальну варіативність, але потребує більше обчислювальних ресурсів.
Типові помилки та їх рішення
| Проблема | Причина | Рішення |
|---|---|---|
| Гравці застрягають у глухих кутах | Відсутність перевірки зв'язності | Додати L-подібні коридори та алгоритм DFS для перевірки |
| Однотипні патерни | Перевикористання одних і тих самих кімнат | Збільшити кількість зразків у WFC до 50+ |
| Дисбаланс складності | difficulty не впливає на розстановку | Прив'язати density ворогів до коефіцієнта 0.3-0.7 |
| Довга генерація на великих картах | Складність BSP O(n²) | Використовувати паралельне розбиття за допомогою numba |
Що входить у роботу та терміни
- Документація — опис архітектури генератора, API, параметрів конфігурації.
- Вихідний код — повністю робочий генератор з прикладами використання.
- Інтеграція — плагін під Unity або Unreal (за запитом).
- Навчання команди — воркшоп з налаштування та кастомізації генератора.
- Підтримка — місяць безкоштовного супроводу після здачі.
Процес роботи: аналітика (3–5 днів), проєктування (5–7 днів), реалізація (10–20 днів), тестування (5–7 днів), деплой (3–5 днів). Терміни орієнтовні: від 3 до 12 тижнів залежно від складності. Вартість розраховується індивідуально — оцінимо ваш проект безкоштовно.
Чому варто довіряти нам? Наші інженери мають понад 5 років досвіду в геймдеві та AI/ML. Сертифіковані в роботі з PyTorch, TensorFlow та Unreal Engine. Успішно запустили генерацію рівнів для 15+ ігор, включаючи комерційні релізи. Гарантуємо стабільну роботу генератора під будь-який seed і повну документацію.
Зв'яжіться для консультації — ми підберемо оптимальне рішення під ваш проект. Замовте розробку AI-системи процедурної генерації рівнів і прискорте випуск гри у 2 рази.







