程序化内容生成 (Procedural Content Generation, PCG) 是通过算法自动创建游戏内容的技术。对于Roguelike游戏,PCG是核心支柱。
PCG的优势:
PCG的挑战:
原理: 基于约束传播的瓦片拼接算法。
基本流程:
1. 定义瓦片集合及其相邻规则
2. 随机选择一个位置,坍缩为确定状态
3. 传播约束到邻居
4. 重复直到所有位置确定
示例 - 简单地牢:
瓦片类型: 墙、地板、门
规则:
- 墙只能邻接墙或门
- 地板只能邻接地板或门
- 门必须连接墙和地板
优点:
缺点:
原理: 递归分割空间,创建房间和走廊。
基本流程:
1. 从整个区域开始
2. 随机选择水平或垂直分割
3. 递归分割子区域
4. 在叶节点创建房间
5. 连接相邻房间
示例:
┌─────────────────┐
│ 房间A │ ← 叶节点1
├────────┬────────┤
│ 房间B │ 房间C │ ← 叶节点2、3
└────────┴────────┘
↑ 走廊连接
优点:
缺点:
原理: 基于简单规则的迭代演化。
经典规则 (4-5规则):
- 如果邻居墙 >= 5: 变成墙
- 如果邻居墙 <= 4: 变成地板
迭代过程:
初始: 随机噪声 (50% 墙)
■□■■□
□■□□■
■■■□□
迭代1: 应用规则
■■■■□
■■■□■
■■■■□
迭代5: 收敛
■■■■■
■□□□■
■■■■■
↑ 生成洞穴状结构
优点:
缺点:
Vampirefall的塔防+肉鸽特性需要手工设计 + 程序生成混合:
┌─────────────────────────────────────┐
│ 第1层:手工模板(塔防布局) │
│ - 预设路径节点 │
│ - 关键塔位标记 │
├─────────────────────────────────────┤
│ 第2层:程序变化(肉鸽随机性) │
│ - 敌人刷新点随机 │
│ - 资源点分布 │
│ - 地形障碍物 │
├─────────────────────────────────────┤
│ 第3层:词条修饰(肉鸽增强) │
│ - "迷雾战场"(降低视野) │
│ - "狭窄通道"(路径变窄) │
└─────────────────────────────────────┘
[CreateAssetMenu(fileName = "MapTemplate", menuName = "PCG/Map Template")]
public class MapTemplate : ScriptableObject
{
[Header("模板信息")]
public string templateName = "森林地图";
public Vector2Int size = new Vector2Int(50, 50);
[Header("路径定义")]
public PathNode[] pathNodes; // 敌人行走路径
[Header("塔位定义")]
public TowerSlot[] towerSlots; // 可建塔位置
[Header("生成规则")]
public GenerationRule[] rules;
}
[System.Serializable]
public class PathNode
{
public Vector2Int position;
public PathNode[] nextNodes; // 支持分支路径
}
[System.Serializable]
public class TowerSlot
{
public Vector2Int position;
public TowerSlotType type; // Normal, Strategic(战略点位)
}
[System.Serializable]
public class GenerationRule
{
public RuleType type;
public float probability; // 触发概率
public string parameters; // JSON参数
}
public enum RuleType
{
SpawnObstacle, // 生成障碍物
SpawnResource, // 生成资源点
ModifyPath, // 修改路径
AddEnemySpawn // 添加刷怪点
}
public class ProceduralMapGenerator : MonoBehaviour
{
public MapTemplate template;
private int seed;
public GeneratedMap Generate(int seed)
{
this.seed = seed;
Random.InitState(seed);
var map = new GeneratedMap
{
size = template.size,
tiles = new TileType[template.size.x, template.size.y]
};
// 1. 初始化基础地形
InitializeBaseTerrain(map);
// 2. 放置路径
PlacePaths(map, template.pathNodes);
// 3. 放置塔位
PlaceTowerSlots(map, template.towerSlots);
// 4. 应用生成规则
ApplyGenerationRules(map, template.rules);
// 5. 验证可玩性
if (!ValidateMap(map))
{
Debug.LogWarning($"[PCG] 地图生成失败(种子:{seed}),重新生成");
return Generate(seed + 1); // 换一个种子
}
return map;
}
private void InitializeBaseTerrain(GeneratedMap map)
{
for (int x = 0; x < map.size.x; x++)
{
for (int y = 0; y < map.size.y; y++)
{
map.tiles[x, y] = TileType.Ground;
}
}
}
private void PlacePaths(GeneratedMap map, PathNode[] nodes)
{
foreach (var node in nodes)
{
// 从节点到下一个节点绘制路径
foreach (var next in node.nextNodes)
{
DrawPath(map, node.position, next.position);
}
}
}
private void DrawPath(GeneratedMap map, Vector2Int from, Vector2Int to)
{
// A* 或 Bresenham 算法绘制路径
var points = CalculatePathPoints(from, to);
foreach (var point in points)
{
if (IsInBounds(map, point))
{
map.tiles[point.x, point.y] = TileType.Path;
// 加一点随机宽度
if (Random.value < 0.3f)
{
var offset = new Vector2Int(Random.Range(-1, 2), Random.Range(-1, 2));
var widePoint = point + offset;
if (IsInBounds(map, widePoint))
{
map.tiles[widePoint.x, widePoint.y] = TileType.Path;
}
}
}
}
}
private bool ValidateMap(GeneratedMap map)
{
// 1. 检查路径连通性
if (!IsPathConnected(map))
return false;
// 2. 检查塔位可达性
if (!AreTowerSlotsValid(map))
return false;
// 3. 检查敌人无法到达塔位
if (!IsTowerSafety(map))
return false;
return true;
}
private bool IsPathConnected(GeneratedMap map)
{
// BFS/DFS 检查从起点到终点是否连通
var start = FindStartPoint(map);
var end = FindEndPoint(map);
return BFS(map, start, end);
}
}
public class GeneratedMap
{
public Vector2Int size;
public TileType[,] tiles;
public List<Vector2Int> towerSlots;
public List<Vector2Int> enemySpawns;
public int seed;
}
public enum TileType
{
Ground,
Path,
Obstacle,
TowerSlot,
Resource
}
问题: 程序生成可能产生不可通行的路径。
解决方案: 路径优先生成 + 验证 + 修复。
public class PathGuarantee
{
public static bool EnsurePathExists(GeneratedMap map, Vector2Int start, Vector2Int end)
{
// 1. A* 寻找路径
var path = AStar.FindPath(map, start, end);
if (path != null)
return true; // 路径已存在
// 2. 强制打通路径
path = ForceCreatePath(map, start, end);
// 3. 应用到地图
foreach (var point in path)
{
map.tiles[point.x, point.y] = TileType.Path;
}
return true;
}
private static List<Vector2Int> ForceCreatePath(GeneratedMap map, Vector2Int start, Vector2Int end)
{
var path = new List<Vector2Int>();
var current = start;
while (current != end)
{
path.Add(current);
// 简单策略:每次移动到更接近终点的方向
var toEnd = end - current;
if (Mathf.Abs(toEnd.x) > Mathf.Abs(toEnd.y))
{
current += new Vector2Int(toEnd.x > 0 ? 1 : -1, 0);
}
else
{
current += new Vector2Int(0, toEnd.y > 0 ? 1 : -1);
}
// 防止无限循环
if (path.Count > map.size.x * map.size.y)
break;
}
path.Add(end);
return path;
}
}
Spelunky使用预制房间模板 + 智能拼接生成关卡。
生成流程:
1. 生成4x4房间网格
[A][B][C][D]
[E][F][G][H]
[I][J][K][L]
[M][N][O][P]
2. 标记关键房间
入口: A
出口: P
商店: 随机1个
暗室: 随机1-2个
3. 确保连通性
A → ... → P 必须可达
使用BFS生成主路径
4. 填充房间模板
从模板库中随机选择符合规则的房间
5. 添加细节
- 陷阱放置
- 敌人刷新
- 宝箱分布
模板库设计:
房间标签:
- 入口房 (ENT)
- 出口房 (EXIT)
- 普通房 (NORMAL)
- 陷阱房 (TRAP)
- 宝藏房 (TREASURE)
连接规则:
- 每个房间4个门(上下左右)
- 门必须对齐
- 每个门有"必须"或"可选"属性
Vampirefall 借鉴:
Isaac使用大量手工设计房间 + 随机组合。
房间库规模:
普通房: 500+ 个
Boss房: 50+ 个
商店: 20+ 个
宝藏房: 30+ 个
秘密房: 20+ 个
总计: 600+ 个手工房间
房间选择算法:
Room SelectRoom(RoomType type, int difficulty, HashSet<int> usedRooms)
{
// 1. 筛选候选房间
var candidates = roomDatabase
.Where(r => r.type == type)
.Where(r => r.difficulty <= difficulty)
.Where(r => !usedRooms.Contains(r.id))
.ToList();
// 2. 权重随机选择
var weights = candidates.Select(r => r.weight).ToArray();
var selected = WeightedRandom.Select(candidates, weights);
// 3. 标记已使用(避免重复)
usedRooms.Add(selected.id);
return selected;
}
设计哲学:
“程序生成不是为了减少工作量,而是为了增加重玩价值。”
Vampirefall 借鉴:
Gungeon结合了BSP分割 + 手工房间 + 特殊规则。
生成算法:
1. BSP生成房间布局
├ 分割次数: 5-7次
├ 房间数量: 15-25个
└ 房间大小: 10x10 到 30x30
2. 分配房间类型
├ 1个Boss房(必须在边缘)
├ 1个商店
├ 1-2个宝藏房
├ 2-3个挑战房
└ 其余为普通战斗房
3. 生成走廊
├ 最短路径连接
├ 添加环路(30%概率)
└ 秘密房间连接(需要炸墙)
4. 填充内容
├ 房间从模板库选择
├ 敌人根据难度曲线生成
└ 物品根据掉落表放置
特殊规则:
- Boss房必须从入口走最远路径
- 商店必须在主路径上
- 宝藏房可能在分支
- 秘密房邻接至少2个房间
Vampirefall 借鉴:
Procedural Content Generation in Games
作者: Noor Shaker, Julian Togelius, Mark J. Nelson
书籍链接
Wave Function Collapse Algorithm
Maxim Gumin
GitHub
[GDC 2017] Spelunky Level Generation
演讲者: Derek Yu
YouTube
[GDC 2015] Diablo’s Dungeon Generation
演讲者: Mike Barlow (Blizzard)
GDC Vault
The Binding of Isaac Room Design
Edmund McMillen Blog
Procedural Map Generation Techniques
RogueBasin Wiki
最后更新: 2025-12-04
维护者: Vampirefall 设计团队