PROJECT-v M5:种子驱动的程序生成关卡

M5 的目标是把 M4 的手工 LevelConfigSO.orderedRooms 升级为可复现、可约束、可验证的程序生成链路。它不是简单随机抽房间,而是把 36 个房间原型、三世界顺序、房间类型强约束、平台可达性、Addressable 加载、难度曲线和 30-run QA gate 接成一条可测试的生成系统。

除注明 docs 仓库的文档外,本文所有工程路径均以 PROJECT-V 项目根目录为基准;docs 路径均以 PROJECT-V-game-docs 根目录为基准。

关联提交和依据

M5 相关提交:

commit 用途
c3840c7848cc4cab8b5ee17b3ea189e2364e6b52 feat(levels): deliver M5 procedural generation,程序生成里程碑实现
c6bc730 docs(reports): add M5 delivery report,阶段交付报告提交

分支状态:

分支 说明
develop M5 实施分支
origin/develop 已推送

主要参考产物:

类型 路径
M5 交付报告 reports/M5_delivery_report.html
M5 QA 报告,docs 仓库 qa/reports/M5_test_report.md
30-run QA 报告,docs 仓库 qa/reports/M5_30runs_test_report.md
M5 程序生成 spec,docs 仓库 design/feature_specs/M5_procedural.md
M5 难度曲线,docs 仓库 design/difficulty_curves/M5_difficulty_curve.md
M5 房间分类,docs 仓库 design/room_prototypes/M5_room_taxonomy.md
EditMode 结果 qa/results/M5_EditMode_TestResults.xml
PlayMode 结果 qa/results/M5_PlayMode_TestResults.xml
60s Runtime Probe qa/results/M5_RuntimeProbe_60s.json
30-run 生成日志 qa/results/M5_30Runs_GenerationLog.json

M5 的目标和边界

M5 承接 M4 的 Level / Room / Boss / Difficulty 结构,但把“手工排好的 9 房间路线”替换为“seed 驱动的三世界生成 run”。

M5 纳入实现的内容:

  • 36 个 RoomPrototype:3 个异世界 × 12 个原型。

  • 每个原型对应 RoomConfigSO 和房间 prefab。

  • RoomPrototypeRegistry:按世界观、房间类型、房间序号和可达性筛选候选房间。

  • SeedSystem:确定性 RNG、seed 派生、三世界顺序洗牌、run hash。

  • LevelGenerator:生成三世界 run,强制 Start / Boss / Shop / Rest 结构。

  • AddressableRoomLoader:按 room prefab key 异步加载 / 卸载生成房间 prefab。

  • M5RuntimeProbe:60s 性能采样和 30 个派生 seed 的生成验证。

  • M5_ProceduralGeneration.unity 测试场景。

  • BuildScript 集成 M5ProjectBuilder.VerifyM5Setup()

M5 明确不做:

  • 不做最终房间美术。房间 tile 已接入三世界占位 sprite,但不是终局视觉。

  • 不做正式 OGG 房间切换音频资产。当前完成 SFX hook,并绑定现有 WAV,这项债务留到 M6/M7 补齐。

  • 不做玩家体感调参收口。当前难度曲线是工程可通关版本,不等于最终节奏。

  • 不做 Roguelite 元进度、存档、纯度 UI,这些是 M6 范围。

这个边界合理。M5 真正要证明的是生成系统不会造出结构性坏关卡:无 Boss、无补给、入口/出口悬空、死路、不可复现路线、Addressable key 缺失和性能劣化。

工程生成入口

M5 的入口是 unity-project/Assets/_Project/Editor/M5ProjectBuilder.cs

SetupM5() 的流程:

1
2
3
4
5
6
7
8
EnsureFolders()
-> CreateOrUpdatePrototypeRegistry()
-> LoadM4LevelConfigs()
-> LevelGenerator.GenerateRun(TestSeed, ...)
-> CreateOrUpdateGeneratedLevel()
-> CreateM5Scene()
-> ConfigureAddressables()
-> ConfigureBuildScenes()

核心产物:

类别 路径
原型注册表 unity-project/Assets/_Project/Levels/SO/Prototypes/RoomPrototypeRegistry_M5.asset
M5 房间配置 unity-project/Assets/_Project/Levels/SO/M5Rooms/
M5 房间 prefab unity-project/Assets/_Project/Levels/Prefabs/M5Rooms/
测试关卡 unity-project/Assets/_Project/Levels/SO/Levels/Level_M5_Procedural_Test.asset
测试场景 unity-project/Assets/Scenes/TestScenes/M5_ProceduralGeneration.unity
生成器 unity-project/Assets/_Project/Levels/Scripts/LevelGenerator.cs
原型模型 unity-project/Assets/_Project/Levels/Scripts/RoomPrototype.cs
原型注册表 unity-project/Assets/_Project/Levels/Scripts/RoomPrototypeRegistry.cs
Seed 系统 unity-project/Assets/_Project/Levels/Scripts/SeedSystem.cs
Addressable loader unity-project/Assets/_Project/Levels/Scripts/AddressableRoomLoader.cs
Runtime Probe unity-project/Assets/_Project/Levels/Scripts/M5RuntimeProbe.cs

VerifyM5Setup() 会先调用 M4ProjectBuilder.VerifyM4Setup(),再检查 M5 自身:

  • registry 数量在 30-45 内,当前为 36。

  • 三个世界观各至少 10 个原型,当前各 12。

  • 每个世界观覆盖 Start / Combat / Elite / Reward / Shop / Rest / Boss。

  • 所有原型可达。

  • Addressable entries 存在。

  • 生成器能通过强约束验证。

  • M5_ProceduralGeneration.unity 场景包含生成关卡 harness。

这延续了前几阶段的原则:阶段产物必须能被工具重建和校验,而不是靠一次手工编辑器状态维持。

RoomPrototype:生成前先定义可达性

M5 的最关键对象不是 LevelGenerator,而是 RoomPrototype。生成器只能在原型质量足够高时工作,否则随机抽取只会放大坏数据。

RoomPrototype 位于 unity-project/Assets/_Project/Levels/Scripts/RoomPrototype.cs,核心字段包括:

字段 含义
worldview Wasteland / Cyberpunk / Biohazard
roomType Start / Combat / Elite / Reward / Shop / Rest / Boss
shapeType Horizontal / Platform / LShape / TShape / Vertical
minRoomIndex / maxRoomIndex 允许出现的房间序号范围
selectionWeight 生成权重
entryLocalPosition / exitLocalPosition 入口和出口点
platforms 平台段集合
requiresJump / requiresVerticalTraversal 生成与 QA 标签
addressableKey / label prefab 加载地址
roomTemplate 对应 RoomConfigSO

M5 的可达性模型使用 M1 玩家能力作为保守边界:

参数
最大水平间距 2.75
最大向上台阶 2.10
最大安全下落 3.20
入口 / 出口支撑容差 0.35

RoomPrototype.ValidateReachability() 的判断方式:

  1. 检查 room size 和平台段尺寸。

  2. 找到支撑入口点的平台集合。

  3. 找到支撑出口点的平台集合。

  4. 把平台段看成图节点。

  5. CanTraverse() 判断平台间是否可走、可跳或可下落。

  6. 从入口支撑平台做 BFS,只要能到达任一出口支撑平台就通过。

这个模型比“所有平台都必须互通”更合适。它允许奖励旁支、Boss 安全台、T 形分叉存在,但主路径不能断。

M5 实施中修正过三类平台问题:

  • Platform 房出口高度与出口平台不一致。

  • Platform / LShape / TShape 主路径水平间距超过默认可跳距离。

  • Vertical 房最后一段到出口间距过大。

最终 36 个原型都通过了 EditMode 和 VerifyM5Setup 双重验证。

36 个房间原型

M5 每个异世界有 12 个原型:

类型 每异世界数量 用途
Start 1 开局安全房
Combat 5 普通战斗
Elite 2 中后段高压战斗
Reward 1 构筑奖励
Shop 1 中段补给
Rest 1 Boss 前缓冲
Boss 1 关卡终点

形状覆盖:

  • Horizontal

  • Platform

  • LShape

  • TShape

  • Vertical

代表性原型:

世界观 示例 ID 类型 形状
Wasteland waste_arrival_ruins Start Horizontal
Wasteland waste_scrap_bridge Combat Platform
Wasteland waste_radio_tower Elite Vertical
Cyberpunk cyber_switchboard_t Combat TShape
Cyberpunk cyber_clockwork_spiral Elite Vertical
Cyberpunk cyber_mechanism_core Boss Horizontal
Biohazard bio_clinic_l_shape Combat LShape
Biohazard bio_spine_climb Elite Vertical
Biohazard bio_maternal_core Boss Horizontal

RoomPrototypeRegistry.Find() 会按四个条件筛选:

1
2
3
4
worldview
roomType
oneBasedRoomIndex
requireReachable

这意味着生成器不会在第 2 房抽到只适合后段的 Elite,也不会抽到可达性失败的 prefab。

SeedSystem:复现不是附加功能

M5 的 seed 系统位于 unity-project/Assets/_Project/Levels/Scripts/SeedSystem.cs

它提供三类能力:

方法 作用
NormalizeSeed() 处理输入 seed
Derive() 用 base seed + salt 派生子 seed
GenerateWorldviewOrder() 对三世界顺序做确定性洗牌
HashGeneratedRun() 把生成结果压成可复验 run hash

随机数本身用 SeededRandom,内部是轻量 xorshift。M5 不需要加密随机数,需要的是低成本、可复现、跨测试稳定。

同一 seed 必须稳定产生:

  • 三世界顺序。

  • 每个世界的房间数。

  • 每个房间的类型计划。

  • 每个房间抽到的 prototype。

  • 最终 run hash。

EditMode 的 SeedSystemProducesStableRunHashAndVariedRoutes 覆盖了两件事:同 seed 重复生成 hash 一致;30 个派生 seed 中至少有足够多不同 route hash,避免“看似随机,实际总是同一条路线”。

LevelGenerator:强约束下的随机

生成器位于 unity-project/Assets/_Project/Levels/Scripts/LevelGenerator.cs

GenerateRun() 负责生成一个完整 run:

1
2
3
4
GenerateWorldviewOrder(seed)
-> for each worldview:
GenerateLevel(Derive(seed, level-order-worldview), ...)
-> HashGeneratedRun()

每个 GeneratedLevel 是 6-12 个房间。BuildRoomPlan() 先写死硬约束,再用 seed 填充可变节点:

1
2
3
4
5
6
room[0] = Start
room[last] = Boss
room[last - 1] = Rest
middle = Shop
remaining = Combat
then insert Elite / Reward by seed

关键规则:

  • Boss 固定最后一房。

  • Rest 固定倒数第二房。

  • Shop 固定中段。

  • Elite 最早第 4 房,且不相邻。

  • Reward 按剩余 Combat 位置插入。

  • 每个候选房间都必须从 RoomPrototypeRegistry.Find(..., requireReachable: true) 获取。

ValidateRun()ValidateLevel() 是 M5 的防线:

验证 失败条件
run level 数 不是 3 个世界观 level
level 房间数 少于 6 或多于 12
首房 不是 Start
末房 不是 Boss
房间数据 prototype 或 room config 缺失
可达性 任一 room prototype 不可达
Boss Boss 房不是正好 1 个
Shop 少于 1 个
Rest 少于 1 个

这就是 M5 的核心工程决策:随机只在强约束内部发生。生成器可以改变路线,但不能改变关卡的最低可玩结构。

难度曲线如何接入生成

M5 没有重写 M4 的难度系统,而是复用 DifficultyScaler.Evaluate()。差异在于 M5 为每个生成房间构造 RoomDifficultyContext

关键输入:

字段 M5 输入
roomIndex 当前房间序号
totalRooms 当前关房间总数
levelOrderIndex 本 run 中第几个异世界
roomType Start / Combat / Elite / Reward / Shop / Rest / Boss
worldview 当前世界观
upgradeScoreRaw (levelOrderIndex - 1) * 3 + roomIndex * 0.75,上限 18
weaponWorldModCount 当前关序号
uniqueWorldviewCount 当前关序号
crossWorldSynergyCount levelOrderIndex - 1
purity 当前原型固定 0.5

这使得难度在两个方向上递增:

  • 关卡内:越靠后压力越高。

  • 关卡间:第 1 / 2 / 3 个异世界逐步加压。

Elite、Boss 高于普通 Combat;Reward、Shop、Rest 不制造战斗压力。M5 当前曲线是工程可通关版本,后续仍需 playtest 调整 Elite 密度、Reward 数量、Rest 位置和 Boss 前压力上限。

AddressableRoomLoader

M5 引入 unity-project/Assets/_Project/Levels/Scripts/AddressableRoomLoader.cs

它的职责很窄:

1
2
3
4
5
6
7
8
9
LoadRoomAsync(RoomConfigSO, parent)
-> Addressables.LoadAssetAsync<GameObject>(roomPrefabKey)
-> Instantiate
-> Configure RoomManager
-> record handle

UnloadRoom(key)
-> Destroy instance
-> Addressables.Release(handle)

测试覆盖 AddressableRoomLoaderCanLoadAndUnloadGeneratedRoomPrefab,确认生成关卡里的 room prefab key 能被加载,加载后 LoadedCount = 1UnloadAll() 后回到 0。

这一步不是为了炫技,而是为 M5/M6 后续关卡 streaming 做工程接口。即使当前仍是测试场景,也不能让生成系统只停留在 SO 数据层。

M5 Runtime Probe

M5 runtime probe 位于 unity-project/Assets/_Project/Levels/Scripts/M5RuntimeProbe.cs

它在 Start() 中先跑 30-run 生成批次,再开始性能采样:

1
2
3
4
5
6
7
8
9
10
11
RunGenerationBatch()
-> for 30 derived seeds:
LevelGenerator.GenerateRun()
LevelGenerator.ValidateRun()
write generation log

then:
-> BeginLevel()
-> trigger dodge / U skill / Boss move / room advance
-> collect FPS samples
-> write runtime JSON

60s probe 结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
"seed": "00003539",
"warmupSeconds": 3,
"totalElapsedSeconds": 63.013,
"measuredSeconds": 60.013,
"frameCount": 3600,
"averageFps": 59.987,
"minFps": 49.857,
"maxFps": 125.604,
"p50Fps": 59.999,
"p95Fps": 59.954,
"p99Fps": 59.415,
"gcCollections": 0,
"monoMemoryDeltaBytes": 4096,
"m5SystemsReady": true,
"generatedRuns": 30,
"thirtyRunGenerationPassed": true,
"reachableRoomsChecked": 788,
"firstRunHash": "F09EE655",
"roomsTotal": 6,
"roomsCleared": 5,
"dodgeStarted": true,
"skillUActivated": true,
"bossMoveStarted": true,
"addressableLoaderReady": true
}

这里最重要的不是平均 FPS,而是 probe 同时证明了:

  • 30 个派生 seed 全部生成并验证通过。

  • 788 个房间原型检查点通过。

  • P95 / P99 仍接近 60 FPS。

  • GC collections 为 0。

  • 生成场景内仍能触发闪避、U 技能、Boss move 和房间推进。

  • Addressable loader 已挂入 runtime harness。

QA Gate

M5 验证结果如下:

验证项 证据 结果
SetupM5 ProjectV.EditorTools.M5ProjectBuilder.SetupM5 Passed
VerifyM5Setup ProjectV.EditorTools.M5ProjectBuilder.VerifyM5Setup Passed
尾随空格清理后 VerifyM5Setup ProjectV.EditorTools.M5ProjectBuilder.VerifyM5Setup Passed
EditMode qa/results/M5_EditMode_TestResults.xml 31 / 31 passed
PlayMode qa/results/M5_PlayMode_TestResults.xml 24 / 24 passed
Windows Development Build unity-project/Builds/Windows/PROJECT-V.exe Success
Runtime Probe 60s qa/results/M5_RuntimeProbe_60s.json Passed
30-run 生成日志 qa/results/M5_30Runs_GenerationLog.json 30 / 30 passed

EditMode 的新增 M5 用例覆盖:

  • PrototypeRegistryContainsThirtySixReachableRoomsAcrossThreeWorldviews

  • EveryPrototypeHasReachableEntryExitAndAddressableRoomTemplate

  • SeedSystemProducesStableRunHashAndVariedRoutes

  • ThirtyGeneratedRunsSatisfyM5RouteConstraints

  • M5SceneContainsGeneratedLevelHarness

  • GeneratedLevelHasM5VisualAndAudioHooks

PlayMode 的新增 M5 用例覆盖:

  • PlayerSpawnsOnReachableGeneratedPlatform

  • GeneratedSceneEntryAndExitPointsHaveGroundSupport

  • LevelManagerCanAdvanceThroughAllGeneratedRooms

  • AddressableRoomLoaderCanLoadAndUnloadGeneratedRoomPrefab

  • ThirtyGeneratedRunsValidateInPlayMode

这组测试把 M5 最容易出事故的地方都压住了:玩家出生悬空、entry/exit 没地面支撑、生成房间不可推进、Addressable key 假存在、30 个 seed 中出现坏路线。

已知风险和限制

M5 已通过核心工程与 QA gate,但仍有明确债务:

  • 正式 OGG 房间切换音频资产未生成。当前完成 SFX hook 并绑定现有 WAV,应作为 M6/M7 前音频资产补齐项。

  • 当前难度曲线是工程可通关版本,尚未经过真实玩家体感 playtest。

  • 程序生成已经能保底结构正确,但“路线是否有趣”仍需要后续内容调参和更多原型。

  • 60s probe 是阶段工程验证,不等同于发布前长时段 soak。

这些限制不影响 M5 验收。M5 的完成标准是生成系统可靠、可复现、可加载、可达、可验证,而不是最终内容手感收口。

M6 方向

M5 之后,PROJECT-V 已经具备从房间原型库生成三世界 run 的基础。M6 不应该再重写生成器,而应在这条链路上接 Roguelite 元进度。

合理方向:

  • 把生成 seed 暴露到后续 UI,支持复盘和分享。

  • 将纯度、升级树、存档与 LevelGenerator 的 build context 对接。

  • 补齐 OGG 房间切换和关卡完成音频资产。

  • 基于真实 playtest 调整 Elite 密度、Reward 数量、Rest 位置和 Boss 前压力上限。

  • 将 30-run gate 扩展到更多 seed 分布和更长 runtime window。

M5 的阶段价值在这里:PROJECT-V 现在不只是有手工关卡和 Boss,而是有一套可复现、带可达性约束、可被自动化验证的关卡生成基础。后续的重点可以转向“生成出来的 run 如何变得更有构筑价值和长期可玩性”。