docs(tech): Level Builder技能逻辑梳理报告——架构/流程/问题分析

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 00:08:17 +08:00
parent 2e40b85006
commit 59d352d385
@@ -0,0 +1,448 @@
# Level Builder 技能逻辑梳理报告
> 生成时间: 2026-06-09
> 梳理范围: `tools/level-builder/` 全部源码 + `.claude/skills/level-builder/SKILL.md`
---
## 一、整体架构
```
搭建指南.md ──parse──▶ JSON 中间数据 ──validate──▶ 验证报告
│
build (UE5内)
│
▼
.umap 关卡文件 + build_log.json
│
report
│
▼
搭建报告.md
```
四个阶段,顺序执行,前三步均可独立离线运行(无需 UE5),仅 `build` 需要 `unreal` 模块。
---
## 二、文件清单与职责
```
tools/level-builder/
├── main.py # CLI 入口,子命令分派 (parse/validate/build/report)
├── build_level.bat # Windows 批处理,自动检测 UE5 并以命令行模式启动构建
├── build_cmdline.py # 命令行构建 Python 入口(由 bat 通过 -ExecutePythonScript 调用)
├── build_in_editor.py # 编辑器内手动执行的入口脚本(Output Log Python 控制台)
├── check_pivots.py / .bat # 锚点检查工具
├── parsers/
│ ├── markdown_table.py # 通用 Markdown 管道表格解析 + 坐标/旋转/缩放解析工具函数
│ └── build_guide_parser.py # 核心解析器——按 Phase 切割搭建指南,生成 ActorData 列表
├── config/
│ ├── asset_mapping.py # 38 种 BSP 资产 → UE5 路径 + 锚点偏移 + get_pivot_compensation()
│ ├── material_mapping.py # 13 种标识色材质 → UE5 路径
│ ├── zone_config.py # 7 区域定义 + Outliner 文件夹结构 + get_folder_path()
│ └── substitution_rules.py # 语义化物件名 → Primitives 替代方案(预留给未来,当前未被导入)
├── builder/
│ └── scene_builder.py # 核心构建器——UE5 Python 环境中 spawn Actor、设 Transform/材质/Outliner
├── validation/
│ └── asset_validator.py # 离线验证:资产 key / 材质 / .uasset 文件存在性
├── report/
│ └── report_generator.py # JSON + build_log → Markdown 搭建报告
└── output/ # 生成物目录 (JSON、build_log、报告)
```
### 模块调用关系
| 模块 | 被谁调用 | 依赖模块 |
|------|---------|---------|
| `main.py` | 用户 / Claude 命令行 | `build_guide_parser`, `asset_validator`, `scene_builder`, `report_generator` |
| `build_level.bat` | 用户双击或命令行 | `build_cmdline.py`(通过 UE5 `-ExecutePythonScript`) |
| `build_cmdline.py` | `build_level.bat` | `scene_builder` (`build_level` + `setup_post_build`) |
| `build_in_editor.py` | 用户在 UE5 Python 控制台执行 | `scene_builder` (`build_level`) |
| `build_guide_parser.py` | `main.py cmd_parse()` | `markdown_table`, `zone_config` |
| `scene_builder.py` | `build_cmdline.py`, `build_in_editor.py`, `main.py cmd_build()` | `asset_mapping`, `material_mapping` |
| `asset_validator.py` | `main.py cmd_validate()` | `asset_mapping`, `material_mapping` |
| `report_generator.py` | `main.py cmd_report()` | `asset_mapping`(仅缩放记录部分) |
| `substitution_rules.py` | **无(死代码)** | — |
---
## 三、核心流程详解
### 3.1 解析流程(`BuildGuideParser`)
解析器按 Phase 切割搭建指南文本,核心是**状态机**模式——维护 `current_zone` / `current_category` 状态,遇到标题行切换状态,遇到表格行按当前状态生成 Actor。
#### Phase 划分与解析顺序
```
_parse_global_info() → metadata(当前为硬编码,未从文档提取)
↓
_parse_phase_ground() → 地面铺设表格(#### Z1 家 地面 / #### 全局地面)
↓
_parse_phase_boundaries() → 边界围挡表格(支持 ~ 范围展开,如 01~15)
↓
_parse_phase_zones() → 逐区域搭建(### C1. Z1 — 家 → #### C1.1 结构体)
↓
_parse_phase_global_objects() → 全局物件(路灯、垃圾桶等)
↓
_parse_phase_lighting() → 光照配置(DirectionalLight / SkyLight / PointLight)
↓
_add_player_start() → 硬编码 PlayerStart 位置
↓
_deduplicate_actors() → 同名 + 同位置 + 同资产 → 去重
↓
_build_summary() → 构建统计摘要
```
#### 关键设计点
**1. 范围展开(`_expand_range_row`)**
输入格式:`LVL_BND_East_01~15` + `(8000, 200+400n, 0) n=0~14`
解析流程:
- 正则 `(.+?)_(\d+)~(\d+)` 提取前缀、起始编号、结束编号
- 正则 `n\s*=\s*(\d+)~(\d+)` 提取 n 的范围
- 去掉 `n=xx~yy` 后缀后解析三个坐标表达式
- 对每个 n 值调用 `_eval_coord_expr()` 计算坐标
- 生成 N 个 ActorData,编号自动补零(`{prefix}_{num:02d}`)
**2. 坐标表达式计算(`_eval_coord_expr`)**
支持格式:
- 纯数字:`8000`
- 含 n 的简单算术:`200+400n` → 替换为 `200+400*{n}` → `eval()`
**3. 表格类型推断(`_parse_zone_row`)**
通过列名判断:
- 有 `"触发半径"` 列 → 交互物表(`_parse_interactable_row`)
- 有 `"稀有度"` 列 → 收集品表(`_parse_collectible_row`)
- 否则 → 结构体/布景表(`_parse_structure_or_prop_row`)
**4. 占位规则**
| 类型 | 占位资产 | 缩放 | 默认材质 | 说明 |
|------|---------|------|---------|------|
| 交互物 | Cube | (0.3, 0.3, 0.3) | MI_DevYellow | 交互物占位标记 |
| 收集品 | CirclePlatform | (0.03, 0.03, 0.03) | MI_DevWhite / MI_DevYellow | 普通=白,稀有用金色 |
**5. 去重逻辑**
- 同名 + 同位置 + 同资产 → 视为完全重复,移除
- 同名但位置或资产不同 → 合法保留(如 Z3 墙段)
### 3.2 构建流程(`scene_builder.build_level`)
#### 主循环
```python
for actor_def in actors:
match asset_type:
"player_start" → _spawn_player_start()
"directional_light" → _spawn_directional_light()
"sky_light" → _spawn_sky_light()
"point_light" → _spawn_point_light()
"exp_height_fog" → _spawn_exp_height_fog()
_ (default) → _spawn_mesh_actor()
```
#### `_spawn_mesh_actor` 详细流程
```
1. 查 MESH_ASSETS 获取 UE5 资产路径
└─ 找不到 → 回退到 Cube + "[MISSING]" 前缀 + MI_DevPink
2. unreal.load_asset(mesh_path)
└─ 加载失败 → 同上 Cube 回退
3. 锚点补偿计算
offset = pivot_offset_xy × scale
rotated_offset = rotate(offset, yaw)
spawn_pos = guide_pos - rotated_offset
4. editor_subsystem.spawn_actor_from_object(static_mesh, spawn_pos)
5. 设置 Rotation(关键字参数:pitch=/yaw=/roll=,避免 UE5.7 位置参数歧义)
6. 设置 Scale
7. 设置材质(查 MATERIAL_PATHS → load_asset → set_material(0, material))
8. 设置 Actor Label 和 Outliner Folder
```
#### 后处理(`setup_post_build` + `build_cmdline.py` 额外逻辑)
| 步骤 | 位置 | 功能 |
|------|------|------|
| 添加 ExponentialHeightFog | `build_cmdline.py` | 雾密度 0.015,自动检测是否已存在 |
| 设置 GameMode | `scene_builder.setup_post_build()` | BP_ThirdPersonGameMode(通过 SoftClassPath) |
| 启用大气效果 | `scene_builder.setup_post_build()` | DirectionalLight 的 `atmosphere_sun_light = True` |
| 保存关卡 | `build_cmdline.py` | `EditorLevelLibrary.save_current_level()` |
| 退出编辑器 | `build_cmdline.py` | `SystemLibrary.execute_console_command(None, "quit")` |
### 3.3 锚点补偿系统
`asset_mapping.py` 中每个资产记录了实测数据:
```python
"Wall_400x300": {
"path": "...",
"default_size": (400, 20, 300),
"actual_bounds": ((0, -10, 0), (400, 10, 300)), # min, max(实测)
"pivot_offset_xy": (200, 0), # 锚点偏离视觉中心的 XY 偏移
"category": "wall",
}
```
#### 锚点类型分类
| 类型 | 典型资产 | 锚点位置 | offset_xy |
|------|---------|---------|-----------|
| 角落锚点(墙体) | Wall_400x300, Wall_200x300 | 底边一端 | (半宽, 0) |
| 角落锚点(地板) | Floor_1000x1000, Floor_300x400 | 角落 | (半宽, 半深) |
| 角落锚点(Cube) | Cube | 角落 (0,0,0) | (50, 50) |
| 底面中心 | Cylinder, 家具类, 工业类 | 底面中心 | (0, 0) |
| 偏心锚点 | BedSingle, RoadLamp_01 | 接近角落/边缘 | 需逐个实测 |
#### 补偿公式
```
offset_scaled = pivot_offset_xy × [sx, sy]
offset_rotated = rotate_z(offset_scaled, yaw) // 绕 Z 轴旋转
spawn_pos = guide_pos - offset_rotated
```
### 3.4 验证流程(`asset_validator`)
三级检查:
1. **资产 key 检查**:`asset_key` 是否在 `MESH_ASSETS` 字典中注册
2. **材质检查**:`material` 是否在 `MATERIAL_PATHS` 字典中注册(warning 级别)
3. **文件存在性检查**:如果提供了 `project_root`,检查 `.uasset` 文件是否实际存在(warning 级别)
跳过规则:`asset_type != "static_mesh"` 的 Actor(光照、PlayerStart 等)直接标记为有效。
### 3.5 报告生成(`report_generator`)
输出结构:
```
# 白模搭建报告 — 自动生成
## 一、搭建概要
- 总 Actor 数、按类别统计、构建成功/失败数
## 二、逐区域对照
- 按 Z1~Z7 + GLOBAL 顺序
- 每区域一张表格:# / Actor名称 / 资产 / 位置 / 缩放 / 材质 / 状态(✅/❌)
## 三、资产使用统计
- 按使用次数降序排列的资产使用表
## 四、缩放记录
- 仅记录缩放 != (1,1,1) 的 Actor
- 包含实际尺寸计算(default_size × scale)
```
---
## 四、构建入口对比
| 特性 | `build_level.bat` → `build_cmdline.py` | `build_in_editor.py` |
|------|---------------------------------------|---------------------|
| 运行方式 | 命令行一键启动 UE5 | 用户手动打开 UE5 编辑器 |
| 项目路径 | 通过环境变量 `LB_PROJECT_ROOT` 传入 | **硬编码** `E:/MiceCola/work/ChildHood` |
| JSON 路径 | 通过环境变量 `LB_JSON_PATH` 传入 | 硬编码 `output/level_v01.json` |
| 关卡创建 | `ensure_map()` 自动创建/加载 | 需用户手动打开目标关卡 |
| 后处理 | ✅ 含 ExponentialHeightFog + GameMode + 大气 | ❌ 无后处理 |
| 保存 | ✅ 自动保存 | ❌ 需用户手动保存 |
| 区域过滤 | 支持(通过 `--zones`) | 代码中写死 `zones=None` |
| 推荐度 | ⭐ 推荐 | 仅调试用 |
---
## 五、数据流总结
```
搭建指南.md
│
├─ 第〇章 全局参数 ──→ metadata(当前硬编码,未动态解析)
├─ 第一章 资产映射表 ──→(未解析,仅验证用)
├─ 第二章 地面铺设 ──→ _parse_phase_ground() ──→ STR Actors(地板)
├─ 第二章 边界围挡 ──→ _parse_phase_boundaries() ──→ BND Actors(含范围展开)
├─ 第三章 逐区域 ──→ _parse_phase_zones() ──→ STR / PROP / INTR / COLL Actors
├─ 第三章 全局物件 ──→ _parse_phase_global_objects() ──→ PROP / LGT Actors
├─ 第四章 光照 ──→ _parse_phase_lighting() ──→ LGT Actors(DL / SkyLight / PointLight)
└─ 硬编码 ──→ _add_player_start() ──→ PLAY Actor
│
▼
JSON: { metadata, actors: [...], summary }
│
├─ validate ──→ 检查 asset_key / material / .uasset 存在性
│
├─ build ──→ UE5 spawn + 锚点补偿 + 后处理 ──→ .umap + build_log.json
│
└─ report ──→ Markdown 搭建报告(逐区域对照表 + 资产统计 + 缩放记录)
```
---
## 六、发现的问题和改进建议
### ⚠️ 问题 1:`_eval_coord_expr` 使用 `eval()` 存在注入风险
**位置**: `parsers/build_guide_parser.py:411`
```python
result = eval(expr) # 受限输入,仅数字和基本运算
```
**风险**: `expr` 来自 Markdown 文本内容,理论上任何字符串都可能被注入执行。
**建议**: 改用简单的算术表达式解析器,只支持 `+`、`-`、`*`、`/`、数字和变量 `n`。例如用 `ast.literal_eval` 的思路做白名单解析,或用正则拆分后逐项计算。
**严重程度**: 低(当前为单人开发、固定格式文档,实际风险极小,但作为工程规范应修复)
---
### ⚠️ 问题 2:`_parse_global_info` 硬编码 metadata
**位置**: `parsers/build_guide_parser.py:156-163`
```python
self.metadata = {
"source": "关卡-v01-搭建指南.md", # 硬编码!
"scene_size": [8000, 6000],
"ground_z": 0,
"character_height": 120,
"camera_height": 100,
}
```
**影响**: 切换到 v02 搭建指南时,metadata 中的 source 仍然是 v01,场景尺寸等参数也不会自动更新。
**建议**: 从搭建指南第〇章的表格中动态解析 metadata,与解析其他 Phase 表格使用相同的基础设施(`markdown_table.parse_table`)。
**严重程度**: 中(多版本场景下会导致报告信息不准确)
---
### ⚠️ 问题 3:`build_in_editor.py` 硬编码绝对路径
**位置**: `build_in_editor.py:18`
```python
PROJECT_ROOT = Path(r"E:/MiceCola/work/ChildHood")
```
**影响**: 不同机器或目录结构下会失效。
**建议**: 参照 `build_cmdline.py` 的做法,优先使用环境变量,fallback 到 `unreal.Paths.project_dir()` 自动检测。
**严重程度**: 低(推荐入口是 `build_level.bat`,`build_in_editor.py` 仅用于调试)
---
### ⚠️ 问题 4:`substitution_rules.py` 是死代码
**现状**: 该模块未被任何其他模块导入使用。设计意图是为未来"执行案解析器"预留——当直接从执行案而非搭建指南生成关卡时,将语义化物件名(如"冰箱")映射到 Primitives 替代方案。
**建议**: 当前无需处理,但建议在模块顶部 docstring 中明确标注"预留接口,当前未被使用"。
**严重程度**: 无(仅代码整洁性问题)
---
### 💡 问题 5:SKILL.md 区域一览表与 `zone_config.py` 不一致
**SKILL.md 中的表格**:
| Zone | origin | bounds |
|------|--------|--------|
| Z5 巷子 | (2000, 0, 0) | 600×800 |
| Z6 水塘边 | (5500, 0, 0) | 1500×1000 |
| Z7 老居民楼 | (5000, 1300, 0) | 1500×1200 |
**`zone_config.py` 中的实际值**:
| Zone | origin | bounds |
|------|--------|--------|
| Z5 巷子 | (2300, 1300, 0) | 300×2000 |
| Z6 水塘边 | (1600, 3300, 0) | 1500×1400 |
| Z7 老居民楼 | (5800, 200, 0) | 1200×1800 |
**判断**: `zone_config.py` 是经过 v01 bugfix 修正后的实际在用值,SKILL.md 表格是旧版本。以代码为准。
**建议**: 更新 SKILL.md 中的区域一览表,使其与 `zone_config.py` 一致。
**严重程度**: 低(不影响功能,仅文档准确性)
---
### 💡 问题 6:`_find_section_start` 简单字符串匹配可能误命中
**位置**: `parsers/build_guide_parser.py:787-791`
```python
def _find_section_start(self, lines, keyword):
for i, line in enumerate(lines):
if keyword in line:
return i
return -1
```
**风险**: 如果关键词出现在非标题行(如表格内容、注释中),会误匹配。
**现状**: 搭建指南格式固定,当前不会出问题。
**建议**: 如需提高鲁棒性,可增加标题行检测(如检查行首是否有 `##` 前缀)。
**严重程度**: 低
---
### 💡 问题 7:LEVEL_NAME 硬编码为 `LVL_SuburbanV01`
**位置**: `config/zone_config.py:72`
```python
LEVEL_NAME = "LVL_SuburbanV01"
```
**影响**: Outliner 文件夹路径和 Global 子文件夹路径都依赖这个常量。v02 版本时需要手动修改。
**建议**: 将 LEVEL_NAME 提升为 CLI 参数或从 JSON metadata 中读取。
**严重程度**: 低(当前手动改一行即可)
---
## 七、总结
### 架构评价
- **模块划分清晰**:解析 / 配置 / 构建 / 验证 / 报告各司其职,耦合度低
- **数据驱动设计好**:资产映射、材质映射、区域配置全部 externalize,新增资产/区域只需改 config
- **锚点补偿系统完善**:38 种资产逐一实测,含旋转补偿,解决了 BSP 资产锚点不一致的核心问题
- **双入口设计合理**:命令行自动构建(推荐)+ 编辑器内手动执行(调试)
### 代码质量
- 整体代码质量良好,注释充分
- 错误处理较完善(资产加载失败有回退,编辑器未就绪有超时等待)
- 命名规范统一(Actor 命名、文件夹结构、日志前缀)
### 风险点
| 风险 | 严重度 | 修复优先级 |
|------|--------|-----------|
| `eval()` 注入 | 低 | P3(单人开发场景风险极小) |
| metadata 硬编码 | 中 | P2(多版本场景下需修复) |
| `build_in_editor.py` 硬编码路径 | 低 | P3(推荐用 bat 入口) |
| SKILL.md 区域表过时 | 低 | P3(文档同步) |
| LEVEL_NAME 硬编码 | 低 | P3(手动改一行即可) |
| `substitution_rules.py` 死代码 | 无 | P4(代码整洁) |
| `_find_section_start` 误匹配风险 | 低 | P4(当前格式固定无问题) |