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 рази.







