随机地牢最怕随机到不能玩
程序化地牢听起来很适合 Phaser 小游戏:每局地图不同,内容复用率高,玩家也有新鲜感。但随机生成不是把房间随便摆一摆。真正上线后,玩家会遇到门被墙挡住、出生点离出口太近、关键钥匙刷在不可达区域、Boss 房前没有补给、怪物密度忽高忽低。随机的结果必须可玩、可解释、可复现。
我参与过一个轻量 Roguelike H5,第一版地牢生成只保证房间不重叠,然后随机连线。测试时最常见的问题不是崩溃,而是“这局很怪”:有的局 30 秒到 Boss,有的局绕很久没有奖励,有的局出生点旁边就是精英怪。生成器没有坏,只是没有把设计约束写进算法。
Phaser 负责把生成结果渲染成 tilemap 和对象,生成器本身应该是纯数据逻辑。它输入 seed 和配置,输出房间、走廊、出生点、出口、怪物、宝箱和事件。可玩性校验在进入 Phaser 场景前完成。
flowchart TD
A[Seed 和生成配置] --> B[生成房间候选]
B --> C[剔除重叠并分类]
C --> D[构建房间连接图]
D --> E[铺设走廊]
E --> F[投放出生点/出口/钥匙/Boss]
F --> G[怪物和奖励预算]
G --> H{可玩性校验}
H -->|通过| I[输出 DungeonData 给 Phaser]
H -->|失败| J[换 seed 或回退模板]
Seed 是生成系统的身份证
程序化生成必须使用可控随机数。不要直接在生成器里用 Math.random()。每一局都应该有 seed,生成器每一步都从同一个 RNG 取值。这样线上玩家反馈某个地图有问题时,工程可以用同一个 seed 复现。
Seed 不只是一个数字,还要和生成配置版本绑定。同一个 seed 在配置版本 A 和版本 B 下可能生成完全不同地图。诊断信息里要记录 seed、generatorVersion、configVersion。否则玩家给你 seed,你仍然复现不出他的地图。
如果有每日地牢或活动地牢,seed 可以由日期和活动 id 派生。这样所有玩家当天看到同一张地图,方便讨论和排行榜。普通随机局则可以为每个房间生成独立 seed,便于回放和防止局部修改影响全局。
先生成房间图,再生成瓦片
很多新手生成地牢时直接在 tile grid 上挖墙。这样能做,但设计约束不清。更适合小型 Roguelike 的方式是先生成房间图:哪些房间存在,类型是什么,如何连接。房间图稳定后,再把它投影到 tilemap。
房间可以有类型:start、normal、elite、shop、treasure、event、boss。连接图可以是一棵主路径加一些支路。主路径控制通关长度,支路提供奖励和风险。这样设计师能理解结构,也能调节节奏。
直接随机连接所有房间,会产生很难控制的体验。玩家可能一开始就遇到商店却没钱,或者绕很久没有战斗。房间图让生成器有“节奏”:前两步普通战斗,中段给奖励,Boss 前给补给,支路放高风险奖励。
可达性校验必须自动做
生成完成后,一定要检查从出生点到出口、钥匙、Boss、商店等关键点是否可达。可达性可以在逻辑图层做,也可以在 tile grid 上做 flood fill。两层最好都检查:房间图连通,不代表瓦片走廊没有被墙堵住。
校验还要检查门和走廊宽度、出生点周围安全区、Boss 房入口、宝箱是否在可交互范围、传送点是否成对。不要只检查“有路径”。能走到但体验很差的地图,也应该被判失败。
失败后怎么办?小项目可以换 seed 重试,重试几次还失败就回退到手工模板。不要让玩家进入未校验地图。生成失败不是玩家该承担的风险。
怪物和奖励要用预算
随机地牢的怪物投放不能只按房间随机。每局应该有难度预算,每个房间消耗一部分预算。普通房间放小怪,精英房间消耗更多预算,Boss 房独立预算。奖励也类似:宝箱、金币、回复、商店折扣都要有总量控制。
预算能保证每局强度相对稳定。随机可以决定具体怪物和位置,但总威胁不要差太多。否则某些 seed 会特别简单,某些 seed 会几乎不可能。玩家可以接受变化,不接受不公平。
怪物投放还要考虑空间。狭窄走廊不适合放冲锋怪,小房间不适合放大量远程怪,出生点附近要留安全区。生成器需要读取房间尺寸、入口位置和障碍布局,而不是盲目随机坐标。
Phaser 渲染要消费数据,不要参与生成决策
生成器输出 DungeonData 后,Phaser Scene 根据数据创建 tilemap、角色、敌人、宝箱和触发器。Scene 不应该在 create 时继续随机决定关键内容。否则同一个 seed 在不同设备、不同帧率下可能表现不同,也不利于回放。
表现随机可以存在,比如火把闪烁、粒子方向、装饰小物件。它们应该使用表现 RNG,不影响玩法 RNG。不要因为多生成一片落叶,导致下一个宝箱奖励变化。规则随机和表现随机分离,是程序化系统可复现的关键。
生成数据也可以保存。玩家中途退出后,重新进入同一局,不应该重新生成另一张地图。保存 seed 和已探索状态,必要时保存完整 DungeonData。若配置版本变化导致无法重建,应该用已保存数据继续,而不是强行重生成。
一个生成结果接口
下面示例展示生成器输出的数据形状。Phaser 只消费结果,不关心生成细节。
type DungeonData = {
seed: string;
rooms: Array<{ id: string; type: string; x: number; y: number; w: number; h: number }>;
corridors: Array<{ from: string; to: string; path: GridPos[] }>;
spawns: Array<{ kind: 'player' | 'enemy' | 'chest' | 'exit'; roomId: string; pos: GridPos; id?: string }>;
};
function buildDungeon(seed: string, config: DungeonConfig): DungeonData {
const rng = createRng(seed);
const graph = buildRoomGraph(rng, config);
const map = carveRoomsAndCorridors(graph, rng);
return placeContent(map, rng, config);
}
这个接口让生成器可以在 Node 脚本、测试和 Phaser 运行时复用。你可以批量生成一万个 seed 做校验,而不需要启动浏览器。
生成调试要能看中间过程
程序化生成最难的是中间步骤。最终地图坏了,你需要知道房间候选是什么,为什么剔除,连接图怎么建,走廊怎么挖,内容怎么投放。开发工具应能显示生成阶段,而不是只显示最终 tilemap。
可以做一个 DungeonPreviewScene,输入 seed 后展示房间图、主路径、支路、预算消耗、可达性结果和失败原因。策划调参数时,不用每次进完整游戏。工程也能快速复现玩家反馈的 seed。
批量校验也很有价值。每天构建时随机生成几千张地图,统计失败率、平均主路径长度、Boss 前补给概率、精英房数量分布。如果配置改动让失败率突然升高,就要在上线前发现。
上线前检查清单
上线前检查:是否使用可控 seed,生成配置是否有版本,关键点是否可达,失败是否能重试和回退,怪物奖励是否有预算,出生点是否安全,规则随机和表现随机是否分离,诊断信息是否包含 seed。
还要测试旧版本存档、新配置打开旧地牢、页面刷新后恢复、同 seed 多次生成一致、批量 seed 失败率、低端设备大地图创建耗时。程序化生成不是只要生成出来,而是每次生成都要对玩家负责。
生成器要支持设计师干预
纯随机生成很酷,但设计师通常需要控制节奏。比如前两层不要出现毒雾房,Boss 前至少有一个补给房,商店不能连续出现,精英房旁边最好有奖励支路。这些规则不应该靠改代码临时写死,而应该成为生成配置的一部分。
可以把生成配置分成硬约束和软权重。硬约束必须满足,例如起点到出口可达、Boss 房存在、钥匙可达。软权重用于调节概率,例如宝箱房权重、精英房密度、支路长度。生成器先满足硬约束,再用权重做变化。这样设计师能调体验,工程也能保证安全。
干预还包括手工房间模板。某些谜题房、Boss 房、教学房不适合随机拼装,可以用模板放入生成图。程序化不等于拒绝手工内容。好的地牢往往是随机结构加手工房间,既有变化,也有设计质量。
难度曲线要从数据回看
生成器上线后,要收集每个 seed 的通关率、死亡房间、平均时长、资源剩余、放弃位置。某些房间组合可能理论可通,但实际玩家大量死亡。只靠生成校验发现不了这些问题。
数据回看可以帮助调整权重。比如某种精英怪和狭窄房间组合死亡率异常,就降低它们的组合概率;某类奖励房很少被访问,说明支路吸引力不够或位置太偏。程序化内容不是发布后就结束,而是通过数据持续调参。
也要保留问题 seed。玩家反馈某局体验糟糕,把 seed 加入回归集。以后改生成器时,重新跑这些 seed,确认不会产生更糟结果。程序化系统的测试资产就是一批有代表性的 seed。
生成耗时要进入加载预算
程序化地牢不只是设计问题,也是性能问题。房间生成、连通校验、tilemap 构建、怪物投放、装饰摆放都可能发生在进入关卡前。如果生成耗时不可控,玩家会看到 Loading 卡住。生成器要记录每个阶段耗时,并设置最大重试次数。
复杂生成可以分帧执行。先生成房间图和关键路径,让 Loading 显示真实阶段;再生成装饰和非关键内容。若多次生成失败,回退到模板地图。不要在主线程里无限尝试随机 seed。H5 移动端上,几百毫秒的同步计算就会让页面显得卡死。
生成结果还可以缓存。同一个 seed 在同一个配置版本下生成过一次,就可以保存 DungeonData。玩家重开同一局、查看地图或复盘时,不必重新生成。缓存要带版本,配置变化后自动失效。
缓存也能保护线上问题复现。玩家反馈某个 seed 有异常时,如果客户端保存了原始 DungeonData,工程能确认问题来自生成阶段,还是来自 Phaser 场景创建阶段。两者的修复方向完全不同。
生成器还可以输出 debug hash。只要 seed、配置和结果 hash 对得上,就能确认复现环境一致。排查随机问题时,这个小字段非常有用。
多人或排行榜模式下,生成结果更要防篡改。客户端可以渲染地牢,但 seed、配置版本和关键生成结果最好由服务端签名或确认。否则玩家修改本地生成配置,可能得到更容易的地图再上传成绩。单机娱乐可以宽松,竞争场景必须严格。
如果暂时没有服务端,也至少把排行榜和奖励限制在固定每日 seed 上,避免玩家无限刷最优地图。
固定 seed 还方便社区讨论攻略。大家面对同一张地图,反馈和数据才有比较意义。
每日 seed 也适合做运营活动。团队可以提前生成并审核未来几天的地图,发现异常就替换,而不是等玩家当天遇到坏图。
结语
Phaser 很适合承载程序化地牢,但生成算法必须作为独立规则系统设计。Seed、房间图、可达性、预算、校验和回放,比随机摆放更重要。随机带来新鲜感,约束保证可玩性。
好的生成器不是永远生成复杂地图,而是生成稳定、可解释、可复现的关卡。玩家看到的是每局不同,工程背后需要的是每局都能被同一个 seed 找回来。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。