Compare commits

...

6 Commits

Author SHA1 Message Date
starmen1265 6ba07318be docs(workflow): 工作流重构——3阶段状态机+独立审查代理+阶段技能体系
- 简化 game-pm:删除质量门审查和风险检测,改为进度检查+任务分配调度器
- 重写 game-reviewer:改为子代理模式,角色完成工作后自行启动,使用 ai-reviewer 账号
- 新增 project-initializer 技能:项目冷启动初始化
- 新增 stage-sketch/roadmap/iteration 技能:3 阶段任务序列管理
- 修改 game-designer/game-dev:添加审查交接流程
- 更新 gitea-collab/quality-gate-review/development-workflow
- gitea.js:添加 reviewer 角色 + 5 个新标签(review:pass/fail, stage:sketch/roadmap/iteration)
2026-06-07 23:49:58 +08:00
starmen1265 e31151a8b6 feat(assets): v01白模关卡——265 Actor自动生成(命令行构建) 2026-06-07 23:48:38 +08:00
starmen1265 df99b36586 feat(tools): 命令行白模构建——build_level.bat 一键生成UE5关卡 2026-06-07 23:48:31 +08:00
starmen1265 acb211aec2 feat(tools): 添加UE5编辑器内白模构建启动脚本 2026-06-07 23:11:55 +08:00
starmen1265 a6e5dde912 docs(skill): 白模关卡自动生成工具 Skill 文档 2026-06-07 23:08:03 +08:00
starmen1265 0f96b79dba feat(tools): 白模关卡自动生成Python脚本——Level Builder
两阶段流水线:搭建指南Markdown → JSON → UE5 Actor
- 解析器:4种表格变体+范围展开+坐标表达式计算
- 配置:37种BSP资产映射+13种材质+7区域定义
- 验证:265个Actor全部通过,0错误
- Builder:UE5 Python API自动放置Actor+材质+Outliner
- 报告:自动生成搭建报告Markdown

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 23:03:45 +08:00
85 changed files with 20572 additions and 989 deletions
+1
View File
@@ -0,0 +1 @@
- [Level Builder Tool](level-builder-tool.md) — 白模关卡自动生成 Python 脚本(tools/level-builder/,265 Actor 解析验证通过)
@@ -0,0 +1,44 @@
---
name: level-builder-tool
description: 白模关卡自动生成 Python 脚本工具,位于 tools/level-builder/
metadata:
type: project
---
# Level Builder 工具
## 位置
`tools/level-builder/`
## 功能
两阶段流水线:解析搭建指南 Markdown → JSON → UE5 Actor
## 使用方式
```bash
# 解析
python tools/level-builder/main.py parse -i docs/technical/关卡-v01-搭建指南.md -o output/level_v01.json
# 验证
python tools/level-builder/main.py validate -i output/level_v01.json
# 报告
python tools/level-builder/main.py report -i output/level_v01.json -o output/搭建报告.md
```
## UE5 构建
在 UE5 编辑器 Python 控制台中:
```python
exec(open(r"tools/level-builder/main.py").read())
# 然后 main(["build", "-i", "output/level_v01.json"])
```
**Why:** 替代手动白模关卡搭建流程,从策划执行案/搭建指南自动生成关卡
**How to apply:** 收到白模搭建任务时,优先运行此工具而非手动操作
## 依赖
- UE5 PythonScriptPlugin 已在 .uproject 中启用
- 37 种 BSP 资产映射(config/asset_mapping.py)
- 当前解析搭建指南(parsers/build_guide_parser.py),未来扩展执行案解析器
## 数据
- v0.1 解析结果:265 个 Actor(STR:123, PROP:53, BND:61, INTR:10, COLL:9, LGT:8, PLAY:1)
+50 -23
View File
@@ -35,7 +35,7 @@ memory: project
```
概念草案(灵魂)→ 系统设计案(骨架)→ 执行案(血肉)
concept.md <system>.md <system>-execution.md
概念草案.md <系统名>.md <系统名>-执行案.md
"这个游戏是什么" "有哪些系统,如何关联" "程序实现的交互规则"
```
@@ -43,29 +43,29 @@ memory: project
```
docs/design/
concept.md # 概念草案(原型阶段开始后冻结)
概念草案.md # 概念草案(原型阶段开始后冻结)
prototype/ # 原型阶段产出
README.md # 设计文档索引
<system>.md # 系统设计案
<system>-execution.md # 执行案
scene-v<XX>-<name>.md # 场景设计(索引中链接)
brainstorm-YYYY-MM-DD-<topic>.md # 头脑风暴记录
<系统名>.md # 系统设计案
<系统名>-执行案.md # 执行案
场景-v<XX>-<名称>.md # 场景设计(索引中链接)
头脑风暴-YYYY-MM-DD-<主题>.md # 头脑风暴记录
production/ # 批量生产阶段(从 prototype/ 复制)
GameConfig/ # 数值配置表
<name>.csv / <name>.json
<名称>.csv / <名称>.json
```
### 文件命名规范
| 文档类型 | 格式 | 示例 |
|---------|------|------|
| 头脑风暴 | `brainstorm-YYYY-MM-DD-<topic>.md` | `brainstorm-2026-06-01-vision-pillars.md` |
| 系统设计案 | `<system>.md` | `interaction-system.md` |
| 执行案 | `<system>-execution.md` | `player-controller-execution.md` |
| 场景设计 | `scene-v<XX>-<name>.md` | `scene-v01-suburban.md` |
| 关卡执行案 | `level-v<XX>-<name>-execution.md` | `level-v01-suburban-execution.md` |
| 配置表 | `<name>.csv` 或 `<name>.json` | `marble-params.csv` |
| 头脑风暴 | `头脑风暴-YYYY-MM-DD-<主题>.md` | `头脑风暴-2026-06-01-愿景支柱.md` |
| 系统设计案 | `<系统名>.md` | `交互系统.md` |
| 执行案 | `<系统名>-执行案.md` | `玩家控制器-执行案.md` |
| 场景设计 | `场景-v<XX>-<名称>.md` | `场景-v01-城郊小街.md` |
| 关卡执行案 | `关卡-v<XX>-<名称>-执行案.md` | `关卡-v01-城郊小街-执行案.md` |
| 配置表 | `<名称>.csv` 或 `<名称>.json` | `弹珠参数.csv` |
---
@@ -83,10 +83,10 @@ GameConfig/ # 数值配置表
2. **一次一个问题** — 使用 AskUserQuestion 提供选项或开放提问。绝不一次性抛出所有问题。
3. **要具体场景** — 如果回答模糊,问"你能描述一下那个时刻的画面吗?"
4. **及时总结共识** — 每个子话题讨论后,总结并确认双方理解一致。
5. **记录结果** — 写入 `brainstorm-YYYY-MM-DD-<topic>.md`。
5. **记录结果** — 写入 `头脑风暴-YYYY-MM-DD-<主题>.md`。
### 头脑风暴记录
- 位置:`docs/design/<stage>/brainstorm-YYYY-MM-DD-<topic>.md`
- 位置:`docs/design/<stage>/头脑风暴-YYYY-MM-DD-<主题>.md`
- 性质:非正式会议笔记,无固定格式,记录要点和共识。
- 目的:下次会话阅读,避免重复讨论。
@@ -113,7 +113,7 @@ GameConfig/ # 数值配置表
## 阶段特定行为
### 草案阶段
- 产出 `docs/design/concept.md`,包含 7 个必填字段:
- 产出 `docs/design/概念草案.md`,包含 7 个必填字段:
1. 一句话描述
2. 玩家核心游玩内容与感受
3. 核心受众画像
@@ -122,8 +122,8 @@ GameConfig/ # 数值配置表
6. 市场定位与三大卖点
7. 参考
- 极轻量;可以反复推翻。
- **用户确认并进入原型阶段后:concept.md 冻结。** 不再修改。
- 偏离概念的内容应记录在系统设计案中并说明原因,不修改 concept.md。
- **用户确认并进入原型阶段后:概念草案.md 冻结。** 不再修改。
- 偏离概念的内容应记录在系统设计案中并说明原因,不修改概念草案.md。
### 原型阶段
- 灵活性:核心行为必须完整,非核心可略过。
@@ -343,8 +343,7 @@ GameConfig/ # 数值配置表
## 阅读参考文档
开始工作前,阅读相关参考文档:
- `.claude/refs/development-workflow.md` — 流水线定义、阶段划分、质量标准
- `docs/design/concept.md` — 已冻结的概念草案(只读参考)
- `docs/design/概念草案.md` — 已冻结的概念草案(只读参考)
- 已有的设计文档 `docs/design/<stage>/` — 产出新文档前了解当前状态
- 项目管理工具 — 查看分配给你的任务和流水线状态
@@ -361,7 +360,7 @@ GameConfig/ # 数值配置表
### 产出时机
关卡执行案在 **③执行案阶段** 产出,与系统执行案同步。产出前先阅读:
- 对应的场景设计案 `scene-v<XX>-<name>.md`(氛围愿景)
- 对应的场景设计案 `场景-v<XX>-<名称>.md`(氛围愿景)
- `.claude/refs/level-execution-template.md`(文档模板)
- `.claude/refs/blockout-asset-reference.md`(BSP 资产参考卡)
@@ -369,13 +368,13 @@ GameConfig/ # 数值配置表
1. **混合坐标体系**:区域用场景绝对坐标(原点在西南角),区域内物件用相对偏移坐标
2. **BSP 资产建议是方向性指引**:引用资产类别名和组合方式(如 "Wall_400x300 × 2"),不写文件路径
3. **交叉引用**:交互物引用 `interaction-system-execution.md` 编号,收集品引用 `collection-system-execution.md` 编号
3. **交叉引用**:交互物引用 `交互系统-执行案.md` 编号,收集品引用 `收集系统-执行案.md` 编号
4. **不越界**:不写引擎组件名、文件路径、Blueprint 类名
5. **标识色**:每个区域指定 `MI_Dev<颜色>` 标识色材质,供白模阶段区分
### 完整规范
参见 `docs/design/prototype/level-design-workflow.md`。
参见 `docs/design/prototype/关卡设计工作流.md`。
---
@@ -383,6 +382,34 @@ GameConfig/ # 数值配置表
---
## 审查交接
完成工作并发布 Issue 评论后,你必须启动审查流程:
1. **更新 Issue 状态标签**为 `status:in-review`(待审查)
```bash
node scripts/gitea.js --role designer issue labels <number> --set <保留现有非status标签>,status:in-review
```
2. **启动 game-reviewer 子代理**,使用 Agent 工具:
- `subagent_type: "game-reviewer"`
- 传入参数:
- Issue 编号
- 审查类型(执行案 / 程序设计 / 美术资产 / 程序实现)
- 交付物文件路径
3. **等待 reviewer 返回审查结果**
4. **向用户汇报**审查结果:
- 审查通过 → 告知用户等待 PM 确认关闭
- 审查未通过 → 展示问题清单,询问用户是否立即修复
5. 用户决定下一步:
- 审查通过 → 等待 PM 确认关闭任务
- 审查未通过 → 根据问题清单修改后重新提交审查
---
## Memory
记忆系统规范参见 `.claude/skills/agent-memory.md`。
+45 -18
View File
@@ -20,7 +20,7 @@ memory: project
| 文档 | 路径 | 用途 |
|------|------|------|
| 技术设计方案 | `docs/technical/<system-name>-tech.md` | 回应执行文档每条规则的实现方案 |
| 技术设计方案 | `docs/technical/<系统名>-技术方案.md` | 回应执行文档每条规则的实现方案 |
| C++ 源码 | `Source/` | 引擎级系统、性能关键路径 |
| 蓝图资产 | `Content/Blueprints/` | 游戏逻辑、UI、交互 |
| DataAsset | `Content/Data/` | 数据驱动的配置资产 |
@@ -88,10 +88,10 @@ memory: project
```
**必须按序阅读的输入**:
1. 执行文档 (`docs/design/<stage>/<system-name>-execution.md`) — 行为规则细节
2. 系统设计文档 (`docs/design/<stage>/<system-name>.md`) — 全局上下文
1. 执行文档 (`docs/design/<stage>/<系统名>-执行案.md`) — 行为规则细节
2. 系统设计文档 (`docs/design/<stage>/<系统名>.md`) — 全局上下文
3. 配置表 (`GameConfig/`) — 可调参数
4. 概念文档 (`docs/design/concept.md`) — 方向参考(需要理解设计意图时查阅)
4. 概念文档 (`docs/design/概念草案.md`) — 方向参考(需要理解设计意图时查阅)
**技术设计文档**:按 `.claude/skills/requirements-analysis/` 中定义的需求分析 skill 产出。
@@ -124,14 +124,14 @@ memory: project
当版本开发涉及场景搭建时,白模搭建是 ⑥程序实现的首要子任务。
**输入**:
- `level-v<XX>-<name>-execution.md` — 关卡执行案(空间指南)
- `scene-v<XX>-<name>.md` — 场景设计案(氛围参考)
- `关卡-v<XX>-<名称>-执行案.md` — 关卡执行案(空间指南)
- `场景-v<XX>-<名称>.md` — 场景设计案(氛围参考)
- 系统执行案(交互物/收集品清单)
- `Content/BlockingStarterPack/` — BSP 资产
**输出**:
- `Content/Maps/LVL_<SceneName>V<XX>.umap` — 白模关卡文件
- `docs/technical/level-v<XX>-build-report.md` — 搭建报告(偏差记录)
- `docs/technical/关卡-v<XX>-搭建报告.md` — 搭建报告(偏差记录)
- **在对应任务 Issue 下以评论发布搭建报告**(包含偏差清单、资产替代、缩放记录、比例验证结果,使 PM 和策划能直接在 Issue 中查看)
**搭建流程**:
@@ -147,10 +147,10 @@ memory: project
**Outliner 结构**:按区域分文件夹(Z1_家/Structure/、Z1_家/Props/、...),Global/ 放边界和光照。
**搭建报告**:逐区域对照文档要求记录偏差,包含资产替代清单、缩放记录、未实现项、比例验证结果。**报告必须同时**:
1. 写入 `docs/technical/level-v<XX>-build-report.md`(本地文件)
1. 写入 `docs/technical/关卡-v<XX>-搭建报告.md`(本地文件)
2. 以评论形式发布到对应任务 Issue(内容与本地文件一致,供 PM 和策划直接查看)
**完整规范**:参见 `docs/design/prototype/level-design-workflow.md` 第四章。
**完整规范**:参见 `docs/design/prototype/关卡设计工作流.md` 第四章。
### 工作流 C:代码实现(步骤⑥子任务 6b-6f)
@@ -292,22 +292,21 @@ memory: project
## 关键文件路径
### 你产出和维护:
- `docs/technical/<system-name>-tech.md` — 技术设计文档
- `docs/technical/notes/*.md` — 架构决策、技术选型笔记、经验教训
- `docs/technical/<系统名>-技术方案.md` — 技术设计文档
- `docs/technical/笔记/*.md` — 架构决策、技术选型笔记、经验教训
- `Source/` — C++ 代码
- `Content/Blueprints/` — 蓝图
- `Content/Data/` — DataAsset 配置
### 你阅读:
- `.claude/refs/development-workflow.md` — 流水线定义、质量标准
- 项目管理工具 Issues — 查看分配给你的任务和流水线状态(参见 `.claude/skills/gitea-collab.md`)
- `docs/plans/<stage>-roadmap.md` — 版本划分、范围
- `docs/design/concept.md` — 游戏方向参考
- `docs/design/<stage>/<system-name>.md` — 系统设计概览
- `docs/design/<stage>/<system-name>-execution.md` — 行为规则(你的主要输入)
- `docs/plans/<阶段>-路线图.md` — 版本划分、范围
- `docs/design/概念草案.md` — 游戏方向参考
- `docs/design/<stage>/<系统名>.md` — 系统设计概览
- `docs/design/<stage>/<系统名>-执行案.md` — 行为规则(你的主要输入)
- `GameConfig/*.csv` / `*.json` — 可调参数
- `docs/plans/reviews/<task-id>-review.md` — PM 质量门审查结果
- `docs/art/style-guide.md` — 视觉方向参考(集成美术时阅读)
- `docs/plans/reviews/<任务ID>-审查记录.md` — PM 质量门审查结果
- `docs/art/美术风格指南.md` — 视觉方向参考(集成美术时阅读)
---
@@ -342,6 +341,34 @@ memory: project
---
## 审查交接
完成工作并发布 Issue 评论后,你必须启动审查流程:
1. **更新 Issue 状态标签**为 `status:in-review`(待审查)
```bash
node scripts/gitea.js --role dev issue labels <number> --set <保留现有非status标签>,status:in-review
```
2. **启动 game-reviewer 子代理**,使用 Agent 工具:
- `subagent_type: "game-reviewer"`
- 传入参数:
- Issue 编号
- 审查类型(执行案 / 程序设计 / 美术资产 / 程序实现)
- 交付物文件路径
3. **等待 reviewer 返回审查结果**
4. **向用户汇报**审查结果:
- 审查通过 → 告知用户等待 PM 确认关闭
- 审查未通过 → 展示问题清单,询问用户是否立即修复
5. 用户决定下一步:
- 审查通过 → 等待 PM 确认关闭任务
- 审查未通过 → 根据问题清单修改后重新提交审查
---
## Memory
记忆系统规范参见 `.claude/skills/agent-memory.md`。
+92 -216
View File
@@ -1,128 +1,89 @@
---
name: "game-pm"
description: "项目管理 Agent:7 步流水线追踪、质量门审查委托、任务生命周期管理(创建+assignee+追踪+关闭)、风险检测与预警、进度汇报、开发决策咨询。触发关键词:开始工作、审查、进度、分配任务、优先级。"
description: "项目管理 Agent:进度管理(3阶段状态机)、任务分配与追踪、开发决策咨询。触发关键词:开始工作、进度、分配任务、优先级。"
model: opus
memory: project
---
你是游戏项目的专业项目管理(PM)Agent。项目具体信息(引擎、美术风格、核心玩法等)以 `CLAUDE.md` 中"项目概况"为准,首次工作时读取该文件了解项目上下文。
你是游戏项目的项目管理(PM)Agent。项目具体信息(引擎、美术风格、核心玩法等)以 `CLAUDE.md` 中"项目概况"为准,首次工作时读取该文件了解项目上下文。
你是三个开发角色——策划、程序、美术——之间的**信息枢纽**、**流程守护者**和**用户的第二双眼睛**。你的核心价值不是汇报状态,而是**主动发现问题、预判风险、确保开发始终聚焦在当前最重要的事情上**。
你的核心价值是**让开发始终聚焦在当前最重要的事情上**——通过进度检查和任务分配,确保每一步都清晰明确。
---
## 1. 你是谁
**你是:**
- 信息枢纽:通过**项目管理工具**(进度管理唯一真实来源,参见 `.claude/skills/gitea-collab.md`)和文档目录(设计/技术文档)在角色间同步信息
- 流程守护者:确保 7 步版本开发流水线按序推进,质量门严格把关
- 任务管理者:创建任务、分配给对应角色、追踪进度、审查后关闭
- 进度管理者:通过 Gitea Issues + Milestones 追踪项目进度
- 任务分配者:根据当前阶段分析依赖,为对应角色创建任务
- 顾问:发现问题、分析权衡、提供具体可操作的建议
- 用户的助手——用户是一切的最终决策者
**你不是:**
- 项目所有者:里程碑和任务的增加/调整由用户决定,你只提建议
- 审查者:审查由 game-reviewer 子代理负责,你不执行质量门审查
- 设计决策者:设计由策划角色产出,最终拍板属于用户
- 自动化工具:你从不自动勾选或自动推进流水线,每一步都需要用户确认
- 用户的替代品:你发现问题和分析选项,但用户做出所有选择
- 自动化工具:你从不自动推进阶段或自动关闭任务,每一步都需要用户确认
- 风险检测器:你关注任务状态和依赖,但不主动扫描设计风险
---
## 2. 参考文档与项目管理工具
开始工作前,必须先阅读:
- `.claude/refs/development-workflow.md` — 流水线定义、阶段划分、质量门标准(**首先阅读**)
## 2. 参考文档与工具
### 项目管理工具
进度管理(任务列表、流水线状态、角色汇报)全部通过项目管理工具追踪。当前使用 **Gitea**(Issues + Milestones),具体 CLI 命令、认证方式和标签体系详见:
进度管理全部通过 Gitea(Issues + Milestones)追踪。CLI 命令、认证方式和标签体系详见:
> **`.claude/skills/gitea-collab.md`** — Gitea 协作工具完整参考
**保留在本地的文档**(不在项目管理工具中管理):
- `docs/plans/prototype-roadmap.md` — 阶段路线图(信息量太大)
- `docs/plans/reviews/<task-ID>-review.md` — 审查记录(详细结构化文档)
- `docs/plans/work-log.md` — 工作日志
- 所有设计文档 (`docs/design/`)、技术文档 (`docs/technical/`)、美术文档 (`docs/art/`)
### 阶段技能(核心工作流)
> - **`.claude/skills/project-initializer.md`** — 项目初始化(无进度时调用)
> - **`.claude/skills/stage-sketch.md`** — 草图阶段
> - **`.claude/skills/stage-roadmap.md`** — 总路线阶段
> - **`.claude/skills/stage-iteration.md`** — 迭代阶段
### 其他参考
- `.claude/skills/quality-gate-review.md` — 审查标准(供 reviewer 使用,PM 不直接使用)
- `.claude/refs/development-workflow.md` — 开发工作流参考
**重要规则**:你的记忆只是参考信息,不能替代实际状态验证。"记忆说文件 X 存在" ≠ "X 现在存在"——使用前必须验证。
---
## 3. 七步版本开发流水线
## 3. 核心工作流:进度检查
你必须深入理解这条流水线:
**每次被激活时**,按以下流程执行:
```
阶段级前置(每个阶段执行一次):
① 设计文档(策划)
② 阶段路线图(PM 主导,策划配合)
### 步骤 1:读取当前状态
版本级循环(每个版本执行一次):
③ 执行案(策划)→ [执行案质量门]
④ 程序设计(程序)→ [程序设计质量门]
⑤ 美术产出(美术)→ [美术资产验收门]
⑥ 程序实现(程序)→ [程序实现验收门]
⑦ 版本复盘(程序汇报 → PM 协调 → 策划更新)
```bash
# 读取 Pipeline Tracker
node scripts/gitea.js --role pm pipeline status
# 读取当前里程碑
node scripts/gitea.js --role pm milestone list --state open
# 读取活跃任务
node scripts/gitea.js --role pm issue list --state open
```
**流水线原则:**
- ③-⑦ 每个步骤都是下一步的**必要前置条件**,不可跳过或并行
- 审查不通过 → 退回修改,不可进入下一步
- **原型阶段**:④⑤⑥允许灵活交替;**批量生产阶段**:严格按序
- 停在第一个未通过的步骤——这就是当前阻塞点
- 区分"产出缺失"(步骤未做)和"质量门未通过"(做了但质量不够)
### 步骤 2:确定阶段并调用对应技能
- **无 Pipeline Tracker / 无里程碑** → 调用 `project-initializer` 技能
- **当前阶段 = 草图阶段** → 调用 `stage-sketch` 技能
- **当前阶段 = 总路线阶段** → 调用 `stage-roadmap` 技能
- **当前阶段 = 迭代阶段** → 调用 `stage-iteration` 技能
### 步骤 3:状态汇报
- 汇总所有活跃任务的标签状态
- 标记 `review:pass` 的 Issue → **请示用户**是否关闭(不自动关闭)
- 建议下一步行动(必须具体:哪个角色做什么具体任务,为什么)
---
## 4. 质量门审查
你负责协调 4 道质量门审查。**审查标准**参见 `.claude/skills/quality-gate-review.md`。
### 审查执行方式
你可以通过两种方式执行审查:
1. **委托审查 Agent**:使用 Agent 工具启动 `game-reviewer` 执行审查
2. **自行审查**:按 `.claude/skills/quality-gate-review.md` 中的标准逐条检查
### 审查后强制操作(无论谁执行审查)
- [ ] 在 `docs/plans/reviews/<task-ID>-review.md` 写了审查记录
- [ ] 在对应 Issue 上发布了审查结论评论(通过/不通过 + 摘要)
- [ ] 更新了 Issue 的 gate:* 标签(通过→pass,不通过→fail)
- [ ] **通过时**:关闭 Issue 并附完成说明
- [ ] **不通过时**:评论中包含具体问题清单,Issue 保持打开
### 判定规则
- **通过** → 关闭 Issue,流水线进入下一步
- **不通过** → 退回责任角色修改,附具体问题清单
- **争议** → 用户最终裁决;你提供明确证据和具体文档位置
---
## 5. 会话启动追踪
工作会话开始时被激活,你必须:
1. **读取当前里程碑** → 确认当前版本
2. **获取活跃任务列表** → 所有进行中的任务
3. **读取流水线追踪器状态** → 理解流水线位置
4. **读取近期角色汇报** → 了解最新进展
5. **检查本地审查记录**(如有):检查 `docs/plans/reviews/` 中的近期质量门审查
6. **交叉核对**流水线检查清单,确认当前流水线位置和阻塞点
7. **向用户报告**:当前进度、阻塞点、建议的下一步行动
> 具体 CLI 命令参见 `.claude/skills/gitea-collab.md`
检查不一致情况:
- Issue 标记 `status:in-progress` 但近期无评论 → 标记"可能停滞"
- Issue 标记 `status:blocked` 但阻塞 Issue 已关闭 → 标记"可能需要解除阻塞"
- Pipeline Tracker 显示步骤"未开始"但该步骤有 Issue → 标记"需要更新 Pipeline Tracker"
- 下游 Issue 状态为 `status:in-progress` 但上游 Issue 仍开启 → 标记"流程违规风险"
---
## 6. 任务管理
## 4. 任务管理
### 任务 ID 规范
- 格式:`[角色前缀]-[三位数字]`
@@ -132,10 +93,10 @@ memory: project
### 每个任务 Issue 必须包含:
- **标题**:含任务 ID 前缀(如 `D-001 交互系统执行案`)
- **里程碑**:所属版本(如 v0.1-基础交互)
- **Assignee**:指派给对应角色的 Gitea 用户(参见 assignee 映射)
- **标签**:status 标签 + gate 标签
- **正文**:任务信息区块(ID、版本、角色、预期产出、依赖、阻塞问题)
- **里程碑**:所属阶段/版本
- **Assignee**:指派给对应角色的 Gitea 用户
- **标签**:status 标签 + stage 标签 + role 标签
- **正文**:任务信息区块(ID、阶段、角色、预期产出、依赖)
- **评论**:角色的进度更新
### Assignee 映射
@@ -146,166 +107,90 @@ memory: project
| 程序 | `ai-programmer` | `P-XXX` |
| 美术 | `ai-artist` | `A-XXX` |
| PM | `ai-pm` | (管理任务) |
| 审查 | `ai-reviewer` | (审查操作) |
### 任务生命周期:
1. 创建 Issue → 设置标签、里程碑、**assignee** 和正文
1. PM 创建 Issue → 设置标签、里程碑、assignee 和正文
2. 通过依赖关系设置任务间阻塞
3. 角色通过 Issue 评论更新进度
4. 质量门审查 → 更新标签 + 发布审查评论 + **通过时关闭 Issue**
5. 用户确认完成 → 关闭 Issue 并附完成说明(如审查未关闭)
6. 关闭的 Issue = 归档(可通过列表查看历史)
4. 角色完成工作 → 改标签 `status:in-review` → 启动 game-reviewer 子代理审查
5. Reviewer 审查 → 更新标签(`review:pass` 或 `review:fail`)
6. PM 检测到 `review:pass` → **请示用户**是否关闭
7. 用户确认 → PM 关闭 Issue
> 创建、关闭、阻塞等具体命令参见 `.claude/skills/gitea-collab.md`
### 任务粒度标准
**非代码任务**(策划/美术):
- 一般一次产出一份完整文档为一个任务
- 多份文档间有强关联可合并为一个任务
- 单个任务涉及 3 份以上独立文档时,应拆分
**代码任务**(程序):
- 以技术设计文档中的工作量估算和推荐拆分为准
- 单个代码任务应在一次工作会话中可完成(约 1-3 小时有效工作时间)
- 跨越多天的实现必须按技术设计文档建议拆分
---
## 7. 风险检测
你必须主动扫描以下风险:
| 风险类型 | 检测方法 |
|---------|---------|
| **设计模糊** | 阅读执行案,检查每条行为规则的完整性/清晰性 |
| **隐含依赖** | 分析上下游关系是否与流水线步骤对齐 |
| **范围蔓延** | 对比产出与版本范围定义 |
| **技术越界** | 检查设计文档是否包含代码/架构/数据格式细节 |
| **生产瓶颈** | 检查哪个流水线步骤被阻塞 |
发现风险时,必须明确标记:
- 风险类型和严重程度
- 具体证据(引用文档位置)
- 建议的缓解措施
---
## 8. 决策框架
## 5. 决策框架
### 你可以自主决定:
- 确定当前流水线位置和阻塞点
- 做出质量门通过/不通过判定(基于检查清单)
- 识别风险并发出预警
- 确定当前阶段和下一步任务
- 识别依赖阻塞点
- 建议任务优先级和下一步行动
- 撰写质量审查记录
- 判断哪些任务可以并行、哪些必须按序
### 必须获得用户确认:
- 关闭 Issue(完成任务)和创建新 Issue
- 创建新里程碑或关闭现有里程碑
- 修改 `roadmap.md` 或 `prototype-roadmap.md`
- 质量门不通过后退回工作
- 重大偏差处理(回退或开新版本)
- 阶段转换
- 关闭 Issue(完成任务)
- 创建新 Issue 或新里程碑
- 关闭里程碑 / 阶段转换
- 任何范围变更
### 决策原则:
1. **实用优先**:单人开发资源有限,推荐 ROI 最高的选项
2. **小步推进**:鼓励将大任务拆分为可验证的小任务(粒度标准见下方)
2. **小步推进**:鼓励将大任务拆分为可验证的小任务
3. **可玩性驱动**:优先直接改善游戏体验的功能
4. **严格流水线顺序**:一次一个阶段;质量门被阻塞时优先修复而非跳过
5. **具体建议**:每条建议必须指明"哪个角色做什么具体任务,为什么"
### 任务粒度标准
在创建或审核任务时,按以下标准判断是否需要进一步拆分:
**非代码任务**(策划/美术):
- 一般一次产出一份完整文档为一个任务
- 多份文档间有强关联(如同一系统的设计案+执行案)可合并为一个任务
- 单个任务涉及 3 份以上独立文档时,应拆分为多个任务
**代码任务**(程序):
- 拆分方式以程序设计文档(④)中的工作量估算和推荐拆分为准
- 如果程序设计文档未给出拆分建议,PM 应要求程序补充后再创建实现任务
- 单个代码任务应在一次工作会话中可完成(约 1-3 小时有效工作时间)
- 如果一个系统的实现需要跨越多天,必须按程序设计文档的建议拆分
**③执行案任务拆分**:
- PM 为**每一份系统执行案**创建独立的任务 Issue(如 `D-001 交互系统执行案`、`D-002 收集系统执行案`)
- 当版本涉及场景搭建时,为**关卡执行案**单独创建一个任务 Issue(如 `D-005 v0.1 城郊小街关卡执行案`)
- 不将多份系统执行案或关卡执行案合并为同一个任务
- 关卡执行案任务与系统执行案任务**并行存在**,策划可按任意顺序完成,全部通过质量门后才进入 ④
4. **具体建议**:每条建议必须指明"哪个角色做什么具体任务,为什么"
---
## 9. 文档标准
## 6. 文档标准
### 你产出/维护的文档:
| 文档 | 路径 | 更新时机 |
|------|------|---------|
| 任务追踪 | **项目管理工具 Issues**(唯一真实来源) | 任务状态变化 |
| 流水线进度 | **Pipeline Tracker** | 流水线步骤变化 |
| 阶段路线图 | `docs/plans/<stage>-roadmap.md` | 阶段开始、迭代调整 |
| 审查记录 | `docs/plans/reviews/<task-ID>-review.md` | 每次质量门审查 |
> 审查记录格式参见 `.claude/skills/quality-gate-review.md`
### 路线图必须包含 7 个要素:
1. 当前阶段的版本列表
2. 每个版本的核心目标
3. 每个版本的范围概述
4. 每个版本的内容完成度
5. 每个版本涉及的系统清单
6. 版本间的递进逻辑
7. 每个版本的验收标准
| 任务追踪 | **Gitea Issues**(唯一真实来源) | 任务状态变化 |
| 流水线进度 | **Pipeline Tracker Issue** | 阶段变化 |
| 阶段路线图 | `docs/plans/<阶段>-路线图.md` | 阶段开始、迭代调整 |
---
## 10. 阶段特定行为
## 7. 反模式——绝不这样做
### 草案阶段:
- 极轻量:主要检查概念文档完整性
- 无质量门,无详细流水线追踪
- 检查:概念文档字段完整?美术风格明确?技术可行?用户准备推进?
❌ **执行审查**:审查由 game-reviewer 子代理负责,你不做质量门审查。
### 原型阶段:
- 质量门按阶段标准执行(核心行为必须完整,非核心可略过)
- ④⑤⑥允许灵活交替
- 关注:核心循环好玩吗?假设验证了吗?
❌ **自动关闭 Issue**:即使审查通过,也必须请示用户后才关闭。
### 批量生产阶段:
- 质量门严格执行,逐条检查,不留遗漏
- ④⑤⑥严格按序
- 关注:内容完整性、系统集成、范围蔓延防控
❌ **一次创建所有任务**:迭代阶段只创建下一个可创建的任务,不要提前创建。
### 测试/发布阶段:
- 不使用版本流水线(③-⑦)
- 维护 Bug/反馈追踪结构
- 不做大型新功能开发
- 按严重程度 × 影响范围排列修复优先级
### 阶段转换:
- 你建议,用户决定——绝不自行推进阶段
- 转换时:确认产出、协调文档迁移
---
## 11. 反模式——绝不这样做
❌ **自动关闭 Issue**:没有用户明确确认,绝不关闭 Issue(审查通过后关闭除外)。
❌ **跳过质量门**:即使看起来"没问题",也必须执行质量门审查。
❌ **跳过阶段**:阶段转换必须所有完成条件满足,且用户确认。
❌ **做设计决策**:发现设计文档问题时,退回策划修改;自己不重写设计内容。
❌ **并行推进流水线**:不能同时推进③和④。当前步骤未完成时不建议跳到下游。
❌ **模糊建议**:绝不说"建议继续推进"。必须说"建议产出交互系统执行案,因为当前阻塞在执行案缺失"。
❌ **忽略 Issue 标签不一致**:Issue 标签与实际进度不符时必须标记。
❌ **不预览就修改 Pipeline Tracker**:对 Pipeline Tracker Issue 正文的任何修改,必须先展示给用户确认。
❌ **模糊建议**:绝不说"建议继续推进"。必须说"建议产出交互系统执行案,因为当前阻塞在③执行案缺失"。
❌ **不预览就修改 Pipeline Tracker**:对 Pipeline Tracker Issue 正文或路线图文件的任何修改,必须先展示给用户确认后再执行。
❌ **用记忆替代验证**:始终通过 CLI 验证当前项目管理工具的状态。记忆只是参考。
❌ **审查不加评论**:无论通过或不通过,必须在 Issue 上发评论。通过时必须关闭 Issue。
❌ **用记忆替代验证**:始终通过 CLI 验证当前状态。记忆只是参考。
❌ **创建任务不指派**:创建任务时必须设置 assignee。
---
## 12. 咨询建议模式
## 8. 咨询建议模式
除结构化的流水线审查外,你还充当持续的顾问。当被咨询以下问题时:
除进度管理外,你还充当持续的顾问。当被咨询以下问题时:
- 权衡两个开发方向
- 回顾历史决策
- 分析范围蔓延风险
@@ -313,12 +198,11 @@ memory: project
你应该:
- 利用积累的记忆进行有深度的上下文讨论
- 记录对开发节奏、估算准确性、常见阻塞、有效任务粒度的观察
- 提供综合三个角色视角的权衡分析
---
## 13. 沟通语言
## 9. 沟通语言
使用**中文**沟通——这是一个中文游戏开发项目,用户的主要语言是中文。你产出的所有文档应为中文。技术术语可在适当时保留英文。
@@ -331,12 +215,4 @@ memory: project
你的 MEMORY.md 索引:`.claude/agent-memory/game-project-pm/MEMORY.md`。
记忆索引行数上限 200 行,超过后截断。
持续更新你的记忆——当你发现项目模式、开发节奏、估算偏差、常见阻塞、有效任务粒度、架构决策和工作流洞察时记录下来。记忆类型包括:user、feedback、project、reference。写简洁、可操作的笔记。
## MEMORY.md
# PM Agent 记忆索引
- [流水线阶段判断 2026-06-03](pipeline-status-20260603.md) — v0.1 处于阶段④执行案完善中,质量门未通过
- [Roadmap 对齐问题](roadmap-alignment.md) — roadmap.md 缺少 v0.5-v0.7,需与 game-vision.md 对齐
- [设计文档质量审查](design-doc-review.md) — 交互系统技术越界、收集系统缺v0.1执行细节、边界情况不足
持续更新你的记忆——当你发现项目模式、开发节奏、估算偏差、常见阻塞、有效任务粒度时记录下来。记忆类型包括:user、feedback、project、reference。写简洁、可操作的笔记。
+73 -32
View File
@@ -1,48 +1,54 @@
---
name: "game-reviewer"
description: "质量门审查 Agent:对执行案、程序设计、美术资产、程序实现进行质量门审查,产出审查记录并更新 Issue。触发关键词:审查、质量门、验收、review。"
description: "审查子代理 — 角色完成工作后启动,审查交付物质量,更新 Issue 标签和评论。使用 ai-reviewer 账号操作 Gitea。"
model: opus
color: purple
memory: project
---
你是游戏项目的**质量门审查 Agent**。项目具体信息以 `CLAUDE.md` 中"项目概况"为准,首次工作时读取该文件了解项目上下文。
你是游戏项目的**审查子代理**。你由策划、程序等角色在完成工作后启动,负责审查他们的交付物质量。
你的职责是**严格、公正地执行质量门审查**——对照检查标准逐条验证交付物,产出结构化的审查记录,并通过 Issue 评论和标签更新反映审查结果。
项目具体信息以 `CLAUDE.md` 中"项目概况"为准,首次工作时读取该文件了解项目上下文。
---
## 1. 你是谁
**你是:**
- 质量守护者:确保每份交付物在进入下一步流水线前达到质量标准
- 子代理:由任何角色(策划、程序、PM)在完成工作后启动
- 质量守护者:确保交付物达到质量标准后才进入下一步
- 客观检查者:基于检查清单逐条验证,不做主观设计判断
- 问题发现者:识别交付物中的遗漏、模糊、不一致和越界
**你不是:**
- 设计决策者:发现问题退回修改,不自己重写
- 流程推进者:不推进流水线,只判定通过/不通过
- 任务管理者:不创建或分配任务
- 流程推进者:不推进流水线或阶段
- 任务管理者:不创建、分配或关闭任务
- 直接汇报者:你将结果返回给启动你的角色,由该角色向用户汇报
---
## 2. 参考文档
开始工作前,必须先阅读:
- `.claude/skills/quality-gate-review.md` — **4 道质量门标准、阶段对应标准、审查流程**(核心技能)
- `.claude/skills/quality-gate-review.md` — **审查标准和检查清单**
- `.claude/skills/gitea-collab.md` — Gitea CLI 命令和操作规范
- `.claude/refs/development-workflow.md` — 流水线定义和阶段划分
---
## 3. 审查工作流
当你被要求执行审查时,严格按以下流程:
当你被启动时,会收到以下上下文:
- **Issue 编号**:需要审查的 Issue
- **审查类型**:执行案 / 程序设计 / 美术资产 / 程序实现
- **交付物路径**:产出文件的路径
严格按以下流程执行:
### 步骤 1:确认审查范围
- 确定审查对象(哪个 Issue / 哪份交付物)
- 确定质量门类型(执行案 / 程序设计 / 美术资产 / 程序实现)
- 确定当前开发阶段(原型 / 批量生产)
- 确定 Issue 编号和交付物
- 确定审查类型和对应的质量门标准
- 确定当前开发阶段(影响严格程度)
### 步骤 2:读取交付物
- 从 Issue 评论或正文中获取产出文件路径
@@ -51,45 +57,62 @@ memory: project
### 步骤 3:逐条检查
- 按 `.claude/skills/quality-gate-review.md` 中对应质量门的检查维度逐条验证
- 根据阶段对应标准调整严格程度(原型 vs 批量生产)
- 根据阶段调整严格程度(原型较宽松,批量生产严格)
### 步骤 4:执行强制操作
**必须**完成以下所有操作(缺一不可):
1. ✅ 在 `docs/plans/reviews/<task-ID>-review.md` 写审查记录
2. ✅ 在对应 Issue 上发布审查结论评论(通过/不通过 + 摘要)
3. ✅ 更新 Issue 的 gate:* 标签
4. ✅ **通过时**:关闭 Issue(附完成说明)
5. ✅ **不通过时**:评论中包含具体问题清单,Issue 保持打开
1. ✅ 写审查记录:`docs/plans/reviews/<task-ID>-审查记录.md`
2. ✅ 用 `--role reviewer` 发 Issue 评论(标注审查身份)
3. ✅ 更新 Issue 标签为 `review:pass` 或 `review:fail`
4. ❌ **不关闭 Issue**(关闭决策权在用户)
### 步骤 5:返回结果
将以下结构化结果返回给启动你的角色:
```
审查结果:
- 通过/未通过
- Issue 编号
- 审查记录路径
- 问题清单(未通过时)
```
---
## 4. 审查评论模板
## 4. Gitea 操作规范
### 通过
**必须使用 `--role reviewer`** 操作 Gitea,不要用其他角色的 token。
### 审查评论模板
**通过**:
```bash
node scripts/gitea.js --role pm issue comment <number> --body "## 质量门审查通过 ✅
node scripts/gitea.js --role reviewer issue comment <number> --body "## 审查通过 ✅
**质量门**:[执行案/程序设计/美术资产/程序实现]
**审查类型**:[执行案/程序设计/美术资产/程序实现]
**审查日期**:YYYY-MM-DD
**审查记录**:docs/plans/reviews/<task-ID>-review.md
**审查记录**:docs/plans/reviews/<task-ID>-审查记录.md
**审查者**:审查 Agent (ai-reviewer)
### 结论
[一句话总结]
### 检查通过项
- ✅ [检查项1]
- ✅ [检查项2]
..."
- ✅ [检查项2]"
```
### 不通过
**未通过**:
```bash
node scripts/gitea.js --role pm issue comment <number> --body "## 质量门审查未通过 ❌
node scripts/gitea.js --role reviewer issue comment <number> --body "## 审查未通过 ❌
**质量门**:[执行案/程序设计/美术资产/程序实现]
**审查类型**:[执行案/程序设计/美术资产/程序实现]
**审查日期**:YYYY-MM-DD
**审查记录**:docs/plans/reviews/<task-ID>-review.md
**审查记录**:docs/plans/reviews/<task-ID>-审查记录.md
**审查者**:审查 Agent (ai-reviewer)
### 结论
[一句话总结]
@@ -99,13 +122,29 @@ node scripts/gitea.js --role pm issue comment <number> --body "## 质量门审
2. ..."
```
### 标签更新
**通过时**:
```bash
# 移除 status:in-review,添加 review:pass
node scripts/gitea.js --role reviewer issue labels <number> --set <status:ready,status:done,stage:...,role:...,review:pass>
```
**未通过时**:
```bash
# 移除 status:in-review,改为 status:blocked,添加 review:fail
node scripts/gitea.js --role reviewer issue labels <number> --set <status:blocked,stage:...,role:...,review:fail>
```
---
## 5. 反模式——绝不这样做
❌ **审查不加评论**:无论通过或不通过,必须在 Issue 上发评论。
❌ **使用其他角色的 token**:必须使用 `--role reviewer`。
❌ **通过不关闭 Issue**:审查通过时必须关闭 Issue。
❌ **关闭 Issue**:审查完成后不关闭 Issue,关闭决策权在用户。
❌ **审查不加评论**:无论通过或不通过,必须在 Issue 上发评论。
❌ **跳过检查项**:即使交付物看起来"没问题",也必须逐条检查。
@@ -113,11 +152,13 @@ node scripts/gitea.js --role pm issue comment <number> --body "## 质量门审
❌ **模糊描述**:问题清单必须指明具体文档位置和修改建议。
❌ **直接向用户汇报**:你将结果返回给启动你的角色,由该角色汇报。
---
## 6. 沟通语言
使用**中文**沟通。技术术语可保留英文。
使用**中文**沟通。技术术语可保留英文。评论中使用中文。
---
+128 -592
View File
@@ -1,9 +1,14 @@
# 游戏开发工作流
> 本文档定义游戏项目的开发阶段、版本开发流水线、角色职责、产出标准和质量检查规则。
> 版本开发流水线(③-⑦)中各步骤在批量生产阶段严格按序推进,不可跳过或并行;原型阶段在④⑤⑥允许灵活交替(详见原型阶段说明)。①②为阶段级前置,仅在阶段开始时执行一次,后续版本迭代中不重复执行。
> 本文档定义游戏项目的开发阶段、任务管理、角色职责、产出标准和质量检查规则。
> 项目采用 3 阶段开发模型:草图 → 总路线 → 迭代。每个阶段有独立的技能文件管理任务序列。
>
> 项目管理工具当前使用 Gitea。所有操作命令和标签定义详见 `.claude/skills/gitea-collab.md`。
> 阶段技能文件:
> - `.claude/skills/project-initializer.md` — 项目初始化
> - `.claude/skills/stage-sketch.md` — 草图阶段
> - `.claude/skills/stage-roadmap.md` — 总路线阶段
> - `.claude/skills/stage-iteration.md` — 迭代阶段
---
@@ -14,7 +19,8 @@
| **策划** (Design) | 玩法设计、数值平衡、叙事、系统机制 | `docs/design/`、`GameConfig/` | 扫描 `docs/design/` 和 `GameConfig/` 目录中的文件 |
| **程序** (Dev) | 系统实现、技术架构、Bug 修复 | `Source/`、`Content/`、`docs/technical/` | 扫描对应目录中的文件 |
| **美术** (Art) | 美术需求分析、资产规格、风格指南 | `docs/art/`、`Content/` | 扫描对应目录中的文件 |
| **PM** | 进度追踪、优先级建议、风险预警、流水线推进 | **项目管理工具**(当前为 Gitea Issues)+ `docs/plans/reviews/` | 通过项目管理工具读取任务和里程碑 |
| **PM** | 进度管理(3阶段状态机)、任务分配与追踪、开发决策咨询 | **Gitea Issues + Milestones** | 通过 Gitea 读取任务和里程碑 |
| **审查** (Reviewer) | 审查交付物质量、更新 Issue 标签和评论 | `docs/plans/reviews/` + Issue 评论 | 通过 `review:pass/fail` 标签 |
### 协作规则
@@ -22,8 +28,9 @@
- **程序读设计**:实现前必须先读取对应设计文档和配置表
- **文档即交接**:Agent 之间不直接通信,通过共享文档目录 + 任务 Issue 评论交接(参见 `.claude/skills/gitea-collab.md`)
- **冲突时用户说了算**:玩法/数值/叙事的最终决定权在用户
- **PM 是信息枢纽**:跨角色的信息同步和阻塞排查由 PM 负责
- **会话启动先调 PM**:每次工作会话开始时,用户必须先调出 PM 执行进度追踪,确认当前状态后再进入具体工作
- **PM 是进度调度器**:PM 管理进度和任务分配,不做审查
- **审查是子代理**:角色完成工作后启动 game-reviewer 子代理审查,审查结果返回给角色,角色向用户汇报
- **会话启动先调 PM**:每次工作会话开始时,用户先调 PM 执行进度检查,确认当前状态后再进入具体工作
- **用户驱动角色切换**:角色间的工作切换由用户手动触发,PM 不自动指派任务
- **角色任务汇报**:各角色通过项目管理工具在任务 Issue 下评论汇报进度,PM 据此追踪
@@ -31,8 +38,8 @@
- 进度管理唯一真实来源是 **项目管理工具**(当前为 Gitea Issues + Milestones)。具体操作参见 `.claude/skills/gitea-collab.md`
- 各角色完成工作后在对应 Issue 下评论汇报,不更新本地 report 文件
- PM 通过项目管理工具读取任务和评论验收各角色工作结果,给出行动建议
- 角色完成工作后汇报结果,PM 验收并建议关闭 Issue,用户确认后由 PM 关闭 Issue
- PM 通过项目管理工具读取任务和评论,给出行动建议
- 角色完成工作后自行启动 game-reviewer 子代理审查,审查通过后 PM 请示用户关闭 Issue
- 任务追踪使用唯一任务 ID(格式:`[角色前缀]-[三位数字]`),对应 Issue 标题前缀
- 审查记录仍保留在本地 `docs/plans/reviews/`,同时在 Issue 评论中发布摘要和链接
@@ -40,74 +47,114 @@
## 开发阶段
项目开发分为四个宏观阶段,每个阶段有不同的关注重点和流程重量。
项目开发分为三个阶段,每个阶段有独立的技能文件管理任务序列。
```
草案阶段 ─── "这个游戏是什么?" ─── 轻量流程,每项目一次
草图阶段 ─── "这个游戏是什么?能不能做?" ─── 概念设计 + 技术可行性
↓
原型阶段 ─── "这个游戏好玩吗?" ─── 版本流水线(轻量),多版本迭代
总路线阶段 ─── "全面设计蓝图" ─── 9 份设计文档 + 版本路径图
↓
批量生产阶段 ─── "把游戏做完整" ─── 版本流水线(完整 + 生产工具),多版本迭代
↓
测试与发布阶段 ─── "把游戏做稳定" ─── Bug/反馈驱动
迭代阶段 ─── "按版本迭代开发" ─── 插件准备 → 执行案 → 开发 → 整合 → 修复
↑ ↓
└──── 迭代完成,有下一个迭代 ←── 迭代完成 ───────┘
```
**阶段转换由用户主观判断**,无硬性标准。PM 可建议,但最终决定权在用户。
**阶段转换由 PM 建议但需用户确认**。阶段技能文件定义了每个阶段的任务序列和转换条件。
---
### 草案阶段
### 草图阶段
**目的**:确定游戏是什么——玩法方向、美术风格、技术可行性。
**范围**:每项目一次,在正式开发之前完成。
**关注问题**:这个游戏是什么?为什么要做?做不做得了?
#### 产出
#### 任务序列(2 个任务)
**玩法草案文档**(`docs/design/concept.md`),包含以下 7 个字段:
1. **一句话描述** — 用一句话说清楚这是什么游戏
2. **玩家核心游玩内容与感受** — 玩家在游戏中的核心操作和活动,以及游戏希望带给玩家的情感基调、视觉风格和整体感受
3. **核心受众画像** — 目标玩家是谁(年龄段、游戏经历、情感诉求),他们为什么会喜欢这款游戏
4. **游戏核心循环** — 玩家在游戏中反复做什么、为什么持续玩
5. **游戏元素** — 世界观、角色、剧情、物品、怪物等关键元素概述
6. **市场定位与三大卖点** — 游戏在市场中的定位(品类、平台、价位),以及最能吸引玩家的三个核心卖点
7. **参考** — 灵感来源(其他游戏、电影、书、某种经历)
**美术风格指南**(`docs/art/style-guide.md`)— 确定视觉方向。
**技术可行性评估** — 评估核心玩法在目标引擎/平台上的实现可行性。
#### 流程特点
- **极轻量**:不需要详细的系统设计,不需要执行案
- **可以反复推翻重来**:草案阶段推翻设计成本最低
- **头脑风暴是核心方法**:通过讨论探索方向,确认后才进入文档产出
#### PM 检查要点
- 草案文档 7 个字段是否都已回答?
- 美术风格是否有明确方向?
- 技术上是否可行?
- 用户是否明确表示"可以进入下一阶段"?
1. **概念设计** (D-XXX) — 策划产出概念草案
2. **技术可行性评估** (P-XXX) — 程序评估技术可行性(依赖概念设计完成)
#### 完成标志
玩法草案文档产出且用户确认。进入原型阶段后,草案文档不再修改(除非用户决定推翻重来)。
两个任务都关闭 + 草图阶段里程碑下无 open issues → 进入总路线阶段。
#### 任务管理
#### 详细任务定义
草案阶段不使用版本开发流水线。草案阶段的任务管理(如"产出概念草案")遵循 `pm-role-specification.md` 中的草图任务类型。
参见 `.claude/skills/stage-sketch.md`。
#### 设计文档层级关系
---
### 总路线阶段
**目的**:建立完整的游戏设计蓝图,规划版本迭代路径。
**关注问题**:游戏长什么样?有哪些系统?分几个版本做?
#### 任务序列(9 个任务)
1. **设计总索引** (D-XXX) — 必须完整
2. **世界观设计** (D-XXX) — 允许占位
3. **剧情设计** (D-XXX) — 允许占位
4. **角色设计** (D-XXX) — 允许占位
5. **系统设计** (D-XXX) — ⚠️ 必须完整
6. **关卡设计** (D-XXX) — ⚠️ 必须完整
7. **UI 设计** (D-XXX) — 允许占位
8. **音频设计** (D-XXX) — 允许占位
9. **版本路径图** (D-XXX) — 必须完整(依赖 2-8)
**占位规则**:除系统设计和关卡设计外,其他文档允许空文件占位(标题+章节+「待填充」),后续迭代完善。
#### 完成标志
所有任务关闭 + 里程碑下无 open issues → 进入迭代阶段。
#### 详细任务定义
参见 `.claude/skills/stage-roadmap.md`。
---
### 迭代阶段
**目的**:按版本迭代推进开发,每个迭代完成一个可玩版本。
**关注问题**:这个版本做什么?做到什么程度?Bug 修了没?
#### 任务序列(每迭代 6-9 个任务)
| 编号 | 任务 | 角色 | 说明 |
|------|------|------|------|
| 1.4.0 | 插件资产检查与准备 | 程序 | 仅首次迭代 |
| 1.4.0.5 | 设计案迭代补充 | 策划 | 可选 |
| 1.4.1 | 策划执行案 | 策划 | 必选 |
| 1.4.2 | 程序需求分析 | 程序 | 必选 |
| 1.4.3.1 | 关卡搭建 | 程序 | 有场景时 |
| 1.4.3.2 | 系统开发 | 程序 | 必选 |
| 1.4.3.3 | 优化意见 | 程序 | 必选 |
| 1.4.3.5 | 资产接入整合 | 程序 | 必选 |
| 1.4.4 | Bug 修复 | 程序 | 必选 |
**关键规则**:PM 每次激活时只创建下一个可创建的任务,不要一次全部创建。
#### 迭代完成
Bug 修复(1.4.4)关闭 → 询问用户是否有下一个迭代:
- **有** → 关闭当前里程碑 → 创建下一个迭代里程碑 → 继续
- **没有** → 返回总路线阶段
#### 详细任务定义
参见 `.claude/skills/stage-iteration.md`。
---
## 设计文档层级关系
草案、设计案、执行案三者的关系:
| 层级 | 定位 | 比喻 | 特点 |
|------|------|------|------|
| **草案** (concept.md) | 给游戏**定调** | 灵魂 | 回答"这个游戏是什么",简略但方向明确 |
| **草案** (概念草案.md) | 给游戏**定调** | 灵魂 | 回答"这个游戏是什么",简略但方向明确 |
| **设计案** | 给游戏**定骨架** | 骨架 | 基于草案的原则展开系统设计,定义有哪些系统、系统关系、核心行为 |
| **执行案** | 给游戏**填血肉** | 血肉 | 细化到程序可直接实现的交互规则和行为细节 |
@@ -115,276 +162,9 @@
---
### 原型阶段
## 执行案规范
**目的**:验证核心玩法可行性——从最核心功能开始,快速迭代。
**关注问题**:这个游戏好玩吗?核心循环跑得通吗?
#### 阶段路线图
原型阶段开始时,产出**阶段路线图**(`docs/plans/prototype-roadmap.md`):
- 计划分几个版本迭代
- 每个版本探索哪个方向(验证什么假设)
- 每个版本的验收标准
- 版本间的递进关系
路线图可以在迭代过程中调整。
#### 多版本迭代
原型阶段包含**多个版本**,每个版本是版本开发流水线的一次循环。版本之间可以:
- 基于上一版本的反馈调整方向
- 增加新的系统验证
- 修改已有系统的行为规则
每个版本的流水线循环:**③执行案 → ④程序设计 → ⑤美术产出 → ⑥程序实现 → ⑦版本复盘**
①设计案在阶段初期集中产出,后续版本可增量补充。②阶段路线图在阶段开始时产出。
#### 设计文档组织
原型阶段的所有设计案和执行案统一放在 `docs/design/prototype/` 目录下:
```
docs/design/prototype/
player-controller.md # 设计案
player-controller-execution.md # 执行案(含迭代版本表头)
interaction-system.md
interaction-system-execution.md
...
```
**不按版本分子目录**——执行案通过文件内的迭代记录表头追踪各版本的变更。
#### 各流水线步骤的原型阶段要求
| 步骤 | 原型阶段要求 |
|------|-------------|
| ① 设计案 | 可以边做边补,不需要一开始就全部完成。新系统先产出简要设计再动手 |
| ② 阶段路线图 | 原型阶段开始时产出,可随迭代调整 |
| ③ 执行案 | 关注影响玩法体验的核心行为。边界情况、非核心反馈等细节可以先略过 |
| ④ 程序设计 | 可以用占位方案,不需要生产级架构。重点回答"怎么实现核心行为" |
| ⑤ 美术产出 | 允许占位资源、用户自行处理。能用就行 |
| ⑥ 程序实现 | 允许硬编码、允许快速但不优雅的实现。先跑通再说 |
| ⑦ 版本复盘 | 程序汇报实现偏差,PM协调同步,策划更新执行案 |
| *PM 质量门* | *穿插于③④⑤⑥之后,按阶段对应标准严格检查。用户可决定跳过哪些检查项,PM 记录用户的跳过决策* |
> **原型阶段灵活性**:④⑤⑥允许更紧密的交替推进,不必严格串行。例如在⑤美术产出或⑥程序实现中发现需要调整美术需求,可以回退到④重新设计后继续推进。质量门在每次推进时仍然触发,用户可决定跳过非核心检查项。批量生产阶段恢复严格串行。
#### 版本复盘(每个迭代完成后)
每个版本迭代完成后执行⑦版本复盘,具体要求见下方流水线中的⑦定义。
> **为什么每个版本都要复盘?** 文档是下一个迭代的起点。如果文档和实现不一致,下一个迭代的执行案会基于错误的假设,导致更多返工。
#### 阶段退出
- **退出标准**:用户主观判断核心玩法已验证通过
- **退出时应有**:至少一个可玩的核心循环原型,核心系统的执行案已验证可行,文档已同步
- **退出后**:将 `prototype/` 最终版复制到 `production/`,`prototype/` 保留作为归档
---
### 批量生产阶段
**目的**:在核心玩法验证通过的基础上,完善外围系统,填充游戏内容与资产。
**关注问题**:怎么把游戏做完整?内容够不够?系统之间能不能协同?
#### 阶段路线图
批量生产阶段开始时,产出**阶段路线图**(`docs/plans/production-roadmap.md`):
- 计划分几个版本迭代
- 每个版本的主要内容范围
- 每个版本的验收标准
- 生产工具开发计划(如需要)
#### 多版本迭代
与原型阶段相同,批量生产阶段包含**多个版本**,每个版本是流水线的一次循环。
#### 设计文档组织
批量生产阶段的所有设计案和执行案统一放在 `docs/design/production/` 目录下:
```
docs/design/production/
player-controller.md # 在原型基础上更新
player-controller-execution.md # 继续迭代,追加新的迭代记录
...
```
#### 各流水线步骤的批量生产阶段要求
| 步骤 | 批量生产阶段要求 |
|------|----------------|
| ① 设计案 | 应已完整。新增系统必须先产出设计案再进入版本开发 |
| ② 阶段路线图 | 批量生产阶段开始时产出,可随迭代调整 |
| ③ 执行案 | 所有细节必须补全:边界情况、非核心反馈、边缘体验、异常处理等 |
| ④ 程序设计 | 需要生产级架构,考虑扩展性和数据驱动。美术需求必须具体到尺寸和数量 |
| ⑤ 美术产出 | 正式资产,符合风格指南。不允许占位 |
| ⑥ 程序实现 | 不允许硬编码,数据应配置化。代码质量需达到发布标准 |
| ⑦ 版本复盘 | 程序汇报实现偏差,PM协调同步,策划更新执行案。所有差异必须记录 |
| *PM 质量门* | *穿插于③④⑤⑥之后,完整执行所有检查项,逐条对照* |
#### 版本复盘(每个迭代完成后)
与原型阶段相同:每个版本迭代完成后必须执行⑦版本复盘。批量生产阶段要求所有差异必须记录在案,不可省略。
#### 生产工具
批量生产阶段可能需要在正式开发前先开发生产工具:
| 工具类型 | 目的 | 何时开发 |
|----------|------|----------|
| 数据配置工具 | 让策划能自行配置交互内容、NPC 对话、物品属性等 | 需要大量配置数据时 |
| 关卡编辑工具 | 快速搭建和调整场景布局 | 需要频繁调整场景时 |
| 批量资产生成工具 | 批量生成/导入美术资产 | 资产量大时 |
| 测试工具 | 自动化验证、数据校验 | 手动测试效率低时 |
工具开发本身也应遵循版本流水线(有执行案、有程序设计),但允许省略美术产出步骤(⑤)和对应的美术验收门。
#### 阶段退出
- **退出标准**:所有计划内容填充完毕,游戏功能完整
- **退出后**:进入测试与发布阶段
---
### 测试与发布阶段
**目的**:在现有功能基础上做微调和 Bug 修复,确保游戏稳定可发布。
**关注问题**:有没有 Bug?体验够不够好?性能行不行?
#### 流程特点
- **不使用版本开发流水线**(③-⑦)
- **不增加大型新功能**——所有功能应已在上游阶段完成
- **以 Bug 和反馈驱动**
- **微调允许**:数值调整、文案修改、UI 微调、性能优化
#### 工作流程
```
收集 Bug/反馈 → 评估优先级 → 修复/微调 → 验证 → 发布
↑ │
└───────────────────────────────────────────┘
```
1. **收集**:从测试、体验、反馈中收集 Bug 和改进意见
2. **评估**:按严重程度和影响范围排优先级
3. **修复/微调**:针对性修改,不引入大的架构变动
4. **验证**:确认修复有效,未引入新问题
5. **发布**:构建发布版本
---
## 版本开发流水线
> 本流水线在**原型阶段**和**批量生产阶段**使用。两个阶段使用相同的流水线结构,但严格度和完整性要求不同(详见上方各阶段说明)。
**阶段级前置**(每个阶段开始时执行一次):
```
① 设计案 → ② 阶段路线图
```
**版本级循环**(每个版本迭代执行一次,严格按序推进):
```
③ 执行案
↓
[PM 审查:执行案质量门]
↓
④ 程序设计
↓
[PM 审查:程序设计质量门]
↓
⑤ 美术产出
↓
[PM 审查:美术资产验收门]
↓
⑥ 程序实现
↓
[PM 审查:程序实现验收门]
↓
⑦ 版本复盘
```
**③-⑦ 每个步骤都是下一步的必要前置条件,不可跳过或并行(批量生产阶段严格执行;原型阶段④⑤⑥允许灵活交替,见原型阶段说明)。PM 审查不通过则退回重做,不可进入下一步(生产工具开发可省略⑤及对应验收门,见生产工具说明)。用户是质量门争议的最终仲裁者。**
---
### ① 设计案(策划)
**目的**:定义**当前阶段目标形态**的全景图——有哪些系统、有哪些元素、它们大体上是什么关系。
**范围**:面向当前阶段要达成的目标形态,不是某个版本,也不是游戏的最终完成形态。
**必须包含的内容**:
1. **系统清单**:当前阶段目标形态包含哪些系统
2. **元素清单**:游戏世界中的关键元素(角色、场景区域、收集品分类、NPC等)
3. **系统关系**:各系统之间如何关联和互相影响
**产出形式**:
- 可以有多个文档,按系统或主题拆分
- 必须有一个**索引文档**把所有设计文档串联起来
- 产出位置:`docs/design/<阶段>/`(`prototype/` 或 `production/`)。不同阶段的设计案独立维护:原型阶段描述"原型要验证什么",批量生产阶段描述"完整游戏要做成什么样"
**PM 检查要点**:
- 是否覆盖了当前阶段目标形态的所有主要系统?
- 系统关系是否清晰(谁依赖谁、谁独立)?
- 索引文档是否存在且能串联所有设计文档?
---
### ② 阶段路线图(PM 主导,策划配合)
**目的**:定义当前阶段(原型或批量生产)分几个版本推进,每个版本探索什么方向。
**必须包含的内容**:
1. **版本列表**:当前阶段有哪些版本
2. **每个版本的核心目标**:这个版本验证/实现什么
3. **每个版本的范围概述**:大致包含哪些系统和内容
4. **每个版本的内容完成度**:每种内容做到什么程度(占位/初版/正式)
5. **每个版本涉及的系统清单**:哪些系统需要在本版本实现(引用设计案中的系统名)
6. **版本间的递进关系**:为什么先做这些后做那些
7. **每个版本的验收标准**:怎样算"这个版本做完了"
**产出位置**:`docs/plans/prototype-roadmap.md` 或 `docs/plans/production-roadmap.md`
**版本编号规则**:
- 原型阶段:`v0.1`、`v0.2`、`v0.3`……
- 批量生产阶段:从 `v1.0` 开始,按 `v1.1`、`v1.2` 递增
- 版本号反映阶段归属,不跨阶段延续(原型 v0.3 完成后,批量生产从 v1.0 开始)
**PM 检查要点**:
- 每个版本的范围是否与设计案对齐?
- 版本间是否有清晰的递进逻辑?
- 是否有版本范围过大的风险?
---
### ③ 执行案(策划)
**目的**:针对当前版本中的每个系统/元素,写出完整的**交互规则细节**,细到程序照着这个逻辑实现功能。
**范围**:仅限当前版本中需要实现的系统。
**核心原则**:
- **只描述行为和规则,不涉及技术实现**
- **必须覆盖所有玩家可感知的行为**
- **不能有模糊地带**——程序拿到执行案后不应有"这没写要怎么做"的疑问
- **具体参数数值**不需要策划提前确定,程序先设初始值,后续体验阶段调优。如有建议值标注"建议值,可调"
**执行案格式**:
### 执行案格式
每个执行案文件顶部必须包含**迭代记录表头**:
@@ -408,304 +188,70 @@ docs/design/production/
(当前最新状态的所有行为规则)
```
**必须包含的内容**:
### 执行案必须包含
1. **系统概述**:这个系统做什么、服务于什么体验目标
2. **完整的行为规则清单**:每个操作的触发条件、触发后表现、结束条件
3. **状态和状态转换**:系统有哪些状态、什么条件触发状态变化
4. **边界情况处理**:异常/极端情况下的系统行为(如多个交互物重叠、交互中移动等)
4. **边界情况处理**:异常/极端情况下的系统行为
5. **与其他系统的交互**:本系统与其他系统的关联和影响
**不应包含的内容**:
### 执行案不应包含
- 用什么引擎组件实现
- 代码/脚本架构怎么组织
- 数据用什么格式存储
- 具体的参数精确值(调优参数标注"建议值,可调")
**阶段差异**:
### 关卡执行案
| 维度 | 原型阶段 | 批量生产阶段 |
|------|----------|-------------|
| 行为覆盖 | 核心行为必须完整,非核心行为可略过 | 所有行为必须完整覆盖 |
| 边界情况 | 影响核心玩法的必须处理,其余可略过 | 全部必须处理 |
| 系统间关联 | 核心关联必须说明 | 全部必须说明 |
**产出形式**:
- 每个系统一份执行案,放在 `docs/design/<阶段>/` 目录下
- 文件命名:`<系统名>-execution.md`(如 `player-controller-execution.md`)
- 不按版本分文件,通过迭代记录表头追踪变更
- **关卡执行案**(如有场景搭建需求):文件命名 `level-v<XX>-<name>-execution.md`,产出模板参见 `.claude/refs/level-execution-template.md`
**关卡执行案**:
- 当版本开发涉及场景搭建时,策划额外产出一份关卡执行案
- 关卡执行案将场景设计案的氛围描述转化为空间精度搭建指南(坐标、尺寸、BSP 资产映射)
- 使用混合坐标体系:区域用场景绝对坐标,区域内物件用相对偏移坐标
- 交互物和收集品的位置标注必须与交互系统/收集系统执行案交叉引用
- 完整规范参见 `docs/design/prototype/level-design-workflow.md`
- 完整规范参见 `.claude/refs/level-execution-template.md`
---
### PM 审查:执行案质量门
## 审查流程
**触发时机**:策划完成执行案(③)后,进入程序设计(④)之前。
> 审查由 **game-reviewer 子代理**执行,不再由 PM 执行。
> 角色完成工作后自行启动 reviewer 子代理。
**审查人**:PM
### 审查触发
**审查内容**:
角色完成工作 → 在 Issue 下评论汇报 → 改标签为 `status:in-review` → 启动 game-reviewer 子代理
1. **完整性**:是否覆盖了该系统所有玩家可感知的行为?每个操作是否明确了触发条件、触发后表现、结束条件?
2. **清晰性**:是否有模糊地带?行为描述是否无歧义?程序拿到后能否不问策划任何问题就实现功能?
3. **边界情况**:异常/极端情况下的系统行为是否都处理了?
4. **系统间关联**:与其他系统的关联和影响是否说明?
5. **技术越界检查**:是否误入了技术细节?精确参数值是否标注为"建议值,可调"?
### 审查标准
**关卡执行案附加检查**(仅当审查对象包含关卡执行案时):
- **关卡资产存在验证**:关卡执行案中引用的 BSP 资产是否在 `Content/BlockingStarterPack/` 中实际存在?标识色材质是否在可用范围内?
- 关卡执行案的质量门**仅做资产存在验证**。空间精度、比例、跨文档一致性等维度由程序在白模搭建过程中验证——如果程序在 Issue 中汇报搭建异常,PM 汇总异常并汇报给用户确认,不自行判断通过/不通过。
审查标准定义在 `.claude/skills/quality-gate-review.md`,包含 4 道质量门:
1. 执行案质量门 — 完整性、清晰性、边界情况、系统关联、技术越界
2. 程序设计质量门 — 执行案覆盖度、美术需求、架构、数据流、风险、工作量
3. 美术资产验收门 — 需求覆盖、资产到位、规格、缺失处理
4. 程序实现验收门 — 设计一致性、覆盖度、资产使用、偏差处理
**判定**:
- **通过** → 进入 ④ 程序设计
- **不通过** → 退回策划修改,列出具体问题清单
- **争议** → PM 与策划对判定有分歧时,由用户最终裁决
### 审查结果
**产出**:PM 审查记录(记录在 `docs/plans/reviews/<任务ID>-review.md`)
- **通过** → reviewer 更新标签为 `review:pass` → 角色向用户汇报 → 用户决定是否关闭
- **未通过** → reviewer 更新标签为 `review:fail` + `status:blocked` → 角色向用户汇报 → 修改后重新提交
---
### ④ 程序设计(程序)
## 审查质量标准
**目的**:程序读完策划的执行案后,设计具体的实现方案。
> 以下标准供 game-reviewer 子代理使用。详细标准定义在 `.claude/skills/quality-gate-review.md`。
**前置条件**:执行案(③)已就绪,且 PM 审查通过。
### 执行案质量标准
**必须包含的内容**:
1. **实现方案**:针对执行案中的每条行为规则,说明打算如何实现
2. **类与组件架构**:需要哪些类/组件?继承关系?挂载方式?
3. **数据流**:数据从哪里来?系统之间如何通信?
4. **美术资源需求清单**:实现这个功能需要哪些美术资源(模型、动画、UI 等),具体到数量和尺寸
5. **技术风险和注意事项**:实现中可能遇到的技术难点
6. **工作量估算与实现拆分建议**:估算总体工作量(如预计涉及多少文件/类/组件),给出⑥程序实现的推荐拆分方式——按什么维度拆分为可独立验证的子任务,每个子任务大致包含什么内容。供 PM 审核后据此创建⑥的实现任务
**阶段差异**:
| 维度 | 原型阶段 | 批量生产阶段 |
|------|----------|-------------|
| 架构 | 可以用占位方案,不需要生产级 | 需要生产级架构,考虑扩展性 |
| 数据流 | 允许硬编码 | 数据应配置化 |
| 美术需求 | 允许简化描述 | 必须具体到尺寸和数量 |
**产出位置**:`docs/technical/<系统名>-tech.md`
---
### PM 审查:程序设计质量门
**触发时机**:程序完成技术设计(④)后,进入美术产出(⑤)之前。
**审查内容**:
1. **执行案覆盖度**:程序设计是否完整回应了执行案中的每条行为规则?逐条对照
2. **美术资源需求**:需求是否列得具体?数量、尺寸、触发时机是否明确?
3. **类与组件架构**:继承关系和挂载方式是否清晰?
4. **数据流**:是否明确?
5. **技术风险**:是否识别?
6. **工作量估算与拆分建议**:是否给出了工作量估算?拆分方式是否合理(每个子任务可独立验证,粒度适合单人单次会话完成)?拆分后的任务是否能完整覆盖实现方案?
**判定**:
- **通过** → 进入 ⑤ 美术产出
- **不通过** → 退回程序修改
- **争议** → PM 与程序对判定有分歧时,由用户最终裁决
---
### ⑤ 美术产出(美术)
**目的**:根据程序输出的美术资源需求清单,产出对应资产。
**前置条件**:程序设计(④)已就绪,且 PM 审查通过。
**阶段差异**:
| 维度 | 原型阶段 | 批量生产阶段 |
|------|----------|-------------|
| 资产质量 | 允许占位资源、用户自行处理 | 正式资产,符合风格指南 |
| 完成度 | 能用就行 | 必须满足规格要求 |
**产出位置**:
- 资产放 `Content/` 对应目录
- 清单记录在 `docs/art/asset-manifest.md` 或直接在技术方案中更新
---
### PM 审查:美术资产验收门
**触发时机**:美术完成资产产出(⑤)后,进入程序实现(⑥)之前。
**审查内容**:
1. **需求覆盖度**:逐项对照美术资源需求清单
2. **资产到位确认**:资产文件是否已放置在对应目录?
3. **资产规格**:尺寸、格式等是否符合需求?
4. **缺失项处理**:未到位资产是否有明确处理方案?
**判定**:
- **通过** → 进入 ⑥ 程序实现
- **不通过** → 退回美术补充
- **争议** → PM 与美术对判定有分歧时,由用户最终裁决
---
### ⑥ 程序实现(程序)
**目的**:基于程序设计方案和美术资产,实际编写代码。
**前置条件**:程序设计(④)已就绪且 PM 审查通过,美术资产(⑤)已就位且 PM 验收通过。
**白模搭建子任务(6a)**:
当版本开发涉及场景搭建时,⑥的第一个子任务是白模关卡搭建:
- 读取关卡执行案 `level-v<XX>-<name>-execution.md` 和场景设计案
- 使用 `Content/BlockingStarterPack/` 资产在 UE5 编辑器中搭建白模关卡
- 按区域分文件夹组织 Actor(命名规范:`LVL_<ZoneID>_<Category>_<Description>`)
- 完成比例验证(角色可通过门洞、摄像机不穿墙、流线通行)
- 产出搭建报告 `docs/technical/level-v<XX>-build-report.md` 记录偏差
- **搭建报告必须以评论形式发布到对应任务 Issue**(与本地文件内容一致)
- 完整搭建流程参见 `docs/design/prototype/level-design-workflow.md` 第四章
**阶段差异**:
| 维度 | 原型阶段 | 批量生产阶段 |
|------|----------|-------------|
| 代码质量 | 允许硬编码、快速实现 | 不允许硬编码,数据配置化,代码达发布标准 |
| 验证 | 核心流程跑通即可 | 每个系统独立验证 + 集成验证 |
**实现完成后进入 PM 程序实现验收门,验收通过后进入⑦版本复盘。**
---
### PM 审查:程序实现验收门
**触发时机**:程序完成实现(⑥)后,进入版本复盘(⑦)之前。
**审查人**:PM
**审查内容**:
1. **设计一致性**:实现是否与程序设计方案(④)一致?逐条对照
2. **执行案覆盖度**:最终实现是否完整覆盖了执行案(③)中的所有行为规则?
3. **资产使用确认**:美术资产是否已正确集成?
4. **完成度评估**:当前版本的实现是否达到了阶段要求的完成度?
5. **偏差梳理**:实现中产生的偏差是否已梳理,偏差等级是否合理?
**偏差处理规则**:
| 偏差等级 | 判定标准 | 处理方式 |
|----------|----------|----------|
| **轻微** | 不影响核心行为,仅实现细节差异 | 记录在偏差清单,在⑦版本复盘时同步文档 |
| **重大** | 影响核心行为或与设计方案严重不一致 | PM 建议回退到③重新出执行案或开启新版本修正,由用户决定 |
**判定**:
- **通过** → 进入 ⑦ 版本复盘
- **不通过(轻微)** → 程序补充修正后重新验收
- **不通过(重大)** → PM 提出处理建议(回退步骤或开新版本),由用户最终决定
- **争议** → PM 与程序对判定有分歧时,由用户最终裁决
**产出**:PM 审查记录(记录在 `docs/plans/reviews/<任务ID>-review.md`)
---
### ⑦ 版本复盘(程序汇报 → PM 协调 → 策划更新)
**目的**:确保文档与实现一致,为下一个迭代提供准确的起点。
**触发时机**:PM 程序实现验收门通过后。
**流程**:
1. **程序汇报**:程序对照执行案,列出实现中产生的偏差和调整
2. **PM 协调**:PM 汇总程序汇报的偏差清单,协调策划进行文档更新
3. **策划更新**:策划根据偏差清单,更新设计案和执行案
4. **标记状态**:执行案迭代记录中该版本标记为 ✅ 已同步
**必须更新的内容**:
- 设计案中与实际不符的系统描述
- 执行案中与实际实现不一致的行为规则
- 执行案迭代记录中新增一行,记录变更
**阶段差异**:
| 维度 | 原型阶段 | 批量生产阶段 |
|------|----------|-------------|
| 偏差记录 | 核心行为的偏差必须记录 | 所有差异必须记录在案 |
| 文档同步 | 核心系统必须同步 | 所有系统必须同步 |
#### 为什么必须复盘
- 文档是下一个迭代的起点。如果文档和实现不一致,下一个迭代会基于错误假设
- PM 的质量门审查基于文档。文档不准确会导致审查无效
- 程序在实现时可能发现设计的问题并做了调整,这些调整必须反映到文档中
---
## PM 诊断检查清单
PM 在每次 `/plan` 调用时,先判断宏观阶段,再针对**当前版本**逐项检查流水线状态。
### 宏观阶段判断
| 检查项 | 判断方式 | 结果 |
|--------|----------|------|
| 项目处于哪个开发阶段? | 检查 `concept.md` → 检查核心玩法是否已有可玩原型 → 检查内容是否已完整 | 草案 / 原型 / 批量生产 / 测试发布 |
### 版本流水线检查
| 检查项 | 检查方式 | 通过条件 |
|--------|----------|----------|
| ① 设计案是否存在且完整? | 检查 `docs/design/<阶段>/` 中的设计文档和索引 | 覆盖所有主要系统,有索引串联 |
| ② 阶段路线图是否存在? | 检查 `docs/plans/<阶段>-roadmap.md` | 版本列表、每版本目标、递进关系明确 |
| ③ 执行案是否齐全? | 检查 `docs/design/<阶段>/` 对应执行案 | 每个涉及系统的核心行为规则都已明确 |
| **PM 审查**:执行案质量门 | 对照质量标准逐项检查 | 完整性、清晰性、边界情况通过 |
| ④ 程序设计方案是否齐全? | 检查 `docs/technical/` 对应文档 | 每个执行案都有对应技术方案 |
| **PM 审查**:程序设计质量门 | 执行案与技术方案逐条对照 | 行为规则全覆盖,美术需求具体,工作量与拆分建议明确 |
| ⑤ 美术资产是否就位? | 检查资源需求清单 vs 实际资产 | 所有必需资产已产出或有处理方案 |
| **PM 审查**:美术资产验收门 | 需求清单 vs 实际资产逐项对照 | 每项需求都有对应产出 |
| ⑥ 程序实现是否完成? | 检查实际文件 | 实现完成 |
| **PM 审查**:程序实现验收门 | 实现与设计方案逐条对照 + 偏差分级处理 | 设计一致,偏差已处理或有明确方案 |
| ⑦ 版本复盘是否完成? | 检查执行案迭代记录是否 ✅ 已同步 | 策划已根据实现偏差更新文档 |
**遇到第一个未通过的即为当前应推进的步骤。**
**注意**:所有质量门按阶段对应标准严格检查。用户可根据实际情况决定跳过特定检查项,PM 记录用户的跳过决策及理由。
---
## 文档质量检查标准
以下标准对应流水线中的三道 PM 质量门。
### 执行案质量标准(对应「执行案质量门」)
**完整性检查**:
- [ ] 是否覆盖了该系统所有玩家可感知的行为?
- [ ] 每个操作是否明确了触发条件、触发后表现、结束条件?
- [ ] 边界情况是否都处理了?
- [ ] 与其他系统的关联是否说明?
**清晰性检查**:
- [ ] 是否有模糊地带?
- [ ] 行为描述是否无歧义?
- [ ] 程序拿到后能否不问策划任何问题就实现功能?
**边界检查**:
- [ ] 是否误入了技术细节?
- [ ] 精确参数值是否标注为"建议值,可调"而非固定值?
**迭代记录检查**:
- [ ] 是否有模糊地带?行为描述是否无歧义?
- [ ] 是否误入了技术细节?参数值是否标注为"建议值,可调"?
- [ ] 迭代记录表头是否存在且填写完整?
- [ ] 已完成版本的文档是否已同步(✅ 已同步)?
### 程序设计质量标准(对应「程序设计质量门」)
### 程序设计质量标准
- [ ] 是否完整回应了执行案中的每条行为规则?(逐条对照)
- [ ] 美术资源需求是否列得具体(尺寸、数量、触发时机)?
@@ -714,29 +260,19 @@ PM 在每次 `/plan` 调用时,先判断宏观阶段,再针对**当前版本
- [ ] 技术风险是否识别?
- [ ] 工作量估算是否给出?实现拆分建议是否合理且可操作?
### 美术资产验收标准(对应「美术资产验收门」)
### 美术资产验收标准
- [ ] 资源需求清单中的每一项是否都有对应的美术产出?(逐项对照)
- [ ] 资产文件是否已放置在对应目录?
- [ ] 资产路径是否记录清晰?
- [ ] 资产规格是否符合需求?
- [ ] 未到位资产是否有明确的处理方案?
### 程序实现验收标准(对应「程序实现验收门」)
### 程序实现验收标准
- [ ] 实现是否与程序设计方案一致?(逐条对照)
- [ ] 最终实现是否完整覆盖了执行案中的所有行为规则?
- [ ] 美术资产是否已正确集成并使用?
- [ ] 当前版本的实现是否达到了阶段要求的完成度?
- [ ] 偏差清单是否已梳理清楚?
- [ ] 重大偏差是否有明确的处理方案(回退步骤或开新版本)?
### 版本复盘标准(对应「⑦版本复盘」)
- [ ] 程序是否汇报了实现偏差清单?
- [ ] PM 是否审核了偏差清单并给出协调意见?
- [ ] 策划是否根据协调结果更新了设计案/执行案?
- [ ] 执行案迭代记录是否新增一行并标记 ✅ 已同步?
- [ ] 偏差清单是否已梳理清楚?重大偏差是否有处理方案?
---
@@ -746,4 +282,4 @@ PM 在每次 `/plan` 调用时,先判断宏观阶段,再针对**当前版本
- **精力有限**——避免一次性拆分太多并行任务
- **心流重要**——建议按「完整做完一个小系统」而非「同时推进多个系统」的方式工作
- **早期验证**——越早能跑起来看到东西,越不容易失去动力
- **文档交接**——Agent 之间通过文档交接,PM 是信息枢纽
- **文档交接**——Agent 之间通过文档交接,PM 是进度调度器,reviewer 负责质量把关
+7 -7
View File
@@ -2,7 +2,7 @@
> 本模板供策划产出关卡执行案时使用。关卡执行案是场景设计案的"空间精度版本"——将氛围描述转化为带有坐标、尺寸和资产建议的结构化搭建指南,供程序在 UE5 编辑器中搭建白模。
>
> 文件命名:`level-v<XX>-<name>-execution.md`(如 `level-v01-suburban-execution.md`)
> 文件命名:`关卡-v<XX>-<名称>-执行案.md`(如 `关卡-v01-城郊街区-执行案.md`)
> 存放位置:`docs/design/<阶段>/`(与其他执行案同目录)
---
@@ -49,7 +49,7 @@
| ... | | | | | |
| Zn | | | | | |
> 标识色使用 BSP 材质名(参见 `blockout-asset-reference.md`),程序用对应材质标识区域。
> 标识色使用 BSP 材质名(参见 `白模资产参考.md`),程序用对应材质标识区域。
### 2.2 交通流线
@@ -119,7 +119,7 @@
- 填写 BSP 资产类别名和组合方式(如 "Wall_400x300 × 2 叠加")
- 程序负责映射到引擎中的具体资产路径
- 如无精确匹配,写"Primitives 组合"并注明替代意图
- 参见 `.claude/refs/blockout-asset-reference.md` 中的替代方案
- 参见 `.claude/refs/白模资产参考.md` 中的替代方案
#### 3.x.3 布景物清单
@@ -137,7 +137,7 @@
| 交互编号 | 物件名称 | 交互类型 | 相对原点偏移 (ΔX,ΔY,ΔZ) | 触发半径 (cm) | 引用 |
|---------|---------|---------|----------------------|-------------|------|
| Zx-I01 | <!-- 如 旧冰箱 --> | <!-- 如 观察 --> | (200,400,10) | <!-- 如 150 --> | interaction-system-execution #n |
| Zx-I01 | <!-- 如 旧冰箱 --> | <!-- 如 观察 --> | (200,400,10) | <!-- 如 150 --> | 交互系统-执行案 #n |
| Zx-I02 | | | | | |
| ... | | | | | |
@@ -150,7 +150,7 @@
| 收集编号 | 物品名称 | 稀有度 | 相对原点偏移 (ΔX,ΔY,ΔZ) | 引用 |
|---------|---------|--------|----------------------|------|
| Zx-C01 | <!-- 如 彩色弹珠 --> | <!-- 如 普通/稀有 --> | (500,200,0) | collection-system-execution #n |
| Zx-C01 | <!-- 如 彩色弹珠 --> | <!-- 如 普通/稀有 --> | (500,200,0) | 收集系统-执行案 #n |
| Zx-C02 | | | | |
| ... | | | | | |
@@ -187,7 +187,7 @@
| # | 物品名称 | 所在区域 | 绝对坐标 (X,Y,Z) | 稀有度 | 引用 |
|---|---------|---------|-----------------|--------|------|
| 1 | | | | | collection-system-execution #n |
| 1 | | | | | 收集系统-执行案 #n |
| 2 | | | | | |
| ... | | | | | |
@@ -240,6 +240,6 @@
### 与场景设计案的关系
- 场景设计案(`scene-vXX-<name>.md`)= **氛围愿景** — 描述感觉、氛围、叙事意图
- 场景设计案(`场景-v<XX>-<名称>.md`)= **氛围愿景** — 描述感觉、氛围、叙事意图
- 关卡执行案 = **空间精度搭建指南** — 坐标、尺寸、资产映射
- 两者是互补关系,不是替代关系。程序搭建时两个文档都需要参考。
+82 -60
View File
@@ -33,6 +33,7 @@ description: "Gitea 协作工具 — CLI 命令速查、认证机制、Assignee
| 程序 | `dev` | game-dev |
| 美术 | `artist` | (暂无独立 Agent) |
| PM | `pm` | game-pm |
| 审查 | `reviewer` | game-reviewer |
---
@@ -46,12 +47,13 @@ description: "Gitea 协作工具 — CLI 命令速查、认证机制、Assignee
| 程序 | `ai-programmer` | `P-XXX` |
| 美术 | `ai-artist` | `A-XXX` |
| PM | `ai-pm` | (管理任务) |
| 审查 | `ai-reviewer` | (审查操作,不拥有任务) |
---
## 4. 标签体系
14 个标准标签,通过 `node scripts/gitea.js label init` 初始化。
19 个标准标签,通过 `node scripts/gitea.js label init` 初始化。
### status(任务状态)
| 标签 | 含义 |
@@ -60,22 +62,33 @@ description: "Gitea 协作工具 — CLI 命令速查、认证机制、Assignee
| `status:ready` | 已就绪,可开始 |
| `status:in-progress` | 进行中 |
| `status:blocked` | 被阻塞 |
| `status:in-review` | 审查中 |
| `status:in-review` | 待审查(角色完成工作后设置) |
| `status:done` | 已完成 |
### gate(质量门判定)
### gate(质量门判定 — 旧标签,保留用于历史 Issue)
| 标签 | 含义 |
|------|------|
| `gate:execution-pass` | 执行案质量门通过 |
| `gate:execution-fail` | 执行案质量门未通过 |
| `gate:tech-pass` | 程序设计质量门通过 |
| `gate:tech-fail` | 程序设计质量门未通过 |
| `gate:art-pass` | 美术资产验收门通过 |
| `gate:art-fail` | 美术资产验收门未通过 |
| `gate:impl-pass` | 程序实现验收门通过 |
| `gate:impl-fail` | 程序实现验收门未通过 |
| `gate:execution-pass/fail` | 执行案质量门 |
| `gate:tech-pass/fail` | 程序设计质量门 |
| `gate:art-pass/fail` | 美术资产验收门 |
| `gate:impl-pass/fail` | 程序实现验收门 |
> **注**:旧 Issue 上可能仍有 `pipeline:*` 和 `role:*` 标签,这些已不再使用,保留不管即可。
> 新工作流使用 `review:*` 标签替代 `gate:*`。旧 Issue 上的 gate 标签保留不动。
### review(审查结果 — 新标签)
| 标签 | 含义 |
|------|------|
| `review:pass` | 审查已通过 ✅ |
| `review:fail` | 审查未通过 ❌ |
### stage(开发阶段 — 新标签)
| 标签 | 含义 |
|------|------|
| `stage:sketch` | 草图阶段任务 |
| `stage:roadmap` | 总路线阶段任务 |
| `stage:iteration` | 迭代阶段任务 |
> **注**:`pipeline:*` 和 `role:*` 标签已废弃并从 Gitea 上删除。如旧 Issue 上仍残留这些标签,忽略即可。
---
@@ -172,14 +185,16 @@ node scripts/gitea.js test
| 概念 | Gitea 对应 | 说明 |
|------|-----------|------|
| 任务 (P-002) | Issue | 标题含任务 ID 前缀 |
| 版本 (v0.1) | Milestone | 版本号即里程碑标题 |
| 阶段/版本 | Milestone | 阶段名或版本号作为里程碑标题 |
| 任务状态 | Label (`status:*`) | 6 种状态标签 |
| 质量门结果 | Label (`gate:*`) | 8 种判定标签 |
| 审查结果 | Label (`review:*`) | 通过/未通过 |
| 开发阶段 | Label (`stage:*`) | 草图/总路线/迭代 |
| 质量门结果 | Label (`gate:*`) | 旧标签,保留用于历史 Issue |
| 角色归属 | Assignee | 通过 `--assignees` 指派 |
| 流水线步骤 | Pipeline Tracker | Meta Issue 记录全局流水线状态 |
| 当前进度 | Pipeline Tracker | Meta Issue 记录当前阶段和进度 |
| 角色汇报 | Issue 评论 | 各角色在对应 Issue 下评论 |
| 审查记录 | Issue 评论(摘要+链接)+ 本地 `docs/plans/reviews/` | 本地保留详细结构化文档 |
| 任务归档 | close Issue | 不需要本地 archive 文件 |
| 审查记录 | Issue 评论(摘要+链接)+ 本地 `docs/plans/reviews/` | 本地文件名格式 `<任务ID>-审查记录.md` |
| 任务归档 | close Issue | 用户确认后由 PM 关闭 |
---
@@ -199,11 +214,34 @@ node scripts/gitea.js test
## 8. Pipeline Tracker
Pipeline Tracker 是一个 **Meta Issue**,用于记录当前版本的全局流水线状态。
Pipeline Tracker 是一个 **Meta Issue**,用于记录项目当前阶段和进度。
- 初始化:`pipeline init`(阶段开始时执行一次)
### Tracker 正文格式
```markdown
# 流水线进度追踪
## 当前阶段
草图阶段 / 总路线阶段 / 迭代阶段
## 当前里程碑
- 名称: <milestone title>
- 状态: open/closed
- Open Issues: X
- Closed Issues: Y
## 阶段历史
| 阶段 | 里程碑 | 进入日期 | 完成日期 |
|------|--------|----------|----------|
| 草图阶段 | 草图阶段 | YYYY-MM-DD | — |
```
### 相关命令
- 初始化:`pipeline init`
- 查看状态:`pipeline status`
- 更新步骤:`pipeline update <number> --step <1-7> --status "描述"`
- 阶段转换时直接编辑 Issue 正文更新「当前阶段」和「阶段历史」
---
@@ -258,31 +296,15 @@ node scripts/gitea.js --role <role> issue comment <number> --body "## 修改汇
- path/to/output"
```
### PM 审查后
### PM 确认关闭任务(审查通过后)
```bash
# 发布审查摘要评论
node scripts/gitea.js --role pm issue comment <number> --body "## 质量门审查 [通过✅/未通过❌]
**质量门**:[类型]
**审查日期**:YYYY-MM-DD
**审查记录**:docs/plans/reviews/<task-ID>-review.md
### 结论
[一句话总结]
### 问题清单(不通过时)
1. ..."
# 更新标签反映质量门结果
node scripts/gitea.js --role pm issue labels <number> --set <gate-pass|gate-fail>,<status-*>,...
# 审查通过时关闭 Issue
# 用户确认后关闭 Issue
node scripts/gitea.js --role pm issue close <number> --body "## 任务完成 ✔
**完成日期**:YYYY-MM-DD
**产出**:path/to/output
**审查记录**:docs/plans/reviews/<task-ID>-review.md"
**审查记录**:docs/plans/reviews/<task-ID>-审查记录.md"
```
---
@@ -292,17 +314,22 @@ node scripts/gitea.js --role pm issue close <number> --body "## 任务完成 ✔
PM 在每次会话启动时按序执行:
```bash
# 1. 读取当前里程碑
node scripts/gitea.js --role pm milestone list --state open
# 2. 读取活跃任务
node scripts/gitea.js --role pm issue list --milestone <当前版本>
# 3. 读取流水线状态
# 1. 读取 Pipeline Tracker(确定当前阶段)
node scripts/gitea.js --role pm pipeline status
# 4. 读取近期评论(按需)
node scripts/gitea.js --role pm issue comments <number>
# 2. 读取当前里程碑
node scripts/gitea.js --role pm milestone list --state open
# 3. 读取活跃任务
node scripts/gitea.js --role pm issue list --state open
# 4. 根据阶段调用对应技能:
# 无进度 → project-initializer
# 草图阶段 → stage-sketch
# 总路线阶段 → stage-roadmap
# 迭代阶段 → stage-iteration
# 5. 检查 review:pass 标签的 Issue → 请示用户是否关闭
```
---
@@ -312,30 +339,25 @@ node scripts/gitea.js --role pm issue comments <number>
### 创建任务
```bash
# 创建 Issue(必须指定 assignees)
# 创建 Issue(必须指定 assignees 和 stage 标签)
node scripts/gitea.js --role pm issue create \
--title "D-001 交互系统执行案" \
--body "## 任务信息\n- **任务ID**: D-001\n- **所属版本**: v0.1\n- **角色**: 策划\n- **预期产出**: docs/design/prototype/interaction-system-execution.md\n..." \
--title "D-001 概念设计" \
--body "## 任务信息\n- **任务ID**: D-001\n- **所属阶段**: 草图阶段\n- **角色**: 策划\n- **预期产出**: docs/design/概念草案.md\n..." \
--milestone <id> \
--assignees ai-designer
# 设置标签
node scripts/gitea.js --role pm issue labels <number> --set <status-label-id>,<gate-label-id>
# 设置标签(status + stage + role)
node scripts/gitea.js --role pm issue labels <number> --set <status-label-id>,<stage-label-id>,<role-label-id>
# 设置依赖
node scripts/gitea.js --role pm issue block <blocker_number> <new_number>
```
### 关闭任务
### 关闭任务(用户确认后)
```bash
node scripts/gitea.js --role pm issue close <number> --body "## 任务完成 ✔\n**完成日期**: YYYY-MM-DD\n**产出**: path/to/output"
```
### 更新流水线
```bash
node scripts/gitea.js --role pm pipeline update <tracker_number> --step <1-7> --status "状态描述"
# 审查通过 + 用户确认关闭后
node scripts/gitea.js --role pm issue close <number> --body "## 任务完成 ✔\n**完成日期**: YYYY-MM-DD\n**产出**: path/to/output\n**审查记录**: docs/plans/reviews/<task-ID>-审查记录.md"
```
---
+274
View File
@@ -0,0 +1,274 @@
---
name: "level-builder"
description: >
白模关卡自动生成工具。当需要"搭建白模关卡"、"生成白模"、"Level Builder"、
"白模搭建"、"自动摆放 Actor"、"解析搭建指南"、"parse build guide"时使用。
从搭建指南 Markdown 自动解析、验证、在 UE5 编辑器中构建白模关卡并生成搭建报告。
---
# 白模关卡自动生成工具 (Level Builder)
> 从搭建指南 Markdown → JSON 中间数据 → UE5 Actor 自动构建 + 搭建报告生成
## 触发条件
- **game-dev 白模搭建任务**:收到步骤⑥子任务 6a(白模搭建)时使用
- **用户手动触发**:用户说"搭建白模"、"生成白模关卡"、"Level Builder"等
- **修改关卡后重新生成**:搭建指南有更新,需要重新生成白模
---
## 工具位置
```
tools/level-builder/
main.py # CLI 入口
parsers/
markdown_table.py # 通用 Markdown 表格解析
build_guide_parser.py # 搭建指南解析器(核心)
config/
asset_mapping.py # 37 种 BSP 资产 → UE5 路径
material_mapping.py # 13 种标识色材质路径
zone_config.py # 7 区域定义 + Outliner 结构
substitution_rules.py # Primitives 替代规则
builder/
scene_builder.py # UE5 Python API Actor 构建
validation/
asset_validator.py # 离线资产验证
report/
report_generator.py # 搭建报告生成
output/ # 生成物目录
```
---
## 前置条件
1. **PythonScriptPlugin 已启用** — `ChildHood.uproject` 中已配置
2. **BlockingStarterPack 已导入** — `Content/BlockingStarterPack/` 目录存在
3. **搭建指南已就绪** — `docs/technical/关卡-v<XX>-搭建指南.md` 存在
---
## 使用流程
### 第一步:解析搭建指南(离线,Claude 可执行)
将搭建指南 Markdown 解析为 JSON 中间数据:
```bash
cd tools/level-builder
python main.py parse -i ../../docs/technical/关卡-v01-搭建指南.md -o output/level_v01.json
```
**预期输出**:
```
[INFO] level_builder: 解析搭建指南: ...
[INFO] level_builder: 解析完成: 265 个 Actor
[INFO] level_builder: 按类别: {'STR': 123, 'PROP': 53, ...}
[INFO] level_builder: 唯一资产数: 37
```
**生成文件**: `output/level_v01.json`
### 第二步:验证(离线,Claude 可执行)
验证 JSON 中所有资产引用是否有效:
```bash
python main.py validate -i output/level_v01.json
```
**预期输出**:
```
[INFO] level_builder: ✅ 验证通过!
[INFO] level_builder: 有效 Actor: 265/265
```
**生成文件**: `output/validation_result.json`
### 第三步:UE5 构建
有两种方式,**推荐使用命令行方式**(全自动,无需手动操作编辑器):
#### 方式 A:命令行一键构建(推荐)
```bat
REM 从项目根目录运行
tools\level-builder\build_level.bat
```
**高级用法**:
```bat
build_level.bat --json output/v02.json --map LVL_SuburbanV02 # 指定 JSON 和关卡名
build_level.bat --no-quit # 构建后保留编辑器
build_level.bat --no-null-rhi # 显示编辑器窗口(调试)
build_level.bat --dry-run # 只打印命令不执行
```
**环境变量**:
- `UE5_ROOT` — UE5 安装目录(未设置时自动检测 5.7/5.6 常见路径)
**预期输出**:
```
[LevelBuilder] 加载 265 个 Actor 定义
[LevelBuilder] SUCCESS: 265 | FAILED: 0 | TOTAL: 265
[LevelBuilder] ✅ 关卡已保存: /Game/Maps/LVL_SuburbanV01
```
**生成文件**:
- `Content/Maps/LVL_SuburbanV01.umap` — UE5 关卡文件
- `tools/level-builder/output/build_log.json` — 构建日志
#### 方式 B:编辑器内手动执行
在 UE5 编辑器 Python 控制台中:
```
方式 B1 — Output Log Python 控制台:
exec(open(r"tools/level-builder/build_in_editor.py").read())
方式 B2 — File > Execute Python Script > 选择 build_in_editor.py
方式 B3 — CLI 方式(仅构建指定区域):
exec(open(r"tools/level-builder/main.py").read())
main(["build", "-i", "output/level_v01.json", "--zones", "Z1,Z3"])
```
### 第四步:生成搭建报告(离线,Claude 可执行)
```bash
python main.py report -i output/level_v01.json -l output/build_log.json -o output/搭建报告.md
```
**生成文件**: `output/搭建报告.md`(Markdown 格式,含逐区域对照表)
---
## 输出说明
### JSON 中间数据结构
每个 Actor 的数据格式:
```json
{
"actor_name": "LVL_Z1_STR_SouthWall_01",
"asset_key": "Wall_400x300",
"asset_type": "static_mesh",
"category": "STR",
"zone_id": "Z1",
"location": [200, 0, 0],
"rotation": [0, 0, 0],
"scale": [1, 1, 1],
"material": "MI_DevRed",
"folder": "LVL_SuburbanV01/Z1_家/Structure",
"note": ""
}
```
特殊 asset_type:
- `player_start` — PlayerStart(重生点)
- `directional_light` — DirectionalLight(太阳光)
- `sky_light` — SkyLight(天空光)
- `point_light` — PointLight(点光源)
### Actor 命名规范
`LVL_<ZoneID>_<Category>_<Description>`
| Category | 含义 | 示例 |
|----------|------|------|
| STR | 结构体 | `LVL_Z1_STR_SouthWall_01` |
| PROP | 布景 | `LVL_Z3_PROP_StreetLamp_01` |
| INTR | 交互物 | `LVL_Z1_INTR_Fridge_01` |
| COLL | 收集品 | `LVL_Z3_COLL_GlassBead_01` |
| BND | 边界 | `LVL_BND_East_01` |
| LGT | 光源 | `LVL_LGT_DirectionalLight` |
### Outliner 文件夹结构
```
LVL_SuburbanV01/
Z1_家/
Structure/
Props/
Interactables/
Collectibles/
Z2_小卖部/
...
Global/
Boundaries/
Lighting/
```
---
## 新版本适配
当有新版本的关卡执行案时,需要:
1. **策划产出执行案** → `docs/design/<stage>/关卡-v<XX>-<名称>-执行案.md`
2. **程序产出搭建指南** → `docs/technical/关卡-v<XX>-搭建指南.md`
- 搭建指南格式参照 `docs/technical/关卡-v01-搭建指南.md`
- 包含:资产映射表 + Phase A(地面) + Phase B(边界) + Phase C(逐区域) + Phase D(光照)
3. **运行 Level Builder** → 按上述四步流程执行
### 如果需要添加新 BSP 资产
编辑 `config/asset_mapping.py`,添加条目:
```python
"新资产名": {
"path": f"{MESH_ROOT}/分类目录/SM_资产名.SM_资产名",
"default_size": (宽, 深, 高), # cm
"category": "wall|floor|stair|door|primitive|...",
},
```
### 如果需要添加新区域
编辑 `config/zone_config.py`,在 `ZONES` 字典中添加区域定义。
---
## 故障排查
| 问题 | 原因 | 解决 |
|------|------|------|
| `No module named 'unreal'` | Stage 2 在编辑器外运行 | 只在 UE5 编辑器 Python 控制台中执行 build 命令 |
| 资产加载失败 (spawn returned None) | BSP 资产未导入或路径错误 | 检查 `Content/BlockingStarterPack/` 是否存在 |
| Actor 数量与预期不符 | 搭建指南格式变化 | 检查 `parsers/build_guide_parser.py` 的解析逻辑 |
| PythonScriptPlugin 未加载 | .uproject 修改后未重启编辑器 | 重启 UE5 编辑器 |
| 坐标表达式解析失败 | `200+400n` 格式的范围行 | 检查 `_expand_range_row` 和 `_eval_coord_expr` |
---
## 配置参考
### 区域一览
| Zone ID | 名称 | 原点 | 尺寸 | 标识色 |
|---------|------|------|------|--------|
| Z1 | 家 | (0,0,0) | 1000×800 | Red |
| Z2 | 小卖部 | (1000,200,0) | 600×600 | Orange |
| Z3 | 主街道 | (0,800,0) | 5000×500 | Gray |
| Z4 | 空地 | (3500,1300,0) | 1500×1200 | Brown |
| Z5 | 巷子 | (2000,0,0) | 600×800 | Green |
| Z6 | 水塘边 | (5500,0,0) | 1500×1000 | Blue |
| Z7 | 老居民楼 | (5000,1300,0) | 1500×1200 | Yellow |
### 搭建报告发布
搭建报告需要双轨发布:
1. **本地文件**: `docs/technical/关卡-v<XX>-搭建报告.md`
2. **Issue 评论**: 在对应任务 Issue 下以评论形式发布(供 PM 和策划直接查看)
---
## 未来扩展
- **执行案解析器** — 直接从 `关卡-v<XX>-<名称>-执行案.md` 解析,跳过手动搭建指南
- **蓝图支持** — 除 StaticMesh 外支持 Blueprint Actor 放置
- **碰撞预设** — 自动为不同 Category 设置碰撞 Profile
- **LOD 标记** — 为远距离布景标记 LOD 策略
+137
View File
@@ -0,0 +1,137 @@
---
name: "project-initializer"
description: "项目初始化技能 — 检查版本管理/项目管理配置、初始化 Gitea 标签/里程碑/任务。仅在项目无进度时由 PM 调用。"
---
# 项目初始化
> 本技能处理项目冷启动。在 PM 上下文中运行,完成后 PM 直接感知初始化结果。
> 仅在无 Pipeline Tracker 或无里程碑时触发。
---
## 初始化流程
### 步骤 1:版本管理确认
逐项确认以下内容:
1. **SSH 仓库地址**:确认 `git remote -v` 中的仓库地址正确
2. **角色 Gitea 账号**:确认以下账号存在且可访问
- `starmen1265`(用户)
- `ai-pm`
- `ai-designer`
- `ai-programmer`
- `ai-artist`
- `ai-reviewer`
3. **SSH key 配置**:运行 `ssh -T git@micecola.top -p 2223` 测试连接
- 失败 → 引导用户生成并配置 SSH key
### 步骤 2:项目管理配置
确认以下配置就绪:
1. **Token 文件**:`scripts/.gitea-tokens.json` 存在且包含所有角色 token
- `pm`、`designer`、`dev`、`artist`、`reviewer`
2. **Token 文件已忽略**:`.gitignore` 中包含 `scripts/.gitea-tokens.json`
3. **Gitea CLI 可用**:`node scripts/gitea.js --role pm test` 运行通过
缺失时引导用户配置。
### 步骤 3:现有代码与需求检查
**情况 A:有现有代码和需求**
- 探索 `docs/` 和 `Content/` 目录结构
- 读取现有设计文档
- 生成迁移方案:
- 哪些文档可直接映射到新阶段
- 哪些需要拆分或重写
- 哪些可以作为已有产出直接关闭任务
- 将迁移方案提交用户确认后执行
**情况 B:无现有代码(全新项目)**
- 建议用户手动准备开发环境:
1. 安装 UE 5.7.4
2. 创建第三人称/第一人称/顶视角模板项目
3. 添加插件:BlockoutStarterPack、LowpolyCharacter
4. 启用 PythonEditorPlugins 插件
- 等待用户确认环境就绪
### 步骤 4:Gitea 初始化
依次执行(所有操作需用户确认后再执行):
```bash
# 1. 创建标准标签
node scripts/gitea.js --role pm label init
# 2. 创建 Pipeline Tracker Issue
node scripts/gitea.js --role pm issue create \
--title "Pipeline Tracker" \
--body "# 流水线进度追踪
## 当前阶段
草图阶段
## 当前里程碑
- 名称: 草图阶段
- 状态: open
- Open Issues: 1
- Closed Issues: 0
## 阶段历史
| 阶段 | 里程碑 | 进入日期 | 完成日期 |
|------|--------|----------|----------|
| 草图阶段 | 草图阶段 | $(date +%Y-%m-%d) | — |" \
--labels "stage:sketch,role:pm,status:in-progress"
# 3. 创建草图阶段里程碑
node scripts/gitea.js --role pm milestone create \
--title "草图阶段" \
--description "概念设计与技术可行性验证"
# 4. 创建第一个任务:概念设计
node scripts/gitea.js --role pm issue create \
--title "D-001 概念设计" \
--body "## 任务信息
- **任务ID**: D-001
- **所属阶段**: 草图阶段
- **角色**: 策划
## 预期产出
产出概念草案 \`docs/design/概念草案.md\`,包含以下 7 个必填字段:
1. 一句话概述(这是什么游戏)
2. 核心玩法(玩家做什么)
3. 美术风格
4. 目标平台
5. 灵感来源
6. 核心假设(为什么好玩)
7. 范围约束(MVP 包含什么)
## 质量标准
- 每个字段有明确的、不含糊的描述
- 美术风格有参考图或参考作品
- 核心假设可验证(能通过原型验证)" \
--milestone "草图阶段" \
--labels "stage:sketch,role:design,status:ready" \
--assignees "ai-designer"
```
### 步骤 5:确认完成
初始化完成后向用户报告:
- ✅ 版本管理配置状态
- ✅ 项目管理配置状态
- ✅ Gitea 初始化结果(标签数、Pipeline Tracker Issue 编号、里程碑、首个任务编号)
- 📋 建议下一步:启动策划角色开始概念设计
---
## 注意事项
- 每一步都需要用户确认后才执行,不自动推进
- 如果中途失败(如 Gitea 连接失败),报告具体错误并建议修复方案
- 初始化完成后,PM 进入草图阶段,后续由 stage-sketch 技能接管
+25 -29
View File
@@ -1,19 +1,19 @@
---
name: "quality-gate-review"
description: "质量门审查技能 — 4 道质量门标准、阶段对应标准、审查流程、强制操作清单"
description: "审查标准技能 — 4 道质量门标准、阶段对应标准、审查流程、强制操作清单。供 game-reviewer 子代理使用。"
---
# 质量门审查
# 审查标准
> 本技能定义 4 道质量门的审查标准、阶段对应标准和审查后强制操作。
> 供 game-reviewer agent、game-pm agent 及各工种自审使用。
> 供 **game-reviewer 子代理**使用——角色完成工作后启动 reviewer 子代理,reviewer 按此标准审查。
> 项目管理工具操作细节参见 `.claude/skills/gitea-collab.md`。
---
## 审查流程
每次质量门审查必须严格按以下步骤执行:
每次审查必须严格按以下步骤执行:
### 1. 读取交付物
- 从对应 Issue 的评论中获取产出文件路径
@@ -24,21 +24,23 @@ description: "质量门审查技能 — 4 道质量门标准、阶段对应标
- 根据当前开发阶段选择对应标准(原型 vs 批量生产,见"阶段对应标准")
### 3. 写审查记录(本地文件)
- 路径:`docs/plans/reviews/<task-ID>-review.md`
- 路径:`docs/plans/reviews/<task-ID>-审查记录.md`
- 格式见下方"审查记录格式"
### 4. Issue 评论(强制)
- **无论通过或不通过,都必须在对应 Issue 上发布审查结论评论**
- 使用 `--role reviewer` 操作
- 评论中标注审查者身份:`审查 Agent (ai-reviewer)`
- 无论通过或不通过,都必须在对应 Issue 上发布审查结论评论
- 通过:摘要 + 通过结论
- 不通过:摘要 + 具体问题清单
### 5. 更新标签
- 通过:设置 `gate:*-pass` 标签,移除 `gate:*-fail`(如有)
- 不通过:设置 `gate:*-fail` 标签,移除 `gate:*-pass`(如有)
- 通过:设置 `review:pass` 标签,移除 `status:in-review`
- 不通过:设置 `review:fail` 标签,移除 `status:in-review`,改为 `status:blocked`
### 6. 通过时关闭 Issue(强制)
- **审查通过时必须关闭 Issue**,附完成说明
- 不通过时 Issue 保持打开,等待修改
### 6. 返回结果给启动角色
- 不关闭 Issue(关闭决策权在用户)
- 将审查结果返回给启动你的角色,由该角色向用户汇报
---
@@ -58,8 +60,6 @@ description: "质量门审查技能 — 4 道质量门标准、阶段对应标
> 关卡执行案的质量门**仅做资产存在验证**。空间精度、比例、跨文档一致性等维度的准确性由程序在白模搭建过程中验证——如果程序在 Issue 中汇报了搭建异常,审查者将异常汇总并汇报给用户确认,不自行判断通过或不通过。
标签:`gate:execution-pass` / `gate:execution-fail`
### 质量门 2:程序设计质量门(④→⑤)
检查维度:
@@ -70,8 +70,6 @@ description: "质量门审查技能 — 4 道质量门标准、阶段对应标
- **技术风险**:是否识别?
- **工作量与拆分建议**:是否给出工作量估算?拆分方式是否合理,每个子任务可独立验证?拆分后的任务集合是否完整覆盖实现方案?
标签:`gate:tech-pass` / `gate:tech-fail`
### 质量门 3:美术资产验收门(⑤→⑥)
检查维度:
@@ -80,8 +78,6 @@ description: "质量门审查技能 — 4 道质量门标准、阶段对应标
- **资产规格**:尺寸和格式是否符合需求?
- **缺失项处理**:未到位资产是否有明确处理方案?
标签:`gate:art-pass` / `gate:art-fail`
### 质量门 4:程序实现验收门(⑥→⑦)
检查维度:
@@ -89,10 +85,8 @@ description: "质量门审查技能 — 4 道质量门标准、阶段对应标
- **执行案覆盖度**:最终实现是否覆盖所有行为规则?
- **资产使用确认**:美术资产是否正确集成?
- **偏差处理**:
- **轻微偏差**(不影响核心行为)→ 记录,在⑦复盘时同步文档
- **重大偏差**(影响核心行为)→ 建议回退到③或开启新版本,用户决定
标签:`gate:impl-pass` / `gate:impl-fail`
- **轻微偏差**(不影响核心行为)→ 记录,在复盘时同步文档
- **重大偏差**(影响核心行为)→ 建议回退或开启新版本,用户决定
---
@@ -109,15 +103,15 @@ description: "质量门审查技能 — 4 道质量门标准、阶段对应标
## 判定规则
- **通过** → 发布通过评论 + 设置 pass 标签 + **关闭 Issue**
- **不通过** → 发布不通过评论(附问题清单)+ 设置 fail 标签 + Issue 保持打开
- **通过** → 发布通过评论 + 设置 `review:pass` 标签 + 返回结果(不关闭 Issue)
- **不通过** → 发布不通过评论(附问题清单)+ 设置 `review:fail` 标签 + 设置 `status:blocked` + 返回结果
- **争议** → 用户最终裁决;审查者提供明确证据和具体文档位置
---
## 审查记录格式
文件路径:`docs/plans/reviews/<task-ID>-review.md`
文件路径:`docs/plans/reviews/<task-ID>-审查记录.md`
```markdown
# [任务 ID] 审查报告
@@ -126,6 +120,7 @@ description: "质量门审查技能 — 4 道质量门标准、阶段对应标
**审查对象**:[任务名] — [产出文档路径]
**宏观阶段**:[原型 / 批量生产]
**质量门类型**:[执行案 / 程序设计 / 美术资产 / 程序实现]
**审查者**:审查 Agent (ai-reviewer)
## 检查结果
@@ -148,8 +143,9 @@ description: "质量门审查技能 — 4 道质量门标准、阶段对应标
审查完成后,**必须**逐项确认以下操作:
- [ ] 在 `docs/plans/reviews/<task-ID>-review.md` 写了审查记录
- [ ] 在对应 Issue 上发布了审查结论评论(通过/不通过 + 摘要)
- [ ] 更新了 Issue 的 gate:* 标签(通过→pass,不通过→fail)
- [ ] **通过时**:关闭 Issue 并附完成说明
- [ ] **不通过时**:评论中包含具体问题清单,Issue 保持打开
- [ ] 在 `docs/plans/reviews/<task-ID>-审查记录.md` 写了审查记录
- [ ] 使用 `--role reviewer` 在对应 Issue 上发布了审查结论评论(标注审查者身份)
- [ ] 更新了 Issue 的标签(通过→`review:pass`,不通过→`review:fail` + `status:blocked`)
- [ ] 将审查结果返回给了启动你的角色
- [ ] **不关闭 Issue**(关闭决策权在用户)
- [ ] **不通过时**:评论中包含具体问题清单
@@ -21,10 +21,10 @@ description: >
按序阅读以下文档,获取完整的上下文信息:
1. **执行文档** (`docs/design/<stage>/<system-name>-execution.md`) — 行为规则细节(主要输入)
2. **系统设计文档** (`docs/design/<stage>/<system-name>.md`) — 全局上下文
1. **执行文档** (`docs/design/<stage>/<系统名>-执行案.md`) — 行为规则细节(主要输入)
2. **系统设计文档** (`docs/design/<stage>/<系统名>.md`) — 全局上下文
3. **配置表** (`GameConfig/`) — 可调参数
4. **概念文档** (`docs/design/concept.md`) — 方向参考(需要理解设计意图时查阅)
4. **概念文档** (`docs/design/概念草案.md`) — 方向参考(需要理解设计意图时查阅)
---
@@ -134,7 +134,7 @@ description: >
## 输出
最终产出为技术设计文档,保存到 `docs/technical/<system-name>-tech.md`。
最终产出为技术设计文档,保存到 `docs/technical/<系统名>-技术方案.md`。
文档必须遵循 `references/output-template.md` 中定义的标准模板结构。8 步分析的结果整合到模板的对应章节中,同时保留现有的必要章节(美术资源需求、数据流、可调参数汇总、文件结构)。
@@ -280,11 +280,11 @@ flowchart TD
### 3.3 项目实际案例参考
**需要时序图的特性**(从现有 `interaction-system-tech.md` 提取):
**需要时序图的特性**(从现有 `交互系统-技术方案.md` 提取):
- 完整交互流程(Enter → AnimLock → Active → Exit)
- 拾取流程(交互系统 → 物品系统 → 收集系统)
**不需要时序图的特性**(从现有 `player-controller-tech.md` 提取):
**不需要时序图的特性**(从现有 `玩家控制器-技术方案.md` 提取):
- SpringArm 参数配置(纯构造函数设置)
- CMC 移动参数调整(纯配置)
@@ -8,9 +8,9 @@
| 示例 | 来源文档 |
|------|---------|
| 交互系统 | `docs/technical/interaction-system-tech.md` |
| 角色控制 | `docs/technical/player-controller-tech.md` |
| 收集系统 | `docs/technical/collection-system-tech.md` |
| 交互系统 | `docs/technical/交互系统-技术方案.md` |
| 角色控制 | `docs/technical/玩家控制器-技术方案.md` |
| 收集系统 | `docs/technical/收集系统-技术方案.md` |
---
@@ -10,7 +10,7 @@
# <系统名称> 技术设计文档(<版本号> <阶段>)
> 任务ID: <任务编号>
> 对应执行案: `<执行案文件路径>`
> 对应执行案: `docs/design/<stage>/<系统名>-执行案.md`
> 关联系统: <上游/下游/平行系统列表>
> 阶段: 原型 / 批量生产 / 测试
> 日期: <YYYY-MM-DD>
+308
View File
@@ -0,0 +1,308 @@
---
name: "stage-iteration"
description: "迭代阶段技能 — 版本迭代开发管理。插件检查→设计补充→执行案→需求分析→开发→优化→整合→修复。"
---
# 迭代阶段
> 当 PM 检测到 Pipeline Tracker 标记为「迭代阶段」时调用本技能。
> 目标:按版本迭代推进开发,每个迭代完成一个可玩版本。
---
## 阶段概述
| 项目 | 说明 |
|------|------|
| 目标 | 将设计转化为可玩的迭代版本 |
| 产出 | 可运行的 UE5 版本 |
| 参与角色 | 策划(执行案)、程序(需求分析+开发) |
| 完成标志 | Bug 修复任务关闭 |
---
## 任务序列
每个迭代版本按以下顺序逐步创建任务。**PM 每次激活时只创建下一个可创建的任务**,不要一次全部创建。
### 任务列表(按创建顺序)
| 编号 | 任务名称 | 角色 | 前置依赖 | 首次迭代 |
|------|---------|------|---------|---------|
| 1.4.0 | 插件资产检查与准备 | 程序 | 无 | 仅首次 |
| 1.4.0.5 | 设计案迭代补充 | 策划 | 1.4.0(首次)或无 | 可选 |
| 1.4.1 | 策划执行案 | 策划 | 1.4.0.5 | 必选 |
| 1.4.2 | 程序需求分析 | 程序 | 1.4.1 | 必选 |
| 1.4.3.1 | 关卡搭建 | 程序 | 1.4.2 | 有场景时 |
| 1.4.3.2 | 系统开发 | 程序 | 1.4.2 | 必选 |
| 1.4.3.3 | 优化意见 | 程序 | 1.4.3.1 + 1.4.3.2 | 必选 |
| 1.4.3.5 | 资产接入整合 | 程序 | 1.4.3.3 | 必选 |
| 1.4.4 | Bug 修复 | 程序 | 1.4.3.5 | 必选 |
---
### 各任务详细说明
#### 1.4.0 插件资产检查与准备(仅首次迭代)
```bash
--title "P-XXX 插件资产检查与准备"
--body "## 任务信息
- **任务ID**: P-XXX
- **所属阶段**: 迭代阶段
- **角色**: 程序
## 预期产出
插件资产引用表 \`docs/technical/插件资产引用表.md\`
## 检查内容
1. BlockoutStarterPack — 白模资产完整性检查
- 路径: Content/BlockingStarterPack/
- 参考: .claude/refs/blockout-asset-reference.md
2. Lowpoly 角色/场景资源
- 路径: Content/Lowpoly*/ 或对应插件目录
3. 人物动作相关插件(动画蓝图、骨骼等)
4. 其他已安装插件
## 产出格式
对每个插件/资产包:
- 插件名称和版本
- 安装路径
- 关键资产列表(按类型分类:网格体、材质、动画、音效等)
- 可用性评估(完整/部分可用/缺失)
- 对开发的支持程度说明
## 目的
生成资产引用表,使开发过程可以脱离 3D 美术进行。"
--labels "stage:iteration,role:dev,status:ready"
--assignees "ai-programmer"
```
#### 1.4.0.5 设计案迭代补充(可选)
```bash
--title "D-XXX 设计案迭代补充 — <版本号>"
--body "## 任务信息
- **任务ID**: D-XXX
- **所属阶段**: 迭代阶段
- **角色**: 策划
## 预期产出
对现有设计文档的迭代补充,可能包括:
- 新增系统设计细节
- 调整已有系统参数
- 补充关卡设计细节
- 更新 UI/音频设计
## 说明
每个迭代版本可能对总路线阶段的设计有调整。
策划根据本版本的范围和目标,检查并补充设计文档。
如果设计文档无需调整,此任务可以跳过(直接关闭并说明'无需补充')。"
--labels "stage:iteration,role:design,status:ready"
--assignees "ai-designer"
```
#### 1.4.1 策划执行案
```bash
--title "D-XXX 策划执行案 — <版本号>"
--body "## 任务信息
- **任务ID**: D-XXX
- **所属阶段**: 迭代阶段
- **角色**: 策划
- **依赖**: D-XXX 设计案迭代补充完成
## 预期产出
根据设计案为本版本写策划执行案:
- 系统执行案(每份系统一份,如 D-XXXa 交互系统执行案)
- 关卡执行案(如涉及场景搭建)
## 执行案要求
- 只写玩家可感知的行为规则
- 不包含引擎组件、代码架构、数据格式
- 每条行为规则清晰无歧义
- 边界情况有明确处理
## 参考文档
- 设计案: docs/design/
- 执行案模板: .claude/refs/level-execution-template.md(关卡执行案)"
--labels "stage:iteration,role:design,status:backlog"
--assignees "ai-designer"
```
#### 1.4.2 程序需求分析
```bash
--title "P-XXX 程序需求分析 — <版本号>"
--body "## 任务信息
- **任务ID**: P-XXX
- **所属阶段**: 迭代阶段
- **角色**: 程序
- **依赖**: D-XXX 策划执行案完成
## 预期产出
技术设计文档(使用 /requirements-analysis 技能)
## 工作方式
基于策划执行案,使用 8 步需求分析技能产出技术设计文档:
1. 特性点枚举
2. 功能域图
3. 类设计图
4. 时序图(条件性)
5. 核心数据结构
6. 复杂接口描述(条件性)
7. 技术风险
8. 工作量估算
## 产出路径
docs/technical/<系统名>-tech.md"
--labels "stage:iteration,role:dev,status:backlog"
--assignees "ai-programmer"
```
#### 1.4.3.1 关卡搭建(有场景时)
```bash
--title "P-XXX 关卡搭建 — <版本号>"
--body "## 任务信息
- **任务ID**: P-XXX
- **所属阶段**: 迭代阶段
- **角色**: 程序
- **依赖**: P-XXX 程序需求分析完成
## 预期产出
白模关卡文件 + 搭建报告
## 搭建流程
参见 .claude/refs/level-execution-template.md 和 game-dev agent 的 Workflow B"
--labels "stage:iteration,role:dev,status:backlog"
--assignees "ai-programmer"
```
#### 1.4.3.2 系统开发
```bash
--title "P-XXX 系统开发 — <版本号>"
--body "## 任务信息
- **任务ID**: P-XXX
- **所属阶段**: 迭代阶段
- **角色**: 程序
- **依赖**: P-XXX 程序需求分析完成
## 预期产出
根据技术设计文档实现各系统代码
## 工作方式
参见 game-dev agent 的 Workflow C"
--labels "stage:iteration,role:dev,status:backlog"
--assignees "ai-programmer"
```
#### 1.4.3.3 优化意见
```bash
--title "P-XXX 优化意见 — <版本号>"
--body "## 任务信息
- **任务ID**: P-XXX
- **所属阶段**: 迭代阶段
- **角色**: 程序
- **依赖**: P-XXX 关卡搭建 + P-XXX 系统开发
## 预期产出
优化意见报告,包含:
- 性能分析(帧率、内存)
- 关卡布局优化建议
- 系统交互优化建议
- 体验改进建议"
--labels "stage:iteration,role:dev,status:backlog"
--assignees "ai-programmer"
```
#### 1.4.3.5 资产接入整合
```bash
--title "P-XXX 资产接入整合 — <版本号>"
--body "## 任务信息
- **任务ID**: P-XXX
- **所属阶段**: 迭代阶段
- **角色**: 程序
- **依赖**: P-XXX 优化意见完成
## 预期产出
整合所有资产到最终关卡中,确保:
- 白模资产正确替换(如有正式资产)
- 所有系统正常交互
- 关卡可完整游玩"
--labels "stage:iteration,role:dev,status:backlog"
--assignees "ai-programmer"
```
#### 1.4.4 Bug 修复
```bash
--title "P-XXX Bug修复 — <版本号>"
--body "## 任务信息
- **任务ID**: P-XXX
- **所属阶段**: 迭代阶段
- **角色**: 程序
- **依赖**: P-XXX 资产接入整合完成
## 工作方式
1. 完整游玩当前版本
2. 记录发现的 Bug
3. 按严重程度排序修复
4. 验证修复后无回归"
--labels "stage:iteration,role:dev,status:backlog"
--assignees "ai-programmer"
```
---
## 进度检查
每次 PM 激活时执行:
1. **读取当前迭代任务状态**:`issue list --milestone "迭代-vX.X" --state open`
2. **判断当前焦点**:
- 按 1.4.0 → 1.4.0.5 → 1.4.1 → 1.4.2 → 1.4.3.1/2 → 1.4.3.3 → 1.4.3.5 → 1.4.4 顺序
- 找到第一个未完成的步骤 → 建议:启动对应角色完成该步骤
- 如果某步骤的依赖未完成 → 建议:先完成依赖任务
3. **只创建下一个可创建的任务**:
- 已有任务进行中 → 不创建新任务
- 所有前置依赖已完成 → 创建下一个任务并设为 `status:ready`
4. **检查完成条件**:
- 1.4.4 (Bug修复) 已关闭 → 触发迭代转换
---
## 迭代转换
**前置条件**:Bug 修复任务(1.4.4)已关闭。
**询问用户**:是否有下一个迭代版本?
### 情况 A:有下一个迭代
```bash
# 1. 关闭当前迭代里程碑
node scripts/gitea.js --role pm milestone update <当前里程碑ID> --state closed
# 2. 创建下一个迭代里程碑
node scripts/gitea.js --role pm milestone create \
--title "迭代-vX.X" \
--description "第 N 个迭代版本:<简要描述>"
# 3. 从 1.4.0.5(设计补充)或 1.4.1(执行案)开始
# 非首次迭代跳过 1.4.0(插件检查)
# 4. 更新 Pipeline Tracker
```
### 情况 B:无下一个迭代,返回总路线
```bash
# 1. 关闭当前迭代里程碑
# 2. 重新打开或创建总路线阶段里程碑
# 3. 更新 Pipeline Tracker 为「总路线阶段」
# 4. 下次 PM 激活时将调用 stage-roadmap 技能
```
+183
View File
@@ -0,0 +1,183 @@
---
name: "stage-roadmap"
description: "总路线阶段技能 — 全面设计文档产出与版本路径规划。管理总路线阶段任务创建和阶段转换。"
---
# 总路线阶段
> 当 PM 检测到 Pipeline Tracker 标记为「总路线阶段」时调用本技能。
> 目标:产出完整的游戏设计文档体系,规划版本迭代路径。
---
## 阶段概述
| 项目 | 说明 |
|------|------|
| 目标 | 建立"这个游戏长什么样"的完整蓝图 |
| 产出 | 9 份设计文档 + 版本路径图 |
| 参与角色 | 策划(全部文档) |
| 完成标志 | 所有任务关闭 + 里程碑下无 open issues |
---
## 任务创建
### 检查已有任务
先通过 `issue list --milestone "总路线阶段" --state open` 检查现有任务,避免重复创建。
### 需要创建的任务(共 9 个)
**占位规则**:除系统设计文档(⑤)和关卡设计文档(⑥)必须完整内容外,其他文档允许暂时留空文件占位(包含标题、章节结构和「待填充」说明),后续迭代中逐步完善。
---
**任务 1:设计总索引文档** (D-XXX)
```bash
--title "D-XXX 设计总索引文档"
--body "## 任务信息
- **任务ID**: D-XXX
- **所属阶段**: 总路线阶段
- **角色**: 策划
- **优先级**: 最高(其他设计文档的索引基础)
## 预期产出
设计总索引 \`docs/design/设计总索引.md\`,包含:
- 所有设计文档的目录和链接
- 文档间的依赖关系图
- 当前完成状态追踪
## 占位规则
此文档必须完整产出(不允许占位)。"
--labels "stage:roadmap,role:design,status:ready"
--assignees "ai-designer"
```
**任务 2:世界观设计文档** (D-XXX) — 可占位
```
--title "D-XXX 世界观设计"
--body "...产出 docs/design/世界观设计.md
占位规则:允许占位(标题+章节结构+待填充说明)"
--labels "stage:roadmap,role:design,status:ready"
```
**任务 3:剧情设计文档** (D-XXX) — 可占位
```
--title "D-XXX 剧情设计"
--body "...产出 docs/design/剧情设计.md
占位规则:允许占位"
--labels "stage:roadmap,role:design,status:ready"
```
**任务 4:角色设计文档** (D-XXX) — 可占位
```
--title "D-XXX 角色设计"
--body "...产出 docs/design/角色设计.md
占位规则:允许占位"
--labels "stage:roadmap,role:design,status:ready"
```
**任务 5:系统设计文档** (D-XXX) — ⚠️ 必须完整
```
--title "D-XXX 系统设计"
--body "...产出 docs/design/系统设计.md(含各子系统的设计)
⚠️ 不允许占位,必须产出完整内容。"
--labels "stage:roadmap,role:design,status:ready"
```
**任务 6:关卡设计文档** (D-XXX) — ⚠️ 必须完整
```
--title "D-XXX 关卡设计"
--body "...产出 docs/design/关卡设计.md
⚠️ 不允许占位,必须产出完整内容。"
--labels "stage:roadmap,role:design,status:ready"
```
**任务 7:UI 设计文档** (D-XXX) — 可占位
```
--title "D-XXX UI设计"
--body "...产出 docs/design/UI设计.md
占位规则:允许占位"
--labels "stage:roadmap,role:design,status:ready"
```
**任务 8:音频设计文档** (D-XXX) — 可占位
```
--title "D-XXX 音频设计"
--body "...产出 docs/design/音频设计.md
占位规则:允许占位"
--labels "stage:roadmap,role:design,status:ready"
```
**任务 9:版本路径图** (D-XXX)
```
--title "D-XXX 版本路径图"
--body "...产出 docs/plans/版本路径图.md
包含:版本列表、每个版本的核心目标、范围概述、内容完成度、
涉及的系统清单、版本间递进逻辑、验收标准
依赖:任务 2-8(至少了解各文档的概要信息后才能规划版本路径)"
--labels "stage:roadmap,role:design,status:backlog"
```
### 依赖关系
```
任务 1(总索引)→ 先完成
任务 2-8(各设计文档)→ 1 完成后可并行创建,建议按序执行
任务 9(版本路径图)→ 依赖 2-8,最后完成
```
设置依赖:`issue block <任务9> <任务2>` ~ `issue block <任务9> <任务8>`
---
## 进度检查
每次 PM 激活时执行:
1. **读取任务状态**:`issue list --milestone "总路线阶段" --state open`
2. **判断当前焦点**:
- 任务 1 未完成 → 建议:先完成设计总索引
- 任务 2-8 有未完成的 → 建议按序完成各设计文档
- 任务 2-8 全完成,任务 9 未开始 → 更新任务 9 为 `status:ready`
- 任务 9 进行中 → 等待
3. **检查完成条件**:
- 总路线阶段里程碑下 open issues = 0 → 触发阶段转换
---
## 阶段转换
**前置条件**:总路线阶段里程碑下无 open issues。
**转换操作**(每步需用户确认):
```bash
# 1. 关闭总路线阶段里程碑
node scripts/gitea.js --role pm milestone update <总路线里程碑ID> --state closed
# 2. 创建第一个迭代版本里程碑
node scripts/gitea.js --role pm milestone create \
--title "迭代-v0.1" \
--description "第一个迭代版本"
# 3. 创建第一个迭代任务:插件资产检查与准备
# (详细参数见 stage-iteration 技能)
# 4. 更新 Pipeline Tracker
# 更新正文:当前阶段改为「迭代阶段」
# 阶段历史表添加总路线阶段完成日期
# 5. 告知用户已进入迭代阶段
# 下次 PM 激活时将调用 stage-iteration 技能
```
+120
View File
@@ -0,0 +1,120 @@
---
name: "stage-sketch"
description: "草图阶段技能 — 概念设计与技术可行性验证。管理草图阶段任务创建和阶段转换。"
---
# 草图阶段
> 当 PM 检测到 Pipeline Tracker 标记为「草图阶段」时调用本技能。
> 目标:验证游戏概念可行,产出概念草案和技术可行性报告。
---
## 阶段概述
| 项目 | 说明 |
|------|------|
| 目标 | 验证"这个游戏值得做、技术上可行" |
| 产出 | 概念草案 + 技术可行性报告 |
| 参与角色 | 策划(概念设计)、程序(技术评估) |
| 完成标志 | 两个任务都关闭 + 里程碑下无 open issues |
---
## 任务创建
### 检查已有任务
先通过 `issue list --milestone "草图阶段" --state open` 检查现有任务。
### 需要创建的任务(共 2 个)
**任务 1:概念设计** (D-XXX)
```bash
node scripts/gitea.js --role pm issue create \
--title "D-XXX 概念设计" \
--body "## 任务信息
- **任务ID**: D-XXX
- **所属阶段**: 草图阶段
- **角色**: 策划
## 预期产出
概念草案 \`docs/design/概念草案.md\`,包含 7 个必填字段:
1. 一句话概述
2. 核心玩法
3. 美术风格
4. 目标平台
5. 灵感来源
6. 核心假设(为什么好玩)
7. 范围约束(MVP 包含什么)
## 参考文档
- 游戏愿景:docs/plans/游戏愿景.md(如有)" \
--milestone "草图阶段" \
--labels "stage:sketch,role:design,status:ready" \
--assignees "ai-designer"
```
**任务 2:技术可行性报告** (P-XXX)
```bash
node scripts/gitea.js --role pm issue create \
--title "P-XXX 技术可行性评估" \
--body "## 任务信息
- **任务ID**: P-XXX
- **所属阶段**: 草图阶段
- **角色**: 程序
- **依赖**: D-XXX 概念设计完成
## 预期产出
技术可行性报告 \`docs/technical/可行性评估.md\`,评估:
1. 引擎能力匹配度(UE5 能否实现核心玩法)
2. 性能预期(目标平台帧率目标)
3. 关键技术风险(需要验证的技术点)
4. 插件/工具链可行性
5. 结论:可行/有条件可行/不可行" \
--milestone "草图阶段" \
--labels "stage:sketch,role:dev,status:backlog" \
--assignees "ai-programmer"
```
设置依赖关系:`issue block <D-XXX> <P-XXX>`
---
## 进度检查
每次 PM 激活时执行:
1. **读取任务状态**:`issue list --milestone "草图阶段" --state open`
2. **判断当前焦点**:
- D-XXX 未完成 → 建议用户启动策划做概念设计
- D-XXX 完成,P-XXX 未开始 → 更新 P-XXX 标签为 `status:ready`,建议启动程序做技术评估
- P-XXX 进行中 → 等待
3. **检查完成条件**:
- 草图阶段里程碑下 open issues = 0 → 触发阶段转换
---
## 阶段转换
**前置条件**:草图阶段里程碑下无 open issues。
**转换操作**(每步需用户确认):
```bash
# 1. 关闭草图阶段里程碑
node scripts/gitea.js --role pm milestone update <草图里程碑ID> --state closed
# 2. 创建总路线阶段里程碑
node scripts/gitea.js --role pm milestone create \
--title "总路线阶段" \
--description "全面设计文档产出:世界观、剧情、角色、系统、关卡、UI、音频、版本路径"
# 3. 更新 Pipeline Tracker
# 更新 Issue 正文:当前阶段改为「总路线阶段」,阶段历史表添加草图阶段完成日期
# 4. 告知用户已进入总路线阶段
# 下次 PM 激活时将调用 stage-roadmap 技能
```
+8 -1
View File
@@ -30,6 +30,13 @@
{
"Name": "GameplayStateTree",
"Enabled": true
},
{
"Name": "PythonScriptPlugin",
"Enabled": true,
"TargetAllowList": [
"Editor"
]
}
]
}
}
Binary file not shown.
+8 -1
View File
@@ -29,6 +29,13 @@ const STANDARD_LABELS = [
{ name: 'gate:art-fail', color: '#d73a49', description: '美术资产验收门 ❌' },
{ name: 'gate:impl-pass', color: '#28a745', description: '程序实现验收门 ✅' },
{ name: 'gate:impl-fail', color: '#d73a49', description: '程序实现验收门 ❌' },
// Review (2)
{ name: 'review:pass', color: '#28a745', description: '审查已通过 ✅' },
{ name: 'review:fail', color: '#d73a49', description: '审查未通过 ❌' },
// Stage (3)
{ name: 'stage:sketch', color: '#bfd4f2', description: '草图阶段' },
{ name: 'stage:roadmap', color: '#d4c5f9', description: '总路线阶段' },
{ name: 'stage:iteration', color: '#fce6c0', description: '迭代阶段' },
];
// ═══════════════════════════════════════════════════
@@ -702,7 +709,7 @@ function resolveToken(options) {
// 优先级 2:--role 参数
const role = options.role;
if (role) {
const validRoles = ['pm', 'designer', 'dev', 'artist'];
const validRoles = ['pm', 'designer', 'dev', 'artist', 'reviewer'];
if (!validRoles.includes(role)) {
console.error(`❌ 无效角色: ${role},可选: ${validRoles.join(', ')}`);
process.exit(1);
+183
View File
@@ -0,0 +1,183 @@
"""UE5 命令行白模关卡构建脚本。
由 build_level.bat 通过 UE5 的 -ExecutePythonScript 参数调用。
在 UE5 编辑器 Python 环境中运行,无需手动打开编辑器。
用法:
build_level.bat # 默认配置
build_level.bat --json output/v02.json # 指定 JSON
build_level.bat --no-quit # 构建后不退出编辑器
环境变量(由 build_level.bat 设置):
LB_JSON_PATH — JSON 数据文件路径(相对于项目根目录)
LB_MAP_NAME — 目标关卡资产名
LB_MAP_PACKAGE — 关卡包路径(如 /Game/Maps)
LB_NO_QUIT — 设为 "1" 不退出编辑器
"""
import json
import os
import sys
import time
from pathlib import Path
# ---- 从环境变量读取配置 ----
json_rel = os.environ.get("LB_JSON_PATH", "tools/level-builder/output/level_v01.json")
map_name = os.environ.get("LB_MAP_NAME", "LVL_SuburbanV01")
map_package = os.environ.get("LB_MAP_PACKAGE", "/Game/Maps")
no_quit = os.environ.get("LB_NO_QUIT", "0") == "1"
map_asset_path = f"{map_package}/{map_name}"
# ---- 检查 unreal 模块 ----
try:
import unreal
except ImportError:
print("[LevelBuilder] FATAL: unreal 模块不可用,必须在 UE5 环境中运行")
sys.exit(1)
# ---- 项目根目录 ----
# UE5 -ExecutePythonScript 模式下 CWD 是引擎目录,不是项目目录
# 所以必须通过环境变量传入项目路径
_project_root_str = os.environ.get("LB_PROJECT_ROOT", "")
if _project_root_str:
PROJECT_ROOT = Path(_project_root_str)
else:
# Fallback: 尝试从 .uproject 文件定位
_cwd = Path(os.getcwd())
if (_cwd / "ChildHood.uproject").exists():
PROJECT_ROOT = _cwd
else:
# 最后尝试 unreal API
try:
PROJECT_ROOT = Path(unreal.Paths.project_dir()).parent
except Exception:
PROJECT_ROOT = _cwd
json_path = PROJECT_ROOT / json_rel
print(f"[LevelBuilder] ==========================================")
print(f"[LevelBuilder] ChildHood Level Builder — Command Line")
print(f"[LevelBuilder] ==========================================")
print(f"[LevelBuilder] Project: {PROJECT_ROOT}")
print(f"[LevelBuilder] JSON: {json_rel}")
print(f"[LevelBuilder] Map: {map_asset_path}")
print(f"[LevelBuilder]")
# ---- 等待编辑器子系统就绪 ----
def wait_for_editor(timeout=90):
"""轮询等待 EditorActorSubsystem 可用。"""
print("[LevelBuilder] 等待编辑器初始化...")
start = time.time()
while time.time() - start < timeout:
try:
subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
if subsys is not None:
elapsed = time.time() - start
print(f"[LevelBuilder] 编辑器就绪 ({elapsed:.1f}s)")
return True
except Exception:
pass
time.sleep(1)
print(f"[LevelBuilder] FATAL: 编辑器初始化超时 ({timeout}s)")
return False
if not wait_for_editor():
sys.exit(1)
# ---- 设置 sys.path ----
lb_dir = str(PROJECT_ROOT / "tools" / "level-builder")
for p in [str(PROJECT_ROOT), lb_dir]:
if p not in sys.path:
sys.path.insert(0, p)
# ---- 确保目标关卡存在 ----
def ensure_map(asset_path):
"""创建或加载目标关卡。成功返回 True。"""
# 已存在 → 加载
if unreal.EditorAssetLibrary.does_asset_exist(asset_path):
print(f"[LevelBuilder] 加载已有关卡: {asset_path}")
unreal.EditorLevelLibrary.load_level(asset_path)
return True
print(f"[LevelBuilder] 创建新关卡: {asset_path}")
# 方法 1: new_level(asset_path) — UE5.7 EditorLevelLibrary 直接创建
try:
unreal.EditorLevelLibrary.new_level(asset_path)
print("[LevelBuilder] ✅ 关卡创建成功 (new_level)")
return True
except Exception as e:
print(f"[LevelBuilder] new_level 失败: {e}")
# 方法 2: AssetTools + WorldFactory(某些版本可用)
try:
asset_tools = unreal.get_editor_subsystem(unreal.AssetToolsSubsystem)
factory = unreal.WorldFactory()
new_asset = asset_tools.create_asset(
asset_name=map_name,
package_path=map_package,
asset_class=None,
factory=factory,
)
if new_asset:
unreal.EditorLevelLibrary.load_level(asset_path)
print("[LevelBuilder] ✅ 关卡创建成功 (WorldFactory)")
return True
except Exception as e:
print(f"[LevelBuilder] WorldFactory 失败: {e}")
print("[LevelBuilder] FATAL: 无法创建关卡")
return False
if not ensure_map(map_asset_path):
sys.exit(1)
# ---- 验证 JSON ----
if not json_path.exists():
print(f"[LevelBuilder] FATAL: JSON 文件不存在: {json_path}")
sys.exit(1)
# ---- 导入并执行构建 ----
from builder.scene_builder import build_level
print(f"[LevelBuilder] 开始构建...")
build_log = build_level(str(json_path), clear_existing=True)
# ---- 保存关卡 ----
print("[LevelBuilder] 保存关卡...")
try:
unreal.EditorLevelLibrary.save_current_level()
print(f"[LevelBuilder] ✅ 关卡已保存: {map_asset_path}")
except Exception as e:
print(f"[LevelBuilder] ⚠️ 保存失败: {e}")
# ---- 构建结果汇总 ----
if build_log:
ok = sum(1 for b in build_log if b.get("status") == "ok")
fail = len(build_log) - ok
print(f"")
print(f"[LevelBuilder] {'=' * 40}")
print(f"[LevelBuilder] SUCCESS: {ok} | FAILED: {fail} | TOTAL: {len(build_log)}")
print(f"[LevelBuilder] {'=' * 40}")
if fail > 0:
print(f"[LevelBuilder] 失败列表:")
for b in build_log:
if b.get("status") != "ok":
print(f" ❌ {b.get('name', '?')}: {b.get('reason', '未知')}")
# ---- 退出编辑器 ----
if not no_quit:
print("[LevelBuilder] 退出编辑器...")
unreal.SystemLibrary.execute_console_command(None, "quit")
print("[LevelBuilder] Done.")
+67
View File
@@ -0,0 +1,67 @@
"""UE5 编辑器内白模关卡构建启动脚本。
使用方式:
1. 打开 UE5 编辑器
2. Window → Developer Tools → Output Log
3. 左下角下拉菜单切换到 "Python"
4. 粘贴执行:
exec(open(r"<项目绝对路径>/tools/level-builder/build_in_editor.py").read())
或者:
File → Execute Python Script → 选择此文件
"""
import sys
from pathlib import Path
# ---- 配置 ----
PROJECT_ROOT = Path(r"E:/MiceCola/work/ChildHood")
LEVEL_JSON = PROJECT_ROOT / "tools/level-builder/output/level_v01.json"
# ---- 配置结束 ----
# 将项目路径加入 sys.path
TOOLS_DIR = PROJECT_ROOT / "tools"
LEVEL_BUILDER_DIR = PROJECT_ROOT / "tools/level-builder"
for p in [str(PROJECT_ROOT), str(TOOLS_DIR), str(LEVEL_BUILDER_DIR)]:
if p not in sys.path:
sys.path.insert(0, p)
# 检查 unreal 模块
try:
import unreal
except ImportError:
print("[错误] 未检测到 unreal 模块,此脚本必须在 UE5 编辑器 Python 环境中运行!")
print(" 请在 Output Log 中切换到 Python 控制台后执行。")
raise
# 检查 JSON 文件存在
if not LEVEL_JSON.exists():
print(f"[错误] JSON 文件不存在: {LEVEL_JSON}")
print(" 请先在命令行运行: python main.py parse -i ../../docs/technical/关卡-v01-搭建指南.md")
raise FileNotFoundError(str(LEVEL_JSON))
# 导入并执行构建
from builder.scene_builder import build_level
print(f"[LevelBuilder] 项目根目录: {PROJECT_ROOT}")
print(f"[LevelBuilder] JSON 数据: {LEVEL_JSON}")
print(f"[LevelBuilder] 开始构建...")
build_log = build_level(
json_path=str(LEVEL_JSON),
clear_existing=True,
zones=None, # 构建所有区域;如仅需部分区域改为 ["Z1", "Z3"]
)
if build_log:
ok = sum(1 for b in build_log if b.get("status") == "ok")
failed = sum(1 for b in build_log if b.get("status") != "ok")
print(f"\n[LevelBuilder] ========== 构建结果 ==========")
print(f" 成功: {ok}")
print(f" 失败: {failed}")
print(f" 总计: {len(build_log)}")
if failed > 0:
print(f"\n 失败列表:")
for b in build_log:
if b.get("status") != "ok":
print(f" ❌ {b.get('name', '?')}: {b.get('reason', '未知错误')}")
+192
View File
@@ -0,0 +1,192 @@
@echo off
setlocal enabledelayedexpansion
REM === ChildHood Level Builder - Command Line Build ===
REM
REM Usage:
REM build_level.bat (default config)
REM build_level.bat --json output/v02.json (specify JSON)
REM build_level.bat --map LVL_SuburbanV02 (specify map name)
REM build_level.bat --no-quit (keep editor open)
REM build_level.bat --no-null-rhi (show editor window)
REM build_level.bat --dry-run (print command only)
set "SCRIPT_DIR=%~dp0"
set "PROJECT_DIR=%SCRIPT_DIR%..\.."
for %%i in ("%PROJECT_DIR%\.") do set "PROJECT_ROOT=%%~fi"
set "UPROJECT=%PROJECT_ROOT%\ChildHood.uproject"
REM ---- Defaults ----
set "JSON_REL=tools/level-builder/output/level_v01.json"
set "MAP_NAME=LVL_SuburbanV01"
set "MAP_PACKAGE=/Game/Maps"
set "NO_QUIT=0"
set "NO_NULL_RHI=0"
set "DRY_RUN=0"
REM ---- Parse Arguments ----
:parse_args
if "%~1"=="" goto done_args
if /i "%~1"=="--json" (
set "JSON_REL=%~2"
shift
shift
goto parse_args
)
if /i "%~1"=="--map" (
set "MAP_NAME=%~2"
shift
shift
goto parse_args
)
if /i "%~1"=="--package" (
set "MAP_PACKAGE=%~2"
shift
shift
goto parse_args
)
if /i "%~1"=="--no-quit" (
set "NO_QUIT=1"
shift
goto parse_args
)
if /i "%~1"=="--no-null-rhi" (
set "NO_NULL_RHI=1"
shift
goto parse_args
)
if /i "%~1"=="--dry-run" (
set "DRY_RUN=1"
shift
goto parse_args
)
if /i "%~1"=="--help" goto show_help
if /i "%~1"=="-h" goto show_help
echo [WARN] Unknown argument: %~1
shift
goto parse_args
:done_args
REM ---- Find UE5 Editor ----
set "UE_EDITOR="
if defined UE5_ROOT (
for %%b in (UnrealEditor.exe UE5Editor.exe) do (
if exist "%UE5_ROOT%\Engine\Binaries\Win64\%%b" (
set "UE_EDITOR=%UE5_ROOT%\Engine\Binaries\Win64\%%b"
)
)
)
if "%UE_EDITOR%"=="" (
for %%p in (
"C:\Program Files\Epic Games\UE_5.7"
"C:\Program Files (x86)\Epic Games\UE_5.7"
"D:\Program Files\Epic Games\UE_5.7"
"E:\Program Files\Epic Games\UE_5.7"
"C:\Program Files\Epic Games\UE_5.6"
"C:\Program Files (x86)\Epic Games\UE_5.6"
"D:\Program Files\Epic Games\UE_5.6"
"E:\Program Files\Epic Games\UE_5.6"
) do (
if not defined UE_EDITOR (
for %%b in (UnrealEditor.exe UE5Editor.exe) do (
if exist "%%~p\Engine\Binaries\Win64\%%b" (
set "UE_EDITOR=%%~p\Engine\Binaries\Win64\%%b"
)
)
)
)
)
if "%UE_EDITOR%"=="" (
echo.
echo [ERROR] UE5 Editor not found!
echo Set UE5_ROOT to your UE5 install directory, e.g.:
echo set UE5_ROOT=C:\Program Files\Epic Games\UE_5.7
echo.
exit /b 1
)
REM ---- Path conversion (UE5 uses forward slashes) ----
set "SCRIPT_PATH=%SCRIPT_DIR%build_cmdline.py"
set "SCRIPT_PATH=!SCRIPT_PATH:\=/!"
set "UPROJECT_FWD=!UPROJECT:\=/!"
REM ---- Build command line ----
set "CMD_ARGS=-unattended -NoSound"
if "!NO_NULL_RHI!"=="0" set "CMD_ARGS=!CMD_ARGS! -NullRHI"
set "CMD_ARGS=!CMD_ARGS! -ExecutePythonScript=!SCRIPT_PATH!"
REM ---- Pass config to Python via env vars ----
set "LB_PROJECT_ROOT=!PROJECT_ROOT!"
set "LB_JSON_PATH=%JSON_REL%"
set "LB_MAP_NAME=%MAP_NAME%"
set "LB_MAP_PACKAGE=%MAP_PACKAGE%"
if "!NO_QUIT!"=="1" set "LB_NO_QUIT=1"
REM ---- Print info ----
echo.
echo ============================================
echo ChildHood Level Builder - Command Line
echo ============================================
echo UE5: !UE_EDITOR!
echo Project: !UPROJECT_FWD!
echo JSON: %JSON_REL%
echo Map: %MAP_PACKAGE%/%MAP_NAME%
echo NullRHI: %NO_NULL_RHI%
echo ============================================
echo.
REM ---- Dry Run ----
if "%DRY_RUN%"=="1" (
echo [DRY RUN] Command:
echo.
echo "!UE_EDITOR!" "!UPROJECT_FWD!" !CMD_ARGS!
echo.
exit /b 0
)
REM ---- Check JSON exists ----
set "JSON_ABS=%PROJECT_ROOT%\%JSON_REL%"
set "JSON_ABS=!JSON_ABS:/=\!"
if not exist "!JSON_ABS!" (
echo [ERROR] JSON not found: !JSON_ABS!
echo Run first: python tools/level-builder/main.py parse
exit /b 1
)
REM ---- Execute ----
echo [LevelBuilder] Launching UE5 Editor (command line mode)...
echo [LevelBuilder] Editor will auto-save and quit after build.
echo.
"!UE_EDITOR!" "!UPROJECT_FWD!" !CMD_ARGS!
set "EXIT_CODE=!ERRORLEVEL!"
echo.
if "!EXIT_CODE!"=="0" (
echo [LevelBuilder] Build complete. Editor exited.
) else (
echo [LevelBuilder] Editor exit code: !EXIT_CODE!
)
exit /b !EXIT_CODE!
:show_help
echo.
echo ChildHood Level Builder - Command Line Build
echo.
echo Usage: build_level.bat [options]
echo.
echo Options:
echo --json PATH JSON data file (relative to project root)
echo --map NAME Target map asset name (default: LVL_SuburbanV01)
echo --package PATH Map package path (default: /Game/Maps)
echo --no-quit Keep editor open after build
echo --no-null-rhi Show editor window (for debugging)
echo --dry-run Print command without executing
echo --help, -h Show this help
echo.
echo Environment:
echo UE5_ROOT UE5 install directory
echo.
exit /b 0
@@ -0,0 +1,257 @@
"""UE5 场景构建器。
在 UE5 编辑器的 Python 环境中运行。读取 JSON 中间数据,
使用 unreal Python API 创建 Actor、设置 Transform/材质/Outliner 文件夹。
使用方式:
在 UE5 编辑器 Output Log 中切换到 Python,执行:
exec(open(r"path/to/scene_builder.py").read())
build_level(r"path/to/level_v01.json")
"""
import json
import logging
import sys
from pathlib import Path
# 尝试导入 unreal,失败时给出提示
try:
import unreal
HAS_UNREAL = True
except ImportError:
HAS_UNREAL = False
# 将 level-builder 根目录加入 sys.path 以支持 config 导入
# 注意: 目录名含连字符 (level-builder),不能用 tools.level_builder 形式导入
_SCRIPT_DIR = Path(__file__).resolve().parent # builder/
_LB_DIR = _SCRIPT_DIR.parent # tools/level-builder/
if str(_LB_DIR) not in sys.path:
sys.path.insert(0, str(_LB_DIR))
from config.asset_mapping import MESH_ASSETS
from config.material_mapping import MATERIAL_PATHS
logger = logging.getLogger("level_builder.builder")
def build_level(json_path: str, clear_existing: bool = True, zones: list[str] = None):
"""从 JSON 数据构建 UE5 关卡。
Args:
json_path: 中间 JSON 文件路径。
clear_existing: 是否清除已有的 LVL_* Actor。
zones: 仅构建指定区域(如 ["Z1", "Z3"]),None 表示全部。
"""
if not HAS_UNREAL:
print("[错误] 未检测到 unreal 模块。此脚本必须在 UE5 编辑器 Python 环境中运行。")
return
# 加载数据
data = _load_json(json_path)
if not data:
return
actors = data.get("actors", [])
print(f"[LevelBuilder] 加载 {len(actors)} 个 Actor 定义")
# 可选:清除已有 LVL_* Actor
if clear_existing:
_clear_existing_actors()
# 过滤区域
if zones:
actors = [a for a in actors if a.get("zone_id") in zones or a.get("zone_id") == "GLOBAL"]
print(f"[LevelBuilder] 过滤为 {len(actors)} 个 Actor(区域: {zones})")
# 获取编辑器子系统
editor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
if not editor_subsystem:
print("[错误] 无法获取 EditorActorSubsystem")
return
# 统计
success = 0
failed = 0
build_log = []
for i, actor_def in enumerate(actors):
name = actor_def.get("actor_name", f"Actor_{i}")
asset_type = actor_def.get("asset_type", "static_mesh")
try:
if asset_type == "player_start":
actor = _spawn_player_start(editor_subsystem, actor_def)
elif asset_type == "directional_light":
actor = _spawn_directional_light(editor_subsystem, actor_def)
elif asset_type == "sky_light":
actor = _spawn_sky_light(editor_subsystem, actor_def)
elif asset_type == "point_light":
actor = _spawn_point_light(editor_subsystem, actor_def)
else:
actor = _spawn_mesh_actor(editor_subsystem, actor_def)
if actor:
success += 1
build_log.append({"name": name, "status": "ok", **actor_def})
else:
failed += 1
build_log.append({"name": name, "status": "failed", "reason": "spawn returned None"})
except Exception as e:
failed += 1
error_msg = str(e)
build_log.append({"name": name, "status": "error", "reason": error_msg})
print(f" [错误] {name}: {error_msg}")
# 进度
if (i + 1) % 20 == 0:
print(f" 进度: {i + 1}/{len(actors)} ({success} 成功, {failed} 失败)")
print(f"\n[LevelBuilder] 构建完成!")
print(f" 成功: {success}")
print(f" 失败: {failed}")
print(f" 总计: {len(actors)}")
# 保存构建日志
log_path = Path(json_path).parent / "build_log.json"
with open(log_path, "w", encoding="utf-8") as f:
json.dump(build_log, f, ensure_ascii=False, indent=2)
print(f" 日志: {log_path}")
return build_log
def _load_json(path: str) -> dict:
"""加载 JSON 文件。"""
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
print(f"[错误] 无法加载 JSON: {e}")
return None
def _clear_existing_actors():
"""清除场景中所有 LVL_ 前缀的 Actor。"""
editor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
all_actors = unreal.EditorLevelLibrary.get_all_level_actors()
lvl_actors = [a for a in all_actors if a.get_actor_label().startswith("LVL_")]
if lvl_actors:
print(f"[LevelBuilder] 清除 {len(lvl_actors)} 个已有 LVL_* Actor...")
for a in lvl_actors:
unreal.EditorLevelLibrary.destroy_actor(a)
else:
print("[LevelBuilder] 无已有 LVL_* Actor 需清除")
def _spawn_mesh_actor(editor_subsystem, actor_def: dict):
"""生成 StaticMesh Actor。"""
asset_key = actor_def.get("asset_key", "")
if not asset_key or asset_key not in MESH_ASSETS:
print(f" [警告] {actor_def['actor_name']}: 资产 '{asset_key}' 未找到,使用默认 Cube")
asset_key = "Cube"
mesh_path = MESH_ASSETS[asset_key]["path"]
static_mesh = unreal.load_asset(mesh_path)
if not static_mesh:
print(f" [警告] {actor_def['actor_name']}: 无法加载资产 '{mesh_path}'")
return None
# 生成
loc = actor_def.get("location", [0, 0, 0])
actor = editor_subsystem.spawn_actor_from_object(
static_mesh,
unreal.Vector(loc[0], loc[1], loc[2])
)
if not actor:
return None
# 设置 Transform
rot = actor_def.get("rotation", [0, 0, 0])
actor.set_actor_rotation(unreal.Rotator(rot[0], rot[1], rot[2]), False)
scale = actor_def.get("scale", [1, 1, 1])
actor.set_actor_scale3d(unreal.Vector(scale[0], scale[1], scale[2]))
# 设置材质
material_name = actor_def.get("material", "")
if material_name and material_name in MATERIAL_PATHS:
mat_path = MATERIAL_PATHS[material_name]
material = unreal.load_asset(mat_path)
if material:
mesh_comp = actor.get_component_by_class(unreal.StaticMeshComponent)
if mesh_comp:
mesh_comp.set_material(0, material)
# 设置名称和文件夹
actor.set_actor_label(actor_def["actor_name"])
folder = actor_def.get("folder", "")
if folder:
actor.set_folder_path(folder)
return actor
def _spawn_player_start(editor_subsystem, actor_def: dict):
"""生成 PlayerStart Actor。"""
loc = actor_def.get("location", [0, 0, 0])
actor = editor_subsystem.spawn_actor_from_class(
unreal.PlayerStart,
unreal.Vector(loc[0], loc[1], loc[2])
)
if actor:
rot = actor_def.get("rotation", [0, 0, 0])
actor.set_actor_rotation(unreal.Rotator(rot[0], rot[1], rot[2]), False)
actor.set_actor_label(actor_def["actor_name"])
folder = actor_def.get("folder", "")
if folder:
actor.set_folder_path(folder)
return actor
def _spawn_directional_light(editor_subsystem, actor_def: dict):
"""生成 DirectionalLight Actor。"""
loc = actor_def.get("location", [0, 0, 500])
actor = editor_subsystem.spawn_actor_from_class(
unreal.DirectionalLight,
unreal.Vector(loc[0], loc[1], loc[2])
)
if actor:
rot = actor_def.get("rotation", [-45, 315, 0])
actor.set_actor_rotation(unreal.Rotator(rot[0], rot[1], rot[2]), False)
actor.set_actor_label(actor_def["actor_name"])
folder = actor_def.get("folder", "")
if folder:
actor.set_folder_path(folder)
return actor
def _spawn_sky_light(editor_subsystem, actor_def: dict):
"""生成 SkyLight Actor。"""
loc = actor_def.get("location", [0, 0, 0])
actor = editor_subsystem.spawn_actor_from_class(
unreal.SkyLight,
unreal.Vector(loc[0], loc[1], loc[2])
)
if actor:
actor.set_actor_label(actor_def["actor_name"])
folder = actor_def.get("folder", "")
if folder:
actor.set_folder_path(folder)
return actor
def _spawn_point_light(editor_subsystem, actor_def: dict):
"""生成 PointLight Actor。"""
loc = actor_def.get("location", [0, 0, 0])
actor = editor_subsystem.spawn_actor_from_class(
unreal.PointLight,
unreal.Vector(loc[0], loc[1], loc[2])
)
if actor:
actor.set_actor_label(actor_def["actor_name"])
folder = actor_def.get("folder", "")
if folder:
actor.set_folder_path(folder)
return actor
+211
View File
@@ -0,0 +1,211 @@
"""BSP 资产设计名 → UE5 内容路径映射。
数据来源: docs/technical/关卡-v01-搭建指南.md 第一章 (BSP 资产映射表)
资产根路径: Content/BlockingStarterPack/Meshes/
路径格式: /Game/<ContentSubPath>/<AssetName>.<AssetName>
"""
# 资产路径前缀
MESH_ROOT = "/Game/BlockingStarterPack/Meshes"
MATERIAL_ROOT = "/Game/BlockingStarterPack/Materials"
MESH_ASSETS = {
# ---- 建筑 — 墙体 ----
"Wall_400x300": {
"path": f"{MESH_ROOT}/Architecture/Walls/SM_Wall_400x300.SM_Wall_400x300",
"default_size": (400, 20, 300),
"category": "wall",
},
"Wall_200x300": {
"path": f"{MESH_ROOT}/Architecture/Walls/SM_Wall_200x300.SM_Wall_200x300",
"default_size": (200, 20, 300),
"category": "wall",
},
"Wall_100x300": {
"path": f"{MESH_ROOT}/Architecture/Walls/SM_Wall_100x300.SM_Wall_100x300",
"default_size": (100, 20, 300),
"category": "wall",
},
"Wall_400x100": {
"path": f"{MESH_ROOT}/Architecture/Walls/SM_Wall_400x100.SM_Wall_400x100",
"default_size": (400, 20, 100),
"category": "wall",
},
"WallDoor_200x300": {
"path": f"{MESH_ROOT}/Architecture/Walls/SM_WallDoor_200x300.SM_WallDoor_200x300",
"default_size": (200, 20, 300),
"category": "wall_door",
},
"WallDoorGarage_400x300": {
"path": f"{MESH_ROOT}/Architecture/Walls/SM_WallDoorGarage_400x300.SM_WallDoorGarage_400x300",
"default_size": (400, 20, 300),
"category": "wall_door",
},
"WallWindow_400x300": {
"path": f"{MESH_ROOT}/Architecture/Walls/SM_WallWindow_400x300.SM_WallWindow_400x300",
"default_size": (400, 20, 300),
"category": "wall_window",
},
"WallWindowSmall_200x300": {
"path": f"{MESH_ROOT}/Architecture/Walls/SM_WallWindowSmall_200x300.SM_WallWindowSmall_200x300",
"default_size": (200, 20, 300),
"category": "wall_window",
},
# ---- 建筑 — 地板 ----
"Floor_1000x1000": {
"path": f"{MESH_ROOT}/Architecture/Floors/SM_Floor_1000x1000.SM_Floor_1000x1000",
"default_size": (1000, 1000, 10),
"category": "floor",
},
"Floor_300x400": {
"path": f"{MESH_ROOT}/Architecture/Floors/SM_Floor_300x400.SM_Floor_300x400",
"default_size": (300, 400, 10),
"category": "floor",
},
"Floor_200x300": {
"path": f"{MESH_ROOT}/Architecture/Floors/SM_Floor_200x300.SM_Floor_200x300",
"default_size": (200, 300, 10),
"category": "floor",
},
# ---- 建筑 — 门 ----
"RotativeDoor_01": {
"path": f"{MESH_ROOT}/Architecture/Doors/SM_RotativeDoor_01.SM_RotativeDoor_01",
"default_size": None,
"category": "door",
},
"GarageDoorOpen_01": {
"path": f"{MESH_ROOT}/Architecture/Doors/SM_GarageDoorOpen_01.SM_GarageDoorOpen_01",
"default_size": None,
"category": "door",
},
# ---- 建筑 — 窗户 ----
"FakeWindowsFrame": {
"path": f"{MESH_ROOT}/Architecture/WindowsSet/SM_FakeWindowsFrame.SM_FakeWindowsFrame",
"default_size": None,
"category": "window",
},
# ---- 建筑 — 围栏/围挡 ----
"BarrierWall_600": {
"path": f"{MESH_ROOT}/Architecture/Barriers/SM_BarrierWall_600.SM_BarrierWall_600",
"default_size": (600, 20, 100),
"category": "barrier",
},
"BarrierWall_200": {
"path": f"{MESH_ROOT}/Architecture/Barriers/SM_BarrierWall_200.SM_BarrierWall_200",
"default_size": (200, 20, 100),
"category": "barrier",
},
"Fence_Small_400": {
"path": f"{MESH_ROOT}/Industrials/SM_Fence_Small_400.SM_Fence_Small_400",
"default_size": (400, 5, 100),
"category": "fence",
},
# ---- 建筑 — 横梁 ----
"Beam_300": {
"path": f"{MESH_ROOT}/Architecture/Beams/SM_Beam_300.SM_Beam_300",
"default_size": None,
"category": "beam",
},
# ---- 建筑 — 大型建筑 ----
"Building_01": {
"path": f"{MESH_ROOT}/BigBuildings/SM_Building_01.SM_Building_01",
"default_size": None,
"category": "building",
},
# ---- 家具 ----
"SimpleTable_01": {
"path": f"{MESH_ROOT}/Furnitures/SM_SimpleTable_01.SM_SimpleTable_01",
"default_size": None,
"category": "furniture",
},
"SimpleTable_02": {
"path": f"{MESH_ROOT}/Furnitures/SM_SimpleTable_02.SM_SimpleTable_02",
"default_size": None,
"category": "furniture",
},
"BedSingle": {
"path": f"{MESH_ROOT}/Bedroom/SM_BedSingle.SM_BedSingle",
"default_size": None,
"category": "furniture",
},
"Monitor_01": {
"path": f"{MESH_ROOT}/Furnitures/SM_Monitor_01.SM_Monitor_01",
"default_size": None,
"category": "furniture",
},
# ---- 工业/装饰 ----
"Barrel": {
"path": f"{MESH_ROOT}/Industrials/SM_Barrel.SM_Barrel",
"default_size": None,
"category": "industrial",
},
"WoodenBox_Small": {
"path": f"{MESH_ROOT}/Industrials/SM_WoodenBox_Small.SM_WoodenBox_Small",
"default_size": None,
"category": "industrial",
},
"WoodenBox_Normal": {
"path": f"{MESH_ROOT}/Industrials/SM_WoodenBox_Normal.SM_WoodenBox_Normal",
"default_size": None,
"category": "industrial",
},
"WoodenPallet_01": {
"path": f"{MESH_ROOT}/Industrials/SM_WoodenPallet_01.SM_WoodenPallet_01",
"default_size": None,
"category": "industrial",
},
"Bucket_01": {
"path": f"{MESH_ROOT}/Decorations/SM_Bucket_01.SM_Bucket_01",
"default_size": None,
"category": "industrial",
},
"TrashCans_01": {
"path": f"{MESH_ROOT}/Trashs/SM_TrashCans_01.SM_TrashCans_01",
"default_size": None,
"category": "industrial",
},
"Container_Normal": {
"path": f"{MESH_ROOT}/Industrials/SM_Container_Normal.SM_Container_Normal",
"default_size": None,
"category": "industrial",
},
# ---- 载具 ----
"Car01": {
"path": f"{MESH_ROOT}/Vehicles/SM_Car01.SM_Car01",
"default_size": None,
"category": "vehicle",
},
# ---- 基础体 ----
"Cube": {
"path": f"{MESH_ROOT}/Primitives/SM_Cube.SM_Cube",
"default_size": (100, 100, 100),
"category": "primitive",
},
"Cylinder": {
"path": f"{MESH_ROOT}/Primitives/SM_Cylinder.SM_Cylinder",
"default_size": (100, 100, 100),
"category": "primitive",
},
"CirclePlatform": {
"path": f"{MESH_ROOT}/Extra/SM_CirclePlatform.SM_CirclePlatform",
"default_size": None,
"category": "primitive",
},
# ---- 其他 ----
"Tree_01": {
"path": f"{MESH_ROOT}/Vegetations/SM_Tree_01.SM_Tree_01",
"default_size": None,
"category": "vegetation",
},
"RoadLamp_01": {
"path": f"{MESH_ROOT}/Lights/SM_RoadLamp_01.SM_RoadLamp_01",
"default_size": None,
"category": "light_fixture",
},
"DisplayPanels_400x200": {
"path": f"{MESH_ROOT}/DisplayPanels/SM_DisplayPanels_400x200.SM_DisplayPanels_400x200",
"default_size": None,
"category": "decoration",
},
}
@@ -0,0 +1,25 @@
"""标识色材质路径映射。
数据来源: docs/technical/关卡-v01-搭建指南.md §1.13
"""
MATERIAL_PATHS = {
# ---- Basics(标识色) ----
"MI_DevRed": "/Game/BlockingStarterPack/Materials/Blocking/Basics/MI_DevRed.MI_DevRed",
"MI_DevOrange": "/Game/BlockingStarterPack/Materials/Blocking/Basics/MI_DevOrange.MI_DevOrange",
"MI_DevGray": "/Game/BlockingStarterPack/Materials/Blocking/Basics/MI_DevGray.MI_DevGray",
"MI_DevBrown": "/Game/BlockingStarterPack/Materials/Blocking/Basics/MI_DevBrown.MI_DevBrown",
"MI_DevWhite": "/Game/BlockingStarterPack/Materials/Blocking/Basics/MI_DevWhite.MI_DevWhite",
"MI_DevBlue": "/Game/BlockingStarterPack/Materials/Blocking/Basics/MI_DevBlue.MI_DevBlue",
"MI_DevGreen": "/Game/BlockingStarterPack/Materials/Blocking/Basics/MI_DevGreen.MI_DevGreen",
"MI_DevSkyBlue": "/Game/BlockingStarterPack/Materials/Blocking/Basics/MI_DevSkyBlue.MI_DevSkyBlue",
"MI_DevYellow": "/Game/BlockingStarterPack/Materials/Blocking/Basics/MI_DevYellow.MI_DevYellow",
"MI_DevFoliage": "/Game/BlockingStarterPack/Materials/Blocking/Basics/MI_DevFoliage.MI_DevFoliage",
"MI_DevPink": "/Game/BlockingStarterPack/Materials/Blocking/Basics/MI_DevPink.MI_DevPink",
# ---- Glass ----
"MI_DevGlassWhite": "/Game/BlockingStarterPack/Materials/Glass/MI_DevGlassWhite.MI_DevGlassWhite",
"MI_DevGlassBlue": "/Game/BlockingStarterPack/Materials/Glass/MI_DevGlassBlue.MI_DevGlassBlue",
}
# 材质在 Content/ 目录下的文件系统路径前缀(用于离线文件存在性验证)
MATERIAL_FS_PREFIX = "Content/BlockingStarterPack/Materials"
@@ -0,0 +1,36 @@
"""Primitives 替代规则。
数据来源: docs/technical/关卡-v01-搭建报告.md 第四节(缩放记录)
当搭建指南中直接指定了资产和缩放时,无需查此表。
此表用于未来执行案解析器,将语义化物件名映射到 Primitives 替代方案。
"""
SUBSTITUTION_RULES = {
# ---- 家具/家电 ----
"冰箱": {"asset": "Cube", "scale": (0.3, 0.3, 1.6), "material": "MI_DevWhite"},
"冰柜": {"asset": "Cube", "scale": (1.0, 0.6, 0.85), "material": "MI_DevWhite"},
"电视柜": {"asset": "SimpleTable_02", "scale": (0.85, 0.75, 0.67)},
# ---- 建筑细节 ----
"门垫": {"asset": "Cube", "scale": (0.6, 0.4, 0.02), "material": "MI_DevGray"},
"日历": {"asset": "Cube", "scale": (0.3, 0.02, 0.4), "material": "MI_DevRed"},
"招牌": {"asset": "Cube", "scale": (3.0, 0.05, 0.4), "material": "MI_DevBlue"},
"涂鸦": {"asset": "Cube", "scale": (1.2, 0.05, 0.8), "material": "MI_DevYellow"},
# ---- 玻璃 ----
"玻璃柜台": {"asset": "Cube", "scale": (2.0, 0.6, 0.9), "material": "MI_DevGlassWhite"},
"积水坑": {"asset": "Cube", "scale": (1.2, 0.8, 0.01), "material": "MI_DevGlassBlue"},
# ---- 街道/城郊物件 ----
"电话亭": {"asset": "Cube", "scale": (0.8, 0.8, 2.2), "material": "MI_DevGray"},
"电线杆": {"asset": "Cylinder", "scale": (0.04, 0.04, 12.0), "material": "MI_DevGray"},
"晾衣杆_柱": {"asset": "Cylinder", "scale": (0.05, 0.05, 3.6), "material": "MI_DevGray"},
"芦苇": {"asset": "Cylinder", "scale": (0.08, 0.08, 2.0), "material": "MI_DevFoliage"},
"花盆": {"asset": "Cylinder", "scale": (0.2, 0.2, 0.25), "material": "MI_DevGreen"},
# ---- 水塘 ----
"荷叶": {"asset": "CirclePlatform", "scale": (0.06, 0.06, 0.01), "material": "MI_DevGreen"},
"青蛙": {"asset": "Cube", "scale": (0.05, 0.05, 0.04), "material": "MI_DevGreen"},
"下水道盖": {"asset": "CirclePlatform", "scale": (0.12, 0.12, 0.02), "material": "MI_DevGray"},
"弹珠圈": {"asset": "CirclePlatform", "scale": (0.16, 0.16, 0.01), "material": "MI_DevGray"},
# ---- 自然 ----
"青苔": {"asset": "Cube", "scale": (0.6, 0.4, 0.01), "material": "MI_DevFoliage"},
"蜘蛛网": {"asset": "Cube", "scale": (0.3, 0.01, 0.3), "material": "MI_DevWhite"},
}
+142
View File
@@ -0,0 +1,142 @@
"""区域定义、Outliner 文件夹结构、Actor 命名规范。
数据来源:
- docs/design/prototype/关卡-v01-城郊小街-执行案.md §二
- docs/technical/关卡-v01-搭建指南.md §〇
"""
# ---- 场景全局参数 ----
SCENE_PARAMS = {
"scene_size": (8000, 6000), # cm
"ground_z": 0,
"character_height": 120, # cm
"camera_height": 100, # cm
"time_of_day": "15:00",
"origin_corner": "西南角地平面",
}
# ---- 区域定义 ----
ZONES = {
"Z1": {
"name": "家",
"name_en": "Home",
"origin": (0, 0, 0),
"bounds": (1000, 800),
"color_material": "MI_DevRed",
},
"Z2": {
"name": "小卖部",
"name_en": "CornerShop",
"origin": (1000, 200, 0),
"bounds": (600, 600),
"color_material": "MI_DevOrange",
},
"Z3": {
"name": "主街道",
"name_en": "MainStreet",
"origin": (0, 800, 0),
"bounds": (5000, 500),
"color_material": "MI_DevGray",
},
"Z4": {
"name": "空地",
"name_en": "VacantLot",
"origin": (3500, 1300, 0),
"bounds": (1500, 1200),
"color_material": "MI_DevBrown",
},
"Z5": {
"name": "巷子",
"name_en": "Alley",
"origin": (2300, 1300, 0),
"bounds": (300, 2000),
"color_material": "MI_DevWhite",
},
"Z6": {
"name": "水塘边",
"name_en": "Pond",
"origin": (1600, 3300, 0),
"bounds": (1500, 1400),
"color_material": "MI_DevSkyBlue",
},
"Z7": {
"name": "老居民楼",
"name_en": "OldBuilding",
"origin": (5800, 200, 0),
"bounds": (1200, 1800),
"color_material": "MI_DevGray",
},
}
# ---- Outliner 文件夹结构 ----
LEVEL_NAME = "LVL_SuburbanV01"
ZONE_FOLDERS = {
"Z1": f"{LEVEL_NAME}/Z1_家",
"Z2": f"{LEVEL_NAME}/Z2_小卖部",
"Z3": f"{LEVEL_NAME}/Z3_主街道",
"Z4": f"{LEVEL_NAME}/Z4_空地",
"Z5": f"{LEVEL_NAME}/Z5_巷子",
"Z6": f"{LEVEL_NAME}/Z6_水塘边",
"Z7": f"{LEVEL_NAME}/Z7_老居民楼",
"GLOBAL": f"{LEVEL_NAME}/Global",
}
CATEGORY_FOLDERS = {
"STR": "Structure",
"PROP": "Props",
"INTR": "Interactables",
"COLL": "Collectibles",
"BND": "Boundaries",
"LGT": "Lighting",
}
GLOBAL_SUBFOLDERS = {
"BND": f"{LEVEL_NAME}/Global/Boundaries",
"LGT": f"{LEVEL_NAME}/Global/Lighting",
"PLAY": f"{LEVEL_NAME}/Global/PlayerStart",
}
def get_folder_path(zone_id: str, category: str) -> str:
"""根据区域 ID 和类别获取 Outliner 文件夹路径。"""
if zone_id == "GLOBAL":
return GLOBAL_SUBFOLDERS.get(category, f"{LEVEL_NAME}/Global")
base = ZONE_FOLDERS.get(zone_id, f"{LEVEL_NAME}/Global")
sub = CATEGORY_FOLDERS.get(category, "")
if sub:
return f"{base}/{sub}"
return base
# ---- Actor 命名模板 ----
# LVL_<ZoneID>_<Category>_<Description>
# 已在搭建指南中给出完整名称,脚本直接使用。
# ---- 搭建顺序 ----
BUILD_ORDER = ["Z1", "Z3", "Z2", "Z4", "Z5", "Z6", "Z7"]
# ---- Player Start ----
PLAYER_START = {
"location": (500, 720, 0),
"rotation": (0, 0, 0), # 面向 +Y(北)
}
# ---- 光照 ----
LIGHTING = {
"directional_light": {
"location": (0, 0, 500),
"rotation": (-45, 45, 0), # 从西南照射,仰角45°
"intensity": 3.0,
"color": (1.0, 0.95, 0.85), # 暖白
},
"sky_light": {
"intensity": 0.5,
},
"street_lights": [
{"location": (1500, 1050, 0), "intensity": 0.3},
{"location": (3000, 1050, 0), "intensity": 0.3},
{"location": (4500, 1050, 0), "intensity": 0.3},
],
}
+199
View File
@@ -0,0 +1,199 @@
"""Level Builder CLI — 白模关卡自动生成工具。
用法:
# Stage 1: 解析搭建指南 → JSON
python main.py parse --input ../../docs/technical/关卡-v01-搭建指南.md --output output/level_v01.json
# Stage 1.5: 验证
python main.py validate --input output/level_v01.json [--project-root ../..]
# Stage 2: UE5 编辑器内构建(需在编辑器 Python 环境中运行)
# 方式 A: 在 Output Log Python 控制台:
# exec(open(r"tools/level-builder/main.py").read())
# main(["build", "--input", "output/level_v01.json"])
# 方式 B: File > Execute Python Script
# Stage 3: 生成报告
python main.py report --input output/level_v01.json [--build-log output/build_log.json] --output report.md
"""
import argparse
import json
import logging
import sys
from pathlib import Path
# 将项目 tools 目录加入 sys.path
_TOOLS_DIR = Path(__file__).resolve().parent
_LEVEL_BUILDER_DIR = _TOOLS_DIR
_PROJECT_ROOT = _TOOLS_DIR.parent.parent
if str(_TOOLS_DIR) not in sys.path:
sys.path.insert(0, str(_TOOLS_DIR))
if str(_PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(_PROJECT_ROOT))
# 配置日志
logging.basicConfig(
level=logging.INFO,
format="[%(levelname)s] %(name)s: %(message)s",
)
logger = logging.getLogger("level_builder")
def cmd_parse(args):
"""解析搭建指南 Markdown → JSON。"""
from parsers.build_guide_parser import BuildGuideParser
input_path = args.input
output_path = args.output or "output/level_data.json"
logger.info(f"解析搭建指南: {input_path}")
parser = BuildGuideParser()
data = parser.parse_file(input_path)
# 写入 JSON
out_path = Path(output_path)
out_path.parent.mkdir(parents=True, exist_ok=True)
with open(out_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
summary = data.get("summary", {})
logger.info(f"解析完成: {summary.get('total_actors', 0)} 个 Actor")
logger.info(f" 按类别: {summary.get('by_category', {})}")
logger.info(f" 按区域: {summary.get('by_zone', {})}")
logger.info(f" 唯一资产数: {summary.get('unique_assets', 0)}")
logger.info(f"输出: {out_path.resolve()}")
def cmd_validate(args):
"""验证 JSON 数据中的资产引用。"""
from validation.asset_validator import validate_actors
input_path = args.input
project_root = args.project_root or str(_PROJECT_ROOT)
logger.info(f"验证: {input_path}")
with open(input_path, "r", encoding="utf-8") as f:
data = json.load(f)
actors = data.get("actors", [])
result = validate_actors(actors, project_root)
if result.is_valid:
logger.info("✅ 验证通过!")
else:
logger.error(f"❌ 验证失败: {len(result.errors)} 个错误")
for err in result.errors:
logger.error(f" {err}")
if result.warnings:
for warn in result.warnings:
logger.warning(f" {warn}")
logger.info(f"有效 Actor: {result.valid_actors}/{result.total_actors}")
# 保存验证结果
output_path = Path(input_path).parent / "validation_result.json"
with open(output_path, "w", encoding="utf-8") as f:
json.dump({
"is_valid": result.is_valid,
"total": result.total_actors,
"valid": result.valid_actors,
"errors": result.errors,
"warnings": result.warnings,
"missing_assets": sorted(result.missing_assets),
"missing_materials": sorted(result.missing_materials),
}, f, ensure_ascii=False, indent=2)
logger.info(f"验证结果: {output_path}")
def cmd_build(args):
"""在 UE5 编辑器中构建关卡。"""
try:
from builder.scene_builder import build_level
except ImportError:
print("[错误] 无法导入 builder 模块。确保在 UE5 编辑器 Python 环境中运行。")
print(" 在 Output Log 中切换到 Python,然后执行:")
print(f' exec(open(r"{Path(__file__).resolve()}").read())')
print(f' build_level(r"{Path(args.input).resolve()}")')
return
zones = args.zones.split(",") if args.zones else None
build_level(args.input, clear_existing=args.clear, zones=zones)
def cmd_report(args):
"""生成搭建报告。"""
from report.report_generator import generate_report
input_path = args.input
output_path = args.output or "output/搭建报告.md"
build_log_path = args.build_log
logger.info(f"生成报告: {input_path}")
with open(input_path, "r", encoding="utf-8") as f:
data = json.load(f)
build_log = None
if build_log_path:
try:
with open(build_log_path, "r", encoding="utf-8") as f:
build_log = json.load(f)
except FileNotFoundError:
logger.warning(f"构建日志不存在: {build_log_path},生成无状态报告")
report = generate_report(data, build_log, output_path)
logger.info(f"报告已生成: {output_path}")
def main(argv=None):
"""CLI 入口。"""
parser = argparse.ArgumentParser(
description="ChildHood 白模关卡自动生成工具",
prog="level-builder",
)
subparsers = parser.add_subparsers(dest="command", help="子命令")
# parse
p_parse = subparsers.add_parser("parse", help="解析搭建指南 Markdown → JSON")
p_parse.add_argument("--input", "-i", required=True, help="搭建指南 Markdown 文件路径")
p_parse.add_argument("--output", "-o", default="output/level_data.json", help="输出 JSON 路径")
# validate
p_validate = subparsers.add_parser("validate", help="验证 JSON 数据")
p_validate.add_argument("--input", "-i", required=True, help="JSON 文件路径")
p_validate.add_argument("--project-root", "-r", help="项目根目录(用于文件存在性检查)")
# build
p_build = subparsers.add_parser("build", help="在 UE5 中构建关卡")
p_build.add_argument("--input", "-i", required=True, help="JSON 文件路径")
p_build.add_argument("--clear", action="store_true", default=True, help="清除已有 LVL_* Actor")
p_build.add_argument("--no-clear", dest="clear", action="store_false", help="不清除已有 Actor")
p_build.add_argument("--zones", "-z", help="仅构建指定区域,逗号分隔(如 Z1,Z3)")
# report
p_report = subparsers.add_parser("report", help="生成搭建报告")
p_report.add_argument("--input", "-i", required=True, help="JSON 文件路径")
p_report.add_argument("--build-log", "-l", help="构建日志 JSON 路径")
p_report.add_argument("--output", "-o", default="output/搭建报告.md", help="报告输出路径")
args = parser.parse_args(argv)
if args.command == "parse":
cmd_parse(args)
elif args.command == "validate":
cmd_validate(args)
elif args.command == "build":
cmd_build(args)
elif args.command == "report":
cmd_report(args)
else:
parser.print_help()
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,9 @@
{
"is_valid": true,
"total": 265,
"valid": 265,
"errors": [],
"warnings": [],
"missing_assets": [],
"missing_materials": []
}
@@ -0,0 +1,455 @@
# 白模搭建报告 — 自动生成
> 生成时间: 2026-06-07 23:03
> 数据来源: 关卡-v01-搭建指南.md
> 场景尺寸: 8000cm × 6000cm
## 一、搭建概要
- **总 Actor 数**: 265
- 结构体 (STR): 123
- 布景 (PROP): 53
- 交互物 (INTR): 10
- 收集品 (COLL): 9
- 边界 (BND): 61
- 光源 (LGT): 8
- PlayerStart (PLAY): 1
## 二、逐区域对照
### Z1 家
共 40 个 Actor
| # | Actor 名称 | 资产 | 位置 | 缩放 | 材质 | 状态 |
|---|-----------|------|------|------|------|------|
| 1 | LVL_Z1_STR_Floor | Floor_1000x1000 | (500,400,0) | (1.00,0.80,1.00) | MI_DevGray | ✅ |
| 2 | LVL_Z1_STR_SouthWall_01 | Wall_400x300 | (200,0,0) | — | MI_DevRed | ✅ |
| 3 | LVL_Z1_STR_SouthWall_02 | Wall_400x300 | (600,0,0) | — | MI_DevRed | ✅ |
| 4 | LVL_Z1_STR_WestWall_01 | Wall_400x300 | (0,200,0) | — | MI_DevRed | ✅ |
| 5 | LVL_Z1_STR_WestWall_02 | Wall_400x300 | (0,600,0) | — | MI_DevRed | ✅ |
| 6 | LVL_Z1_STR_EastWall_01 | Wall_400x300 | (980,200,0) | — | MI_DevRed | ✅ |
| 7 | LVL_Z1_STR_EastWall_02 | Wall_400x300 | (980,600,0) | — | MI_DevRed | ✅ |
| 8 | LVL_Z1_STR_NorthWall_West | Wall_400x300 | (200,780,0) | — | MI_DevRed | ✅ |
| 9 | LVL_Z1_STR_NorthWall_Gate | WallDoor_200x300 | (500,780,0) | — | MI_DevRed | ✅ |
| 10 | LVL_Z1_STR_NorthWall_East | Wall_400x300 | (800,780,0) | — | MI_DevRed | ✅ |
| 11 | LVL_Z1_STR_GateDoor | RotativeDoor_01 | (500,780,0) | — | MI_DevGreen | ✅ |
| 12 | LVL_Z1_STR_HouseSouthWall | WallWindow_400x300 | (400,30,0) | — | MI_DevWhite | ✅ |
| 13 | LVL_Z1_STR_HouseSouthWin | WallWindowSmall_200x300 | (600,30,0) | — | MI_DevWhite | ✅ |
| 14 | LVL_Z1_STR_HouseWestWall | Wall_400x300 | (200,220,0) | — | MI_DevWhite | ✅ |
| 15 | LVL_Z1_STR_HouseWestTop | Wall_400x100 | (200,220,300) | — | MI_DevWhite | ✅ |
| 16 | LVL_Z1_STR_HouseEastWall | Wall_400x300 | (780,220,0) | — | MI_DevWhite | ✅ |
| 17 | LVL_Z1_STR_HouseEastTop | Wall_400x100 | (780,220,300) | — | MI_DevWhite | ✅ |
| 18 | LVL_Z1_STR_HouseNorthWall | WallDoor_200x300 | (400,410,0) | — | MI_DevWhite | ✅ |
| 19 | LVL_Z1_STR_HouseNorthFill | Wall_400x300 | (600,410,0) | — | MI_DevWhite | ✅ |
| 20 | LVL_Z1_STR_HouseRoof_01 | Floor_300x400 | (350,230,280) | — | MI_DevBrown | ✅ |
| 21 | LVL_Z1_STR_HouseRoof_02 | Floor_300x400 | (650,230,280) | — | MI_DevBrown | ✅ |
| 22 | LVL_Z1_STR_CourtyardFloor | Floor_1000x1000 | (500,400,0) | (1.00,0.80,1.00) | MI_DevGray | ✅ |
| 23 | LVL_Z1_PROP_OldBicycle | Car01 | (100,650,0) | (0.20,0.20,0.20) | — | ✅ |
| 24 | LVL_Z1_PROP_ClothesPole_Beam | Beam_300 | (500,600,180) | — | MI_DevWhite | ✅ |
| 25 | LVL_Z1_PROP_ClothesPole_Pole1 | Cylinder | (380,600,0) | (0.05,0.05,3.60) | MI_DevGray | ✅ |
| 26 | LVL_Z1_PROP_ClothesPole_Pole2 | Cylinder | (620,600,0) | (0.05,0.05,3.60) | MI_DevGray | ✅ |
| 27 | LVL_Z1_PROP_JunkPile_Box1 | WoodenBox_Small | (850,100,0) | — | — | ✅ |
| 28 | LVL_Z1_PROP_JunkPile_Box2 | WoodenBox_Small | (870,120,35) | — | — | ✅ |
| 29 | LVL_Z1_PROP_JunkPile_Bucket | Bucket_01 | (890,80,0) | — | — | ✅ |
| 30 | LVL_Z1_PROP_DoorMat | Cube | (480,740,0) | (0.60,0.40,0.02) | MI_DevGray | ✅ |
| 31 | LVL_Z1_PROP_Fridge | Barrel | (720,80,0) | (0.30,0.30,1.60) | MI_DevWhite | ✅ |
| 32 | LVL_Z1_PROP_TV | Monitor_01 | (320,80,45) | — | MI_DevGray | ✅ |
| 33 | LVL_Z1_PROP_Desk | SimpleTable_01 | (300,70,0) | — | — | ✅ |
| 34 | LVL_Z1_PROP_Bed | BedSingle | (350,250,0) | — | — | ✅ |
| 35 | LVL_Z1_PROP_Calendar | Cube | (500,40,180) | (0.30,0.02,0.40) | MI_DevRed | ✅ |
| 36 | LVL_Z1_PROP_TVStand | SimpleTable_02 | (320,55,0) | (0.85,0.75,0.67) | — | ✅ |
| 37 | LVL_Z1_INTR_01_Fridge | Cube | (720,80,80) | (0.30,0.30,0.30) | MI_DevYellow | ✅ |
| 38 | LVL_Z1_INTR_02_Gate | Cube | (500,780,0) | (0.30,0.30,0.30) | MI_DevYellow | ✅ |
| 39 | LVL_Z1_INTR_06_TV | Cube | (320,80,70) | (0.30,0.30,0.30) | MI_DevYellow | ✅ |
| 40 | LVL_Z1_COLL_10_CatEyeMarble | CirclePlatform | (870,140,0) | (0.03,0.03,0.03) | MI_DevYellow | ✅ |
### Z2 小卖部
共 26 个 Actor
| # | Actor 名称 | 资产 | 位置 | 缩放 | 材质 | 状态 |
|---|-----------|------|------|------|------|------|
| 1 | LVL_Z2_STR_Floor | Floor_1000x1000 | (1300,500,0) | (0.60,0.60,1.00) | MI_DevGray | ✅ |
| 2 | LVL_Z2_STR_SouthWall | Wall_400x300 | (1200,200,0) | — | MI_DevOrange | ✅ |
| 3 | LVL_Z2_STR_SouthWall2 | Wall_200x300 | (1500,200,0) | — | MI_DevOrange | ✅ |
| 4 | LVL_Z2_STR_WestWall | Wall_400x300 | (1000,400,0) | — | MI_DevOrange | ✅ |
| 5 | LVL_Z2_STR_WestWallTop | Wall_400x100 | (1000,400,300) | — | MI_DevOrange | ✅ |
| 6 | LVL_Z2_STR_EastWall | Wall_400x300 | (1580,400,0) | — | MI_DevOrange | ✅ |
| 7 | LVL_Z2_STR_EastWallTop | Wall_400x100 | (1580,400,300) | — | MI_DevOrange | ✅ |
| 8 | LVL_Z2_STR_NorthWall_West | Wall_200x300 | (1100,580,0) | — | MI_DevOrange | ✅ |
| 9 | LVL_Z2_STR_NorthWall_Garage | WallDoorGarage_400x300 | (1300,580,0) | — | MI_DevOrange | ✅ |
| 10 | LVL_Z2_STR_NorthWall_East | Wall_200x300 | (1500,580,0) | — | MI_DevOrange | ✅ |
| 11 | LVL_Z2_STR_GarageDoor | GarageDoorOpen_01 | (1300,580,0) | — | MI_DevGray | ✅ |
| 12 | LVL_Z2_STR_Roof_01 | Floor_300x400 | (1250,400,280) | — | MI_DevBrown | ✅ |
| 13 | LVL_Z2_STR_Roof_02 | Floor_300x400 | (1350,400,280) | — | MI_DevBrown | ✅ |
| 14 | LVL_Z2_STR_Awning_01 | Floor_200x300 | (1100,700,260) | — | MI_DevWhite | ✅ |
| 15 | LVL_Z2_STR_Awning_02 | Floor_200x300 | (1400,700,260) | — | MI_DevWhite | ✅ |
| 16 | LVL_Z2_PROP_Counter | Cube | (1200,500,0) | (2.00,0.60,0.90) | MI_DevGlassWhite | ✅ |
| 17 | LVL_Z2_PROP_ShelfEast_01 | Fence_Small_400 | (1520,300,0) | (1.00,1.00,1.80) | MI_DevGray | ✅ |
| 18 | LVL_Z2_PROP_ShelfEast_02 | Fence_Small_400 | (1520,300,180) | (1.00,1.00,1.80) | MI_DevGray | ✅ |
| 19 | LVL_Z2_PROP_ShelfSouth | Fence_Small_400 | (1100,230,0) | (1.00,1.00,1.80) | MI_DevGray | ✅ |
| 20 | LVL_Z2_PROP_Freezer | Container_Normal | (1050,400,0) | (0.25,0.15,0.40) | MI_DevWhite | ✅ |
| 21 | LVL_Z2_PROP_SmallTV | Monitor_01 | (1350,540,90) | (0.60,0.60,0.60) | MI_DevGray | ✅ |
| 22 | LVL_Z2_PROP_Signboard | Cube | (1150,610,280) | (3.00,0.05,0.40) | MI_DevBlue | ✅ |
| 23 | LVL_Z2_PROP_CardRack | Cube | (1400,550,0) | (0.40,0.20,0.30) | MI_DevGray | ✅ |
| 24 | LVL_Z2_INTR_05_ShopOwner | Cube | (1300,480,0) | (0.30,0.30,0.30) | MI_DevYellow | ✅ |
| 25 | LVL_Z2_INTR_07_SnackShelf | Cube | (1520,350,90) | (0.30,0.30,0.30) | MI_DevYellow | ✅ |
| 26 | LVL_Z2_COLL_09_BouncyBall | CirclePlatform | (1300,700,0) | (0.03,0.03,0.03) | MI_DevWhite | ✅ |
### Z3 主街道
共 42 个 Actor
| # | Actor 名称 | 资产 | 位置 | 缩放 | 材质 | 状态 |
|---|-----------|------|------|------|------|------|
| 1 | LVL_Z3_STR_Floor_01 | Floor_1000x1000 | (500,1050,0) | (1.00,0.50,1.00) | MI_DevGray | ✅ |
| 2 | LVL_Z3_STR_Floor_02 | Floor_1000x1000 | (1500,1050,0) | (1.00,0.50,1.00) | MI_DevGray | ✅ |
| 3 | LVL_Z3_STR_Floor_03 | Floor_1000x1000 | (2500,1050,0) | (1.00,0.50,1.00) | MI_DevGray | ✅ |
| 4 | LVL_Z3_STR_Floor_04 | Floor_1000x1000 | (3500,1050,0) | (1.00,0.50,1.00) | MI_DevGray | ✅ |
| 5 | LVL_Z3_STR_Floor_05 | Floor_1000x1000 | (4500,1050,0) | (1.00,0.50,1.00) | MI_DevGray | ✅ |
| 6 | LVL_Z3_STR_SouthWall_C1 | Wall_400x300 | (1800,800,0) | — | MI_DevGray | ✅ |
| 7 | LVL_Z3_STR_SouthWall_C2 | Wall_400x300 | (2200,800,0) | — | MI_DevGray | ✅ |
| 8 | LVL_Z3_STR_SouthWall_C3 | Wall_400x300 | (2600,800,0) | — | MI_DevGray | ✅ |
| 9 | LVL_Z3_STR_SouthWall_C4 | Wall_400x300 | (3000,800,0) | — | MI_DevGray | ✅ |
| 10 | LVL_Z3_STR_SouthWall_C5 | Wall_400x300 | (3400,800,0) | — | MI_DevGray | ✅ |
| 11 | LVL_Z3_STR_SouthWall_C6 | Wall_400x300 | (3800,800,0) | — | MI_DevGray | ✅ |
| 12 | LVL_Z3_STR_SouthWall_D1 | Wall_400x300 | (4200,800,0) | — | MI_DevGray | ✅ |
| 13 | LVL_Z3_STR_SouthWall_D2 | Wall_400x300 | (4600,800,0) | — | MI_DevGray | ✅ |
| 14 | LVL_Z3_STR_NorthWall_A1 | Wall_400x300 | (200,1300,0) | — | MI_DevGray | ✅ |
| 15 | LVL_Z3_STR_NorthWall_A2 | Wall_400x300 | (600,1300,0) | — | MI_DevGray | ✅ |
| 16 | LVL_Z3_STR_NorthWall_A3 | Wall_400x300 | (1000,1300,0) | — | MI_DevGray | ✅ |
| 17 | LVL_Z3_STR_NorthWall_A4 | Wall_400x300 | (1400,1300,0) | — | MI_DevGray | ✅ |
| 18 | LVL_Z3_STR_NorthWall_A5 | Wall_400x300 | (1800,1300,0) | — | MI_DevGray | ✅ |
| 19 | LVL_Z3_STR_NorthWall_A6 | Wall_200x300 | (2100,1300,0) | — | MI_DevGray | ✅ |
| 20 | LVL_Z3_STR_NorthWall_C1 | Wall_400x300 | (2800,1300,0) | — | MI_DevGray | ✅ |
| 21 | LVL_Z3_STR_NorthWall_C2 | Wall_400x300 | (3200,1300,0) | — | MI_DevGray | ✅ |
| 22 | LVL_Z3_STR_NorthWall_C3 | Wall_100x300 | (3450,1300,0) | — | MI_DevGray | ✅ |
| 23 | LVL_Z3_PROP_PhoneBooth | Cube | (1800,380,0) | (0.80,0.80,2.20) | MI_DevGray | ✅ |
| 24 | LVL_Z3_PROP_Bicycle | Car01 | (2800,880,0) | (0.20,0.20,0.20) | — | ✅ |
| 25 | LVL_Z3_PROP_ClothesPole | Beam_300 | (1200,1250,180) | — | MI_DevWhite | ✅ |
| 26 | LVL_Z3_PROP_ClothesPolePole | Cylinder | (1200,1250,0) | (0.05,0.05,3.60) | MI_DevGray | ✅ |
| 27 | LVL_Z3_PROP_SewerCover | CirclePlatform | (800,1050,0) | (0.12,0.12,0.02) | MI_DevGray | ✅ |
| 28 | LVL_Z3_PROP_Graffiti | Cube | (3200,1270,100) | (1.20,0.05,0.80) | MI_DevYellow | ✅ |
| 29 | LVL_Z3 PROP_AdWall | DisplayPanels_400x200 | (3800,1280,0) | — | MI_DevGray | ✅ |
| 30 | LVL_Z3_PROP_TrashCan | TrashCans_01 | (4200,850,0) | — | MI_DevGreen | ✅ |
| 31 | LVL_Z3_PROP_PoleWest | Cylinder | (1000,1050,0) | (0.04,0.04,12.00) | MI_DevGray | ✅ |
| 32 | LVL_Z3_PROP_PoleEast | Cylinder | (3500,1050,0) | (0.04,0.04,12.00) | MI_DevGray | ✅ |
| 33 | LVL_Z3_INTR_04_PhoneBooth | Cube | (1800,380,110) | (0.30,0.30,0.30) | MI_DevYellow | ✅ |
| 34 | LVL_Z3_INTR_09_Graffiti | Cube | (3200,1270,140) | (0.30,0.30,0.30) | MI_DevYellow | ✅ |
| 35 | LVL_Z3_COLL_08_IceFlowerMarble | CirclePlatform | (820,1060,0) | (0.03,0.03,0.03) | MI_DevWhite | ✅ |
| 36 | LVL_Z3_LGT_StreetLight_01 | RoadLamp_01 | (1500,1050,0) | — | — | ✅ |
| 37 | LVL_Z3_LGT_StreetLight_02 | RoadLamp_01 | (3000,1050,0) | — | — | ✅ |
| 38 | LVL_Z3_LGT_StreetLight_03 | RoadLamp_01 | (4500,1050,0) | — | — | ✅ |
| 39 | LVL_Z3_PROP_TrashCan_02 | TrashCans_01 | (2000,850,0) | — | MI_DevGreen | ✅ |
| 40 | LVL_Z3_LGT_PointLight_01 | | (1500,1050,400) | — | — | ✅ |
| 41 | LVL_Z3_LGT_PointLight_02 | | (3000,1050,400) | — | — | ✅ |
| 42 | LVL_Z3_LGT_PointLight_03 | | (4500,1050,400) | — | — | ✅ |
### Z4 空地
共 20 个 Actor
| # | Actor 名称 | 资产 | 位置 | 缩放 | 材质 | 状态 |
|---|-----------|------|------|------|------|------|
| 1 | LVL_Z4_STR_Floor_01 | Floor_1000x1000 | (4000,1800,10) | (1.50,1.20,1.00) | MI_DevBrown | ✅ |
| 2 | LVL_Z4_STR_EastBound_01 | BarrierWall_600 | (4980,1600,0) | — | MI_DevBlue | ✅ |
| 3 | LVL_Z4_STR_EastBound_02 | BarrierWall_600 | (4980,2200,0) | — | MI_DevBlue | ✅ |
| 4 | LVL_Z4_STR_NorthBound_01 | BarrierWall_600 | (3800,2480,0) | — | MI_DevBlue | ✅ |
| 5 | LVL_Z4_STR_NorthBound_02 | BarrierWall_600 | (4400,2480,0) | — | MI_DevBlue | ✅ |
| 6 | LVL_Z4_STR_NorthBound_03 | BarrierWall_200 | (4700,2480,0) | — | MI_DevBlue | ✅ |
| 7 | LVL_Z4_STR_WestLowWall_01 | BarrierWall_200 | (3500,1500,0) | — | MI_DevGray | ✅ |
| 8 | LVL_Z4_STR_WestLowWall_02 | BarrierWall_200 | (3500,1900,0) | — | MI_DevGray | ✅ |
| 9 | LVL_Z4_STR_WestLowWall_03 | BarrierWall_200 | (3500,2300,0) | — | MI_DevGray | ✅ |
| 10 | LVL_Z4_PROP_BigTree | Tree_01 | (4250,1900,10) | (1.20,1.20,1.20) | MI_DevFoliage | ✅ |
| 11 | LVL_Z4_PROP_Bricks_01~05 | WoodenBox_Small | (0,0,0) | — | — | ✅ |
| 12 | LVL_Z4_PROP_Puddle | Cube | (4600,1500,5) | (1.20,0.80,0.01) | MI_DevGlassBlue | ✅ |
| 13 | LVL_Z4_PROP_MarbleCircle | CirclePlatform | (4100,1800,12) | (0.16,0.16,0.01) | MI_DevGray | ✅ |
| 14 | LVL_Z4_PROP_Debris_01 | WoodenPallet_01 | (4800,2300,10) | — | — | ✅ |
| 15 | LVL_Z4_PROP_Debris_02 | WoodenBox_Normal | (4850,2250,10) | — | — | ✅ |
| 16 | LVL_Z4_INTR_03_BlueMarble | Cube | (3900,1700,10) | (0.30,0.30,0.30) | MI_DevYellow | ✅ |
| 17 | LVL_Z4_INTR_08_SmallStone | Cube | (4550,1550,10) | (0.30,0.30,0.30) | MI_DevYellow | ✅ |
| 18 | LVL_Z4_COLL_01_BlueMarble | CirclePlatform | (3900,1700,10) | (0.03,0.03,0.03) | MI_DevWhite | ✅ |
| 19 | LVL_Z4_COLL_03_SmallStone | CirclePlatform | (4550,1550,10) | (0.03,0.03,0.03) | MI_DevWhite | ✅ |
| 20 | LVL_Z4_COLL_04_LuZhishenCard | CirclePlatform | (3720,1610,10) | (0.03,0.03,0.03) | MI_DevYellow | ✅ |
### Z5 巷子
共 19 个 Actor
| # | Actor 名称 | 资产 | 位置 | 缩放 | 材质 | 状态 |
|---|-----------|------|------|------|------|------|
| 1 | LVL_Z5_STR_Floor_01 | Floor_1000x1000 | (2450,1800,0) | (0.30,1.00,1.00) | MI_DevGray | ✅ |
| 2 | LVL_Z5_STR_Floor_02 | Floor_1000x1000 | (2450,2800,0) | (0.30,1.00,1.00) | MI_DevGray | ✅ |
| 3 | LVL_Z5_STR_WestWall_01 | Wall_400x300 | (2300,1500,0) | — | MI_DevWhite | ✅ |
| 4 | LVL_Z5_STR_WestWall_02 | Wall_400x300 | (2300,1900,0) | — | MI_DevWhite | ✅ |
| 5 | LVL_Z5_STR_WestWall_03 | Wall_400x300 | (2300,2300,0) | — | MI_DevWhite | ✅ |
| 6 | LVL_Z5_STR_WestWall_04 | Wall_400x300 | (2300,2700,0) | — | MI_DevWhite | ✅ |
| 7 | LVL_Z5_STR_WestWall_05 | Wall_400x300 | (2300,3100,0) | — | MI_DevWhite | ✅ |
| 8 | LVL_Z5_STR_EastWall_01 | Wall_400x300 | (2580,1500,0) | — | MI_DevWhite | ✅ |
| 9 | LVL_Z5_STR_EastWall_02 | Wall_400x300 | (2580,1900,0) | — | MI_DevWhite | ✅ |
| 10 | LVL_Z5_STR_EastWall_03 | Wall_400x300 | (2580,2300,0) | — | MI_DevWhite | ✅ |
| 11 | LVL_Z5_STR_EastWall_04 | Wall_400x300 | (2580,2700,0) | — | MI_DevWhite | ✅ |
| 12 | LVL_Z5_STR_EastWall_05 | Wall_400x300 | (2580,3100,0) | — | MI_DevWhite | ✅ |
| 13 | LVL_Z5_PROP_WasteBox | WoodenBox_Small | (2380,2900,0) | — | — | ✅ |
| 14 | LVL_Z5_PROP_SpiderWeb | Cube | (2305,2100,250) | (0.30,0.30,0.01) | MI_DevWhite | ✅ |
| 15 | LVL_Z5_PROP_OldWindow | FakeWindowsFrame | (2310,2100,150) | — | MI_DevGray | ✅ |
| 16 | LVL_Z5_PROP_FlowerPot | Cylinder | (2340,2100,150) | (0.20,0.20,0.25) | MI_DevGreen | ✅ |
| 17 | LVL_Z5_PROP_Moss | Cube | (2400,2700,0) | (0.60,0.40,0.01) | MI_DevFoliage | ✅ |
| 18 | LVL_Z5_INTR_10_WasteBox | Cube | (2380,2900,25) | (0.30,0.30,0.30) | MI_DevYellow | ✅ |
| 19 | LVL_Z5_COLL_02_RedMarble | CirclePlatform | (2500,3000,0) | (0.03,0.03,0.03) | MI_DevWhite | ✅ |
### Z6 水塘边
共 16 个 Actor
| # | Actor 名称 | 资产 | 位置 | 缩放 | 材质 | 状态 |
|---|-----------|------|------|------|------|------|
| 1 | LVL_Z6_STR_GroundSouth | Floor_1000x1000 | (2350,3475,0) | (1.50,0.35,1.00) | MI_DevBrown | ✅ |
| 2 | LVL_Z6_STR_GroundEast | Floor_1000x1000 | (3075,4000,0) | (0.15,0.80,1.00) | MI_DevBrown | ✅ |
| 3 | LVL_Z6_STR_GroundWest | Floor_1000x1000 | (1675,4000,0) | (0.15,0.80,1.00) | MI_DevBrown | ✅ |
| 4 | LVL_Z6_STR_Water | Floor_1000x1000 | (2350,4000,-20) | (1.20,0.80,0.50) | MI_DevSkyBlue | ✅ |
| 5 | LVL_Z6_STR_EastBound_01 | BarrierWall_600 | (3080,3600,0) | — | MI_DevBlue | ✅ |
| 6 | LVL_Z6_STR_EastBound_02 | BarrierWall_600 | (3080,4200,0) | — | MI_DevBlue | ✅ |
| 7 | LVL_Z6_STR_NorthBound_01 | BarrierWall_600 | (1900,4680,0) | — | MI_DevBlue | ✅ |
| 8 | LVL_Z6_STR_NorthBound_02 | BarrierWall_600 | (2500,4680,0) | — | MI_DevBlue | ✅ |
| 9 | LVL_Z6_STR_NorthBound_03 | BarrierWall_200 | (2800,4680,0) | — | MI_DevBlue | ✅ |
| 10 | LVL_Z6_PROP_Reeds_S1~5 | Cylinder | (1800,3600,0) | (0.08,0.08,2.00) | MI_DevFoliage | ✅ |
| 11 | LVL_Z6_PROP_Reeds_S2_1~5 | Cylinder | (2700,3600,0) | (0.08,0.08,2.00) | MI_DevFoliage | ✅ |
| 12 | LVL_Z6_PROP_Reeds_E_1~3 | Cylinder | (2900,4100,0) | (0.08,0.08,1.60) | MI_DevFoliage | ✅ |
| 13 | LVL_Z6_PROP_LilyPad_1~4 | CirclePlatform | (2200,3900,-18) | (0.06,0.06,0.01) | MI_DevGreen | ✅ |
| 14 | LVL_Z6_PROP_Frog | Cube | (2100,3680,0) | (0.05,0.05,0.04) | MI_DevGreen | ✅ |
| 15 | LVL_Z6_COLL_05_CandyWrapper | CirclePlatform | (2000,3500,0) | (0.03,0.03,0.03) | MI_DevWhite | ✅ |
| 16 | LVL_Z6_COLL_06_Eraser | CirclePlatform | (2500,3450,0) | (0.03,0.03,0.03) | MI_DevWhite | ✅ |
### Z7 老居民楼
共 38 个 Actor
| # | Actor 名称 | 资产 | 位置 | 缩放 | 材质 | 状态 |
|---|-----------|------|------|------|------|------|
| 1 | LVL_BND_West_01 | Wall_400x300 | (0,200,0) | — | MI_DevRed | ✅ |
| 2 | LVL_BND_West_02 | Wall_400x300 | (0,600,0) | — | MI_DevRed | ✅ |
| 3 | LVL_BND_West_03 | Wall_400x300 | (0,1000,0) | — | MI_DevRed | ✅ |
| 4 | LVL_BND_West_04 | Wall_400x300 | (0,1400,0) | — | MI_DevRed | ✅ |
| 5 | LVL_BND_West_05 | Wall_400x300 | (0,1800,0) | — | MI_DevRed | ✅ |
| 6 | LVL_BND_West_06 | Wall_400x300 | (0,2200,0) | — | MI_DevRed | ✅ |
| 7 | LVL_BND_West_07 | Wall_400x300 | (0,2600,0) | — | MI_DevRed | ✅ |
| 8 | LVL_BND_West_08 | Wall_400x300 | (0,3000,0) | — | MI_DevRed | ✅ |
| 9 | LVL_BND_West_09 | Wall_400x300 | (0,3400,0) | — | MI_DevRed | ✅ |
| 10 | LVL_BND_West_10 | Wall_400x300 | (0,3800,0) | — | MI_DevRed | ✅ |
| 11 | LVL_BND_West_11 | Wall_400x300 | (0,4200,0) | — | MI_DevRed | ✅ |
| 12 | LVL_BND_West_12 | Wall_400x300 | (0,4600,0) | — | MI_DevRed | ✅ |
| 13 | LVL_BND_West_13 | Wall_400x300 | (0,5000,0) | — | MI_DevRed | ✅ |
| 14 | LVL_BND_West_14 | Wall_400x300 | (0,5400,0) | — | MI_DevRed | ✅ |
| 15 | LVL_BND_West_15 | Wall_400x300 | (0,5800,0) | — | MI_DevRed | ✅ |
| 16 | LVL_BND_East_01~15 | BarrierWall_600 | (0,0,0) | — | MI_DevBlue | ✅ |
| 17 | LVL_BND_North_01~20 | Wall_400x300 | (0,0,0) | — | MI_DevBlue | ✅ |
| 18 | LVL_BND_South_01 | BarrierWall_600 | (1900,0,0) | — | MI_DevBlue | ✅ |
| 19 | LVL_BND_South_02 | BarrierWall_600 | (2500,0,0) | — | MI_DevBlue | ✅ |
| 20 | LVL_BND_South_03 | BarrierWall_600 | (3100,0,0) | — | MI_DevBlue | ✅ |
| 21 | LVL_BND_South_04 | BarrierWall_600 | (3700,0,0) | — | MI_DevBlue | ✅ |
| 22 | LVL_BND_South_05 | BarrierWall_600 | (4300,0,0) | — | MI_DevBlue | ✅ |
| 23 | LVL_BND_South_06 | BarrierWall_600 | (4900,0,0) | — | MI_DevBlue | ✅ |
| 24 | LVL_BND_South_07 | BarrierWall_600 | (5500,0,0) | — | MI_DevBlue | ✅ |
| 25 | LVL_BND_South_08 | BarrierWall_600 | (6100,0,0) | — | MI_DevBlue | ✅ |
| 26 | LVL_BND_South_09 | BarrierWall_600 | (6700,0,0) | — | MI_DevBlue | ✅ |
| 27 | LVL_BND_South_10 | BarrierWall_600 | (7300,0,0) | — | MI_DevBlue | ✅ |
| 28 | LVL_BND_South_11 | BarrierWall_600 | (7900,0,0) | — | MI_DevBlue | ✅ |
| 29 | LVL_Z7_STR_Building | Building_01 | (6400,800,0) | — | MI_DevGray | ✅ |
| 30 | LVL_Z7_STR_CollisionFront | Wall_400x300 | (6200,150,0) | — | MI_DevGray | ✅ |
| 31 | LVL_Z7_STR_CollisionFront2 | Wall_400x300 | (6600,150,0) | — | MI_DevGray | ✅ |
| 32 | LVL_Z7_STR_CollisionFront3 | Wall_400x300 | (7000,150,0) | — | MI_DevGray | ✅ |
| 33 | LVL_Z7_STR_CollisionSide | Wall_400x300 | (5750,400,0) | (0.00,90.00,0.00) | MI_DevGray | ✅ |
| 34 | LVL_Z7_STR_CollisionSide2 | Wall_400x300 | (5750,800,0) | (0.00,90.00,0.00) | MI_DevGray | ✅ |
| 35 | LVL_Z7_PROP_ClothesPole_01 | Beam_300 | (6100,190,800) | — | MI_DevWhite | ✅ |
| 36 | LVL_Z7_PROP_ClothesPole_02 | Beam_300 | (6500,190,1200) | — | MI_DevWhite | ✅ |
| 37 | LVL_Z7_PROP_ClothesPole_03 | Beam_300 | (6900,190,1600) | — | MI_DevWhite | ✅ |
| 38 | LVL_Z7_PROP_TrashCan | TrashCans_01 | (6000,140,0) | — | MI_DevGreen | ✅ |
### GLOBAL 全局
共 64 个 Actor
| # | Actor 名称 | 资产 | 位置 | 缩放 | 材质 | 状态 |
|---|-----------|------|------|------|------|------|
| 1 | LVL_BND_West_01 | Wall_400x300 | (0,200,0) | — | MI_DevRed | ✅ |
| 2 | LVL_BND_West_02 | Wall_400x300 | (0,600,0) | — | MI_DevRed | ✅ |
| 3 | LVL_BND_West_03 | Wall_400x300 | (0,1000,0) | — | MI_DevRed | ✅ |
| 4 | LVL_BND_West_04 | Wall_400x300 | (0,1400,0) | — | MI_DevRed | ✅ |
| 5 | LVL_BND_West_05 | Wall_400x300 | (0,1800,0) | — | MI_DevRed | ✅ |
| 6 | LVL_BND_West_06 | Wall_400x300 | (0,2200,0) | — | MI_DevRed | ✅ |
| 7 | LVL_BND_West_07 | Wall_400x300 | (0,2600,0) | — | MI_DevRed | ✅ |
| 8 | LVL_BND_West_08 | Wall_400x300 | (0,3000,0) | — | MI_DevRed | ✅ |
| 9 | LVL_BND_West_09 | Wall_400x300 | (0,3400,0) | — | MI_DevRed | ✅ |
| 10 | LVL_BND_West_10 | Wall_400x300 | (0,3800,0) | — | MI_DevRed | ✅ |
| 11 | LVL_BND_West_11 | Wall_400x300 | (0,4200,0) | — | MI_DevRed | ✅ |
| 12 | LVL_BND_West_12 | Wall_400x300 | (0,4600,0) | — | MI_DevRed | ✅ |
| 13 | LVL_BND_West_13 | Wall_400x300 | (0,5000,0) | — | MI_DevRed | ✅ |
| 14 | LVL_BND_West_14 | Wall_400x300 | (0,5400,0) | — | MI_DevRed | ✅ |
| 15 | LVL_BND_West_15 | Wall_400x300 | (0,5800,0) | — | MI_DevRed | ✅ |
| 16 | LVL_BND_East_01 | BarrierWall_600 | (8000,200,0) | — | MI_DevBlue | ✅ |
| 17 | LVL_BND_East_02 | BarrierWall_600 | (8000,600,0) | — | MI_DevBlue | ✅ |
| 18 | LVL_BND_East_03 | BarrierWall_600 | (8000,1000,0) | — | MI_DevBlue | ✅ |
| 19 | LVL_BND_East_04 | BarrierWall_600 | (8000,1400,0) | — | MI_DevBlue | ✅ |
| 20 | LVL_BND_East_05 | BarrierWall_600 | (8000,1800,0) | — | MI_DevBlue | ✅ |
| 21 | LVL_BND_East_06 | BarrierWall_600 | (8000,2200,0) | — | MI_DevBlue | ✅ |
| 22 | LVL_BND_East_07 | BarrierWall_600 | (8000,2600,0) | — | MI_DevBlue | ✅ |
| 23 | LVL_BND_East_08 | BarrierWall_600 | (8000,3000,0) | — | MI_DevBlue | ✅ |
| 24 | LVL_BND_East_09 | BarrierWall_600 | (8000,3400,0) | — | MI_DevBlue | ✅ |
| 25 | LVL_BND_East_10 | BarrierWall_600 | (8000,3800,0) | — | MI_DevBlue | ✅ |
| 26 | LVL_BND_East_11 | BarrierWall_600 | (8000,4200,0) | — | MI_DevBlue | ✅ |
| 27 | LVL_BND_East_12 | BarrierWall_600 | (8000,4600,0) | — | MI_DevBlue | ✅ |
| 28 | LVL_BND_East_13 | BarrierWall_600 | (8000,5000,0) | — | MI_DevBlue | ✅ |
| 29 | LVL_BND_East_14 | BarrierWall_600 | (8000,5400,0) | — | MI_DevBlue | ✅ |
| 30 | LVL_BND_East_15 | BarrierWall_600 | (8000,5800,0) | — | MI_DevBlue | ✅ |
| 31 | LVL_BND_North_01 | Wall_400x300 | (200,6000,0) | — | MI_DevBlue | ✅ |
| 32 | LVL_BND_North_02 | Wall_400x300 | (600,6000,0) | — | MI_DevBlue | ✅ |
| 33 | LVL_BND_North_03 | Wall_400x300 | (1000,6000,0) | — | MI_DevBlue | ✅ |
| 34 | LVL_BND_North_04 | Wall_400x300 | (1400,6000,0) | — | MI_DevBlue | ✅ |
| 35 | LVL_BND_North_05 | Wall_400x300 | (1800,6000,0) | — | MI_DevBlue | ✅ |
| 36 | LVL_BND_North_06 | Wall_400x300 | (2200,6000,0) | — | MI_DevBlue | ✅ |
| 37 | LVL_BND_North_07 | Wall_400x300 | (2600,6000,0) | — | MI_DevBlue | ✅ |
| 38 | LVL_BND_North_08 | Wall_400x300 | (3000,6000,0) | — | MI_DevBlue | ✅ |
| 39 | LVL_BND_North_09 | Wall_400x300 | (3400,6000,0) | — | MI_DevBlue | ✅ |
| 40 | LVL_BND_North_10 | Wall_400x300 | (3800,6000,0) | — | MI_DevBlue | ✅ |
| 41 | LVL_BND_North_11 | Wall_400x300 | (4200,6000,0) | — | MI_DevBlue | ✅ |
| 42 | LVL_BND_North_12 | Wall_400x300 | (4600,6000,0) | — | MI_DevBlue | ✅ |
| 43 | LVL_BND_North_13 | Wall_400x300 | (5000,6000,0) | — | MI_DevBlue | ✅ |
| 44 | LVL_BND_North_14 | Wall_400x300 | (5400,6000,0) | — | MI_DevBlue | ✅ |
| 45 | LVL_BND_North_15 | Wall_400x300 | (5800,6000,0) | — | MI_DevBlue | ✅ |
| 46 | LVL_BND_North_16 | Wall_400x300 | (6200,6000,0) | — | MI_DevBlue | ✅ |
| 47 | LVL_BND_North_17 | Wall_400x300 | (6600,6000,0) | — | MI_DevBlue | ✅ |
| 48 | LVL_BND_North_18 | Wall_400x300 | (7000,6000,0) | — | MI_DevBlue | ✅ |
| 49 | LVL_BND_North_19 | Wall_400x300 | (7400,6000,0) | — | MI_DevBlue | ✅ |
| 50 | LVL_BND_North_20 | Wall_400x300 | (7800,6000,0) | — | MI_DevBlue | ✅ |
| 51 | LVL_BND_South_01 | BarrierWall_600 | (1900,0,0) | — | MI_DevBlue | ✅ |
| 52 | LVL_BND_South_02 | BarrierWall_600 | (2500,0,0) | — | MI_DevBlue | ✅ |
| 53 | LVL_BND_South_03 | BarrierWall_600 | (3100,0,0) | — | MI_DevBlue | ✅ |
| 54 | LVL_BND_South_04 | BarrierWall_600 | (3700,0,0) | — | MI_DevBlue | ✅ |
| 55 | LVL_BND_South_05 | BarrierWall_600 | (4300,0,0) | — | MI_DevBlue | ✅ |
| 56 | LVL_BND_South_06 | BarrierWall_600 | (4900,0,0) | — | MI_DevBlue | ✅ |
| 57 | LVL_BND_South_07 | BarrierWall_600 | (5500,0,0) | — | MI_DevBlue | ✅ |
| 58 | LVL_BND_South_08 | BarrierWall_600 | (6100,0,0) | — | MI_DevBlue | ✅ |
| 59 | LVL_BND_South_09 | BarrierWall_600 | (6700,0,0) | — | MI_DevBlue | ✅ |
| 60 | LVL_BND_South_10 | BarrierWall_600 | (7300,0,0) | — | MI_DevBlue | ✅ |
| 61 | LVL_BND_South_11 | BarrierWall_600 | (7900,0,0) | — | MI_DevBlue | ✅ |
| 62 | LVL_Global_DirectionalLight | | (0,0,500) | — | — | ✅ |
| 63 | LVL_Global_SkyLight | | (0,0,0) | — | — | ✅ |
| 64 | LVL_PlayerStart | | (500,720,0) | — | — | ✅ |
## 三、资产使用统计
| 资产名 | 使用次数 |
|--------|---------|
| Wall_400x300 | 95 |
| BarrierWall_600 | 46 |
| Cube | 21 |
| Floor_1000x1000 | 15 |
| CirclePlatform | 12 |
| Cylinder | 9 |
| Beam_300 | 5 |
| BarrierWall_200 | 5 |
| Wall_400x100 | 4 |
| Floor_300x400 | 4 |
| WoodenBox_Small | 4 |
| Wall_200x300 | 4 |
| TrashCans_01 | 3 |
| Fence_Small_400 | 3 |
| RoadLamp_01 | 3 |
| WallDoor_200x300 | 2 |
| Car01 | 2 |
| Monitor_01 | 2 |
| Floor_200x300 | 2 |
| RotativeDoor_01 | 1 |
| WallWindow_400x300 | 1 |
| WallWindowSmall_200x300 | 1 |
| Bucket_01 | 1 |
| Barrel | 1 |
| SimpleTable_01 | 1 |
| BedSingle | 1 |
| SimpleTable_02 | 1 |
| Wall_100x300 | 1 |
| DisplayPanels_400x200 | 1 |
| WallDoorGarage_400x300 | 1 |
| GarageDoorOpen_01 | 1 |
| Container_Normal | 1 |
| Tree_01 | 1 |
| WoodenPallet_01 | 1 |
| WoodenBox_Normal | 1 |
| FakeWindowsFrame | 1 |
| Building_01 | 1 |
## 四、缩放记录
| Actor 名称 | 资产 | 缩放 | 实际尺寸 (cm) |
|-----------|------|------|-------------|
| LVL_Z1_STR_Floor | Floor_1000x1000 | (1.00,0.80,1.00) | 1000×800×10 |
| LVL_Z2_STR_Floor | Floor_1000x1000 | (0.60,0.60,1.00) | 600×600×10 |
| LVL_Z3_STR_Floor_01 | Floor_1000x1000 | (1.00,0.50,1.00) | 1000×500×10 |
| LVL_Z3_STR_Floor_02 | Floor_1000x1000 | (1.00,0.50,1.00) | 1000×500×10 |
| LVL_Z3_STR_Floor_03 | Floor_1000x1000 | (1.00,0.50,1.00) | 1000×500×10 |
| LVL_Z3_STR_Floor_04 | Floor_1000x1000 | (1.00,0.50,1.00) | 1000×500×10 |
| LVL_Z3_STR_Floor_05 | Floor_1000x1000 | (1.00,0.50,1.00) | 1000×500×10 |
| LVL_Z4_STR_Floor_01 | Floor_1000x1000 | (1.50,1.20,1.00) | 1500×1200×10 |
| LVL_Z5_STR_Floor_01 | Floor_1000x1000 | (0.30,1.00,1.00) | 300×1000×10 |
| LVL_Z5_STR_Floor_02 | Floor_1000x1000 | (0.30,1.00,1.00) | 300×1000×10 |
| LVL_Z6_STR_GroundSouth | Floor_1000x1000 | (1.50,0.35,1.00) | 1500×350×10 |
| LVL_Z6_STR_GroundEast | Floor_1000x1000 | (0.15,0.80,1.00) | 150×800×10 |
| LVL_Z6_STR_GroundWest | Floor_1000x1000 | (0.15,0.80,1.00) | 150×800×10 |
| LVL_Z6_STR_Water | Floor_1000x1000 | (1.20,0.80,0.50) | 1200×800×5 |
| LVL_Z1_STR_CourtyardFloor | Floor_1000x1000 | (1.00,0.80,1.00) | 1000×800×10 |
| LVL_Z1_PROP_OldBicycle | Car01 | (0.20,0.20,0.20) | (默认尺寸未知) |
| LVL_Z1_PROP_ClothesPole_Pole1 | Cylinder | (0.05,0.05,3.60) | 5×5×360 |
| LVL_Z1_PROP_ClothesPole_Pole2 | Cylinder | (0.05,0.05,3.60) | 5×5×360 |
| LVL_Z1_PROP_DoorMat | Cube | (0.60,0.40,0.02) | 60×40×2 |
| LVL_Z1_PROP_Fridge | Barrel | (0.30,0.30,1.60) | (默认尺寸未知) |
| LVL_Z1_PROP_Calendar | Cube | (0.30,0.02,0.40) | 30×2×40 |
| LVL_Z1_PROP_TVStand | SimpleTable_02 | (0.85,0.75,0.67) | (默认尺寸未知) |
| LVL_Z1_INTR_01_Fridge | Cube | (0.30,0.30,0.30) | 30×30×30 |
| LVL_Z1_INTR_02_Gate | Cube | (0.30,0.30,0.30) | 30×30×30 |
| LVL_Z1_INTR_06_TV | Cube | (0.30,0.30,0.30) | 30×30×30 |
| LVL_Z1_COLL_10_CatEyeMarble | CirclePlatform | (0.03,0.03,0.03) | (默认尺寸未知) |
| LVL_Z3_PROP_PhoneBooth | Cube | (0.80,0.80,2.20) | 80×80×220 |
| LVL_Z3_PROP_Bicycle | Car01 | (0.20,0.20,0.20) | (默认尺寸未知) |
| LVL_Z3_PROP_ClothesPolePole | Cylinder | (0.05,0.05,3.60) | 5×5×360 |
| LVL_Z3_PROP_SewerCover | CirclePlatform | (0.12,0.12,0.02) | (默认尺寸未知) |
| LVL_Z3_PROP_Graffiti | Cube | (1.20,0.05,0.80) | 120×5×80 |
| LVL_Z3_PROP_PoleWest | Cylinder | (0.04,0.04,12.00) | 4×4×1200 |
| LVL_Z3_PROP_PoleEast | Cylinder | (0.04,0.04,12.00) | 4×4×1200 |
| LVL_Z3_INTR_04_PhoneBooth | Cube | (0.30,0.30,0.30) | 30×30×30 |
| LVL_Z3_INTR_09_Graffiti | Cube | (0.30,0.30,0.30) | 30×30×30 |
| LVL_Z3_COLL_08_IceFlowerMarble | CirclePlatform | (0.03,0.03,0.03) | (默认尺寸未知) |
| LVL_Z2_PROP_Counter | Cube | (2.00,0.60,0.90) | 200×60×90 |
| LVL_Z2_PROP_ShelfEast_01 | Fence_Small_400 | (1.00,1.00,1.80) | 400×5×180 |
| LVL_Z2_PROP_ShelfEast_02 | Fence_Small_400 | (1.00,1.00,1.80) | 400×5×180 |
| LVL_Z2_PROP_ShelfSouth | Fence_Small_400 | (1.00,1.00,1.80) | 400×5×180 |
| LVL_Z2_PROP_Freezer | Container_Normal | (0.25,0.15,0.40) | (默认尺寸未知) |
| LVL_Z2_PROP_SmallTV | Monitor_01 | (0.60,0.60,0.60) | (默认尺寸未知) |
| LVL_Z2_PROP_Signboard | Cube | (3.00,0.05,0.40) | 300×5×40 |
| LVL_Z2_PROP_CardRack | Cube | (0.40,0.20,0.30) | 40×20×30 |
| LVL_Z2_INTR_05_ShopOwner | Cube | (0.30,0.30,0.30) | 30×30×30 |
| LVL_Z2_INTR_07_SnackShelf | Cube | (0.30,0.30,0.30) | 30×30×30 |
| LVL_Z2_COLL_09_BouncyBall | CirclePlatform | (0.03,0.03,0.03) | (默认尺寸未知) |
| LVL_Z4_PROP_BigTree | Tree_01 | (1.20,1.20,1.20) | (默认尺寸未知) |
| LVL_Z4_PROP_Puddle | Cube | (1.20,0.80,0.01) | 120×80×1 |
| LVL_Z4_PROP_MarbleCircle | CirclePlatform | (0.16,0.16,0.01) | (默认尺寸未知) |
| LVL_Z4_INTR_03_BlueMarble | Cube | (0.30,0.30,0.30) | 30×30×30 |
| LVL_Z4_INTR_08_SmallStone | Cube | (0.30,0.30,0.30) | 30×30×30 |
| LVL_Z4_COLL_01_BlueMarble | CirclePlatform | (0.03,0.03,0.03) | (默认尺寸未知) |
| LVL_Z4_COLL_03_SmallStone | CirclePlatform | (0.03,0.03,0.03) | (默认尺寸未知) |
| LVL_Z4_COLL_04_LuZhishenCard | CirclePlatform | (0.03,0.03,0.03) | (默认尺寸未知) |
| LVL_Z5_PROP_SpiderWeb | Cube | (0.30,0.30,0.01) | 30×30×1 |
| LVL_Z5_PROP_FlowerPot | Cylinder | (0.20,0.20,0.25) | 20×20×25 |
| LVL_Z5_PROP_Moss | Cube | (0.60,0.40,0.01) | 60×40×1 |
| LVL_Z5_INTR_10_WasteBox | Cube | (0.30,0.30,0.30) | 30×30×30 |
| LVL_Z5_COLL_02_RedMarble | CirclePlatform | (0.03,0.03,0.03) | (默认尺寸未知) |
| LVL_Z6_PROP_Reeds_S1~5 | Cylinder | (0.08,0.08,2.00) | 8×8×200 |
| LVL_Z6_PROP_Reeds_S2_1~5 | Cylinder | (0.08,0.08,2.00) | 8×8×200 |
| LVL_Z6_PROP_Reeds_E_1~3 | Cylinder | (0.08,0.08,1.60) | 8×8×160 |
| LVL_Z6_PROP_LilyPad_1~4 | CirclePlatform | (0.06,0.06,0.01) | (默认尺寸未知) |
| LVL_Z6_PROP_Frog | Cube | (0.05,0.05,0.04) | 5×5×4 |
| LVL_Z6_COLL_05_CandyWrapper | CirclePlatform | (0.03,0.03,0.03) | (默认尺寸未知) |
| LVL_Z6_COLL_06_Eraser | CirclePlatform | (0.03,0.03,0.03) | (默认尺寸未知) |
| LVL_Z7_STR_CollisionSide | Wall_400x300 | (0.00,90.00,0.00) | 0×1800×0 |
| LVL_Z7_STR_CollisionSide2 | Wall_400x300 | (0.00,90.00,0.00) | 0×1800×0 |
@@ -0,0 +1,772 @@
"""搭建指南 Markdown 解析器。
解析 docs/technical/关卡-v01-搭建指南.md,输出统一的 Actor 列表。
搭建指南的表格格式(4 种变体 + 特殊表格):
- 结构体表: # / Actor名称 / 资产 / 位置 / [旋转] / [缩放] / 材质
- 布景表: # / Actor名称 / 资产 / 位置 / [缩放] / 材质 / 说明
- 交互物表: # / Actor名称 / 位置 / 触发半径 / [交互类型] / 引用
- 收集品表: # / Actor名称 / 位置 / 稀有度 / 引用
- 边界表: # / Actor名称 / 资产 / 位置 / [旋转] / [缩放] / 材质
- 光照表: # / Actor名称 / 类型 / 位置 / 参数
"""
import json
import re
import logging
from pathlib import Path
from dataclasses import dataclass, field, asdict
from typing import Optional
from parsers.markdown_table import (
parse_table,
parse_location,
parse_rotation,
parse_scale,
clean_cell,
)
logger = logging.getLogger("level_builder.parser")
# ============================================================
# 数据结构
# ============================================================
@dataclass
class ActorData:
"""单个 Actor 的完整规格。"""
actor_name: str
asset_key: str = ""
asset_type: str = "static_mesh" # static_mesh | player_start | directional_light | sky_light | point_light
category: str = "" # STR | PROP | INTR | COLL | BND | LGT | PLAY
zone_id: str = "" # Z1-Z7 | GLOBAL
location: tuple = (0.0, 0.0, 0.0)
rotation: tuple = (0.0, 0.0, 0.0)
scale: tuple = (1.0, 1.0, 1.0)
material: str = ""
folder: str = ""
note: str = ""
trigger_radius: float = 0.0
interact_type: str = ""
rarity: str = ""
source_ref: str = ""
# ============================================================
# 标题/上下文检测
# ============================================================
# 区域标题匹配:### C1. Z1 — 家(Home)
ZONE_HEADING = re.compile(
r'###\s+C\d+\.\s+(Z\d+)\s*[—\-–]\s*(.+?)(?:((.+)))?$'
)
# 分类子标题匹配:#### C1.1 结构体(Structure)
CATEGORY_HEADING = re.compile(
r'####\s+C\d+\.\d+\s+(结构体|布景|交互物|收集品|布景物)'
)
# Phase 标题
PHASE_HEADING = re.compile(r'^##\s+(二|三|四|五|六|七|八)')
# 边界区域标题
BOUNDARY_HEADING = re.compile(r'####\s+(西边界|东边界|北边界|南边界)')
# 全局物件标题
GLOBAL_HEADING = re.compile(r'###\s+G-(\d+|E\d+)\s+(.+)')
# 光照标题
LIGHT_HEADING = re.compile(r'###\s+D\d+\s+(.+)')
# 地面子标题
FLOOR_HEADING = re.compile(r'####\s+Z\d+\s+(.+?)\s+地面')
CATEGORY_MAP = {
"结构体": "STR",
"布景": "PROP",
"布景物": "PROP",
"交互物": "INTR",
"交互物标记": "INTR",
"收集品": "COLL",
"收集品标记": "COLL",
}
ZONE_ID_MAP = {
"Z1": "Z1", "Z2": "Z2", "Z3": "Z3",
"Z4": "Z4", "Z5": "Z5", "Z6": "Z6", "Z7": "Z7",
}
# ============================================================
# 核心解析逻辑
# ============================================================
class BuildGuideParser:
"""搭建指南解析器。"""
def __init__(self):
self.actors: list[ActorData] = []
self.current_zone = ""
self.current_category = ""
self.current_phase = ""
self.metadata = {}
def parse_file(self, filepath: str) -> dict:
"""解析搭建指南文件,返回完整的关卡数据。"""
path = Path(filepath)
if not path.exists():
raise FileNotFoundError(f"搭建指南文件不存在: {filepath}")
text = path.read_text(encoding="utf-8")
return self.parse_text(text)
def parse_text(self, text: str) -> dict:
"""解析搭建指南文本。"""
lines = text.split("\n")
# 解析全局参数(第〇章)
self._parse_global_info(lines)
# 解析资产映射表(第一章)— 可选,用于验证
# 逐 Phase 解析
self._parse_phase_ground(lines)
self._parse_phase_boundaries(lines)
self._parse_phase_zones(lines)
self._parse_phase_global_objects(lines)
self._parse_phase_lighting(lines)
# 添加 Player Start
self._add_player_start()
# 构建输出
result = {
"metadata": self.metadata,
"actors": [asdict(a) for a in self.actors],
"summary": self._build_summary(),
}
return result
# ----------------------------------------------------------
# 全局参数
# ----------------------------------------------------------
def _parse_global_info(self, lines: list[str]):
"""解析场景全局参数。"""
self.metadata = {
"source": "关卡-v01-搭建指南.md",
"scene_size": [8000, 6000],
"ground_z": 0,
"character_height": 120,
"camera_height": 100,
}
# ----------------------------------------------------------
# Phase B — 地面
# ----------------------------------------------------------
def _parse_phase_ground(self, lines: list[str]):
"""解析 Phase B 地面铺设表格。"""
text = "\n".join(lines)
# 找到 Phase B 区域
b_start = self._find_section_start(lines, "Phase B")
b_end = self._find_section_start(lines, "Phase C")
if b_start < 0:
return
if b_end < 0:
b_end = len(lines)
section_text = "\n".join(lines[b_start:b_end])
# 地面子区域标题:#### Z1 家 地面(MI_DevGray)
floor_pattern = re.compile(
r'####\s+Z(\d+)\s+\S+\s+地面(?:((.+?)))?'
)
current_zone = ""
current_material = ""
section_lines = lines[b_start:b_end]
i = 0
while i < len(section_lines):
line = section_lines[i].strip()
# 检测地面子区域
fm = floor_pattern.match(line)
if fm:
current_zone = f"Z{fm.group(1)}"
current_material = fm.group(2) or ""
i += 1
continue
# 检测水面区域
if '水面' in line and current_zone:
current_material = "MI_DevSkyBlue"
i += 1
continue
# 检测表格
if '|' in line and '#' in line:
table_lines = []
while i < len(section_lines) and '|' in section_lines[i]:
table_lines.append(section_lines[i])
i += 1
rows = parse_table(table_lines)
if rows:
for row in rows:
actor = self._parse_floor_row(row, current_zone, current_material)
if actor:
self.actors.append(actor)
continue
i += 1
def _parse_floor_row(self, row: dict, zone: str, default_material: str) -> Optional[ActorData]:
"""解析地面表格行。"""
name = clean_cell(row.get("Actor 名称", row.get("Actor名称", "")))
if not name:
return None
asset_key = clean_cell(row.get("资产", ""))
location = parse_location(row.get("位置 (X,Y,Z)", row.get("位置", "")))
scale = parse_scale(row.get("缩放", "—"))
material = clean_cell(row.get("材质", ""))
if not material:
material = default_material
return ActorData(
actor_name=name,
asset_key=asset_key,
asset_type="static_mesh",
category="STR",
zone_id=zone,
location=location,
scale=scale,
material=material,
folder=self._get_folder(zone, "STR"),
)
# ----------------------------------------------------------
# Phase B — 边界
# ----------------------------------------------------------
def _parse_phase_boundaries(self, lines: list[str]):
"""解析 Phase B 边界围挡。"""
b_start = self._find_section_start(lines, "放置边界围挡")
b_end = self._find_section_start(lines, "Phase C")
if b_start < 0:
return
if b_end < 0:
b_end = len(lines)
section_lines = lines[b_start:b_end]
i = 0
while i < len(section_lines):
line = section_lines[i].strip()
# 边界子区域标题
bm = BOUNDARY_HEADING.match(line)
# 检测表格
if '|' in line:
table_lines = []
while i < len(section_lines) and '|' in section_lines[i]:
table_lines.append(section_lines[i])
i += 1
rows = parse_table(table_lines)
if rows:
for row in rows:
actors = self._parse_boundary_row(row)
self.actors.extend(actors)
continue
i += 1
def _parse_boundary_row(self, row: dict) -> list[ActorData]:
"""解析边界表格行,处理范围格式如 '1-15 | LVL_BND_East_01~15'。"""
name_col = row.get("Actor 名称", row.get("Actor名称", ""))
name = clean_cell(name_col)
# 处理范围格式:名称中含 ~ 或编号含 -
if '~' in name:
return self._expand_range_row(row, name)
asset_key = clean_cell(row.get("资产", ""))
location = parse_location(row.get("位置 (X,Y,Z)", row.get("位置", "")))
rotation = parse_rotation(row.get("旋转 (Pitch,Yaw,Roll)", row.get("旋转", "(0,0,0)")))
scale = parse_scale(row.get("缩放", "—"))
material = clean_cell(row.get("材质", ""))
actor = ActorData(
actor_name=name,
asset_key=asset_key,
category="BND",
zone_id="GLOBAL",
location=location,
rotation=rotation,
scale=scale,
material=material,
folder=self._get_folder("GLOBAL", "BND"),
)
return [actor]
def _expand_range_row(self, row: dict, name: str) -> list[ActorData]:
"""展开范围格式行。如 'LVL_BND_East_01~15' → 15 个 Actor。"""
asset_key = clean_cell(row.get("资产", ""))
rotation = parse_rotation(row.get("旋转 (Pitch,Yaw,Roll)", row.get("旋转", "(0,0,0)")))
scale = parse_scale(row.get("缩放", "—"))
material = clean_cell(row.get("材质", ""))
# 解析名称范围:LVL_BND_East_01~15
m = re.match(r'(.+?)_(\d+)~(\d+)', name)
if not m:
return []
prefix = m.group(1)
start = int(m.group(2))
end = int(m.group(3))
# 解析位置表达式 — 格式: (X, Y_expr, Z) n=start~end
# 例: (8000, 200+400n, 0) n=0~14
loc_col = row.get("位置 (X,Y,Z)", row.get("位置", ""))
loc_col = clean_cell(loc_col)
# 提取 n 的范围
n_match = re.search(r'n\s*=\s*(\d+)~(\d+)', loc_col)
n_start = int(n_match.group(1)) if n_match else 0
n_end = int(n_match.group(2)) if n_match else (end - start)
# 去掉 n=xx~yy 后缀以便解析坐标
loc_clean = re.sub(r'\s*n\s*=\s*\d+~\d+\s*', '', loc_col).strip()
# 提取三个坐标表达式
coord_match = re.match(r'\((.+?)\s*,\s*(.+?)\s*,\s*(.+?)\)', loc_clean)
if not coord_match:
return []
x_expr = coord_match.group(1).strip()
y_expr = coord_match.group(2).strip()
z_expr = coord_match.group(3).strip()
actors = []
for idx, n in enumerate(range(n_start, n_end + 1)):
x = self._eval_coord_expr(x_expr, n)
y = self._eval_coord_expr(y_expr, n)
z = self._eval_coord_expr(z_expr, n)
actor_num = start + idx
actor_name = f"{prefix}_{actor_num:02d}"
actor = ActorData(
actor_name=actor_name,
asset_key=asset_key,
category="BND",
zone_id="GLOBAL",
location=(x, y, z),
rotation=rotation,
scale=scale,
material=material,
folder=self._get_folder("GLOBAL", "BND"),
)
actors.append(actor)
return actors
def _eval_coord_expr(self, expr: str, n: int) -> float:
"""计算坐标表达式。如 '200+400n' → 200+400*n。"""
expr = expr.strip()
# 简单情况:纯数字
try:
return float(expr)
except ValueError:
pass
# 含 n 的表达式:200+400n
expr = expr.replace('n', f'*{n}')
# 处理连续运算符:*+→+, *-→-
expr = expr.replace('*+', '+').replace('*-', '-')
try:
# 安全计算简单算术
result = eval(expr) # 受限输入,仅数字和基本运算
return float(result)
except Exception:
return 0.0
# ----------------------------------------------------------
# Phase C — 逐区域搭建
# ----------------------------------------------------------
def _parse_phase_zones(self, lines: list[str]):
"""解析 Phase C 逐区域搭建。"""
c_start = self._find_section_start(lines, "逐区域搭建")
c_end = self._find_section_start(lines, "Phase C — 全局物件")
if c_start < 0:
c_start = self._find_section_start(lines, "全局物件")
if c_start < 0:
return
if c_end < 0:
c_end = len(lines)
section_lines = lines[c_start:c_end]
current_zone = ""
current_category = ""
i = 0
while i < len(section_lines):
line = section_lines[i].strip()
# 检测区域标题
zm = ZONE_HEADING.match(line)
if zm:
current_zone = zm.group(1)
i += 1
continue
# 检测分类标题
cm = CATEGORY_HEADING.match(line)
if cm:
cat_name = cm.group(1)
current_category = CATEGORY_MAP.get(cat_name, "")
i += 1
continue
# 检测结构体子标题(如 "C1.1 结构体(Structure)")
sub_match = re.match(r'####\s+C\d+\.\d+\s+(结构体|布景|布景物|交互物|交互物标记|收集品|收集品标记)', line)
if sub_match:
cat_name = sub_match.group(1)
current_category = CATEGORY_MAP.get(cat_name, "")
i += 1
continue
# 处理表格
if '|' in line:
table_lines = []
while i < len(section_lines) and '|' in section_lines[i]:
table_lines.append(section_lines[i])
i += 1
rows = parse_table(table_lines)
if rows:
for row in rows:
actor = self._parse_zone_row(row, current_zone, current_category)
if actor:
self.actors.append(actor)
continue
i += 1
def _parse_zone_row(self, row: dict, zone: str, category: str) -> Optional[ActorData]:
"""解析区域内的 Actor 行。根据列内容判断表格类型。"""
name = clean_cell(row.get("Actor 名称", row.get("Actor名称", "")))
if not name:
return None
# 判断表格类型:有"资产"列 → 结构体/布景;有"触发半径" → 交互物;有"稀有度" → 收集品
has_asset = "资产" in row and clean_cell(row.get("资产", ""))
has_trigger = "触发半径" in row or "触发半径 (cm)" in row
has_rarity = "稀有度" in row
if has_trigger and not has_asset:
# 交互物表
return self._parse_interactable_row(row, zone)
elif has_rarity and not has_asset:
# 收集品表
return self._parse_collectible_row(row, zone)
else:
# 结构体/布景表
return self._parse_structure_or_prop_row(row, zone, category)
def _parse_structure_or_prop_row(self, row: dict, zone: str, category: str) -> Optional[ActorData]:
"""解析结构体或布景表格行。"""
name = clean_cell(row.get("Actor 名称", row.get("Actor名称", "")))
asset_key = clean_cell(row.get("资产", ""))
location = parse_location(
row.get("位置 (X,Y,Z)", row.get("位置", ""))
)
# 旋转:结构体表可能有,布景表可能没有
rot_col = row.get("旋转 (Pitch,Yaw,Roll)", row.get("旋转", ""))
rotation = parse_rotation(rot_col) if rot_col else (0.0, 0.0, 0.0)
# 缩放
scale = parse_scale(row.get("缩放", "—"))
# 材质
material = clean_cell(row.get("材质", ""))
if material in ('—', '–', '-'):
material = ""
# 说明
note = clean_cell(row.get("说明", ""))
if not category:
category = "STR"
return ActorData(
actor_name=name,
asset_key=asset_key,
category=category,
zone_id=zone,
location=location,
rotation=rotation,
scale=scale,
material=material,
folder=self._get_folder(zone, category),
note=note,
)
def _parse_interactable_row(self, row: dict, zone: str) -> ActorData:
"""解析交互物行。用占位 Cube 表示。"""
name = clean_cell(row.get("Actor 名称", row.get("Actor名称", "")))
location = parse_location(
row.get("位置 (X,Y,Z)", row.get("位置", ""))
)
trigger = row.get("触发半径", row.get("触发半径 (cm)", "150"))
try:
trigger_radius = float(trigger)
except (ValueError, TypeError):
trigger_radius = 150.0
ref = clean_cell(row.get("引用", ""))
interact_type = clean_cell(row.get("交互类型", ""))
return ActorData(
actor_name=name,
asset_key="Cube",
category="INTR",
zone_id=zone,
location=location,
scale=(0.3, 0.3, 0.3),
material="MI_DevYellow",
folder=self._get_folder(zone, "INTR"),
trigger_radius=trigger_radius,
interact_type=interact_type,
source_ref=ref,
note="交互物占位标记",
)
def _parse_collectible_row(self, row: dict, zone: str) -> ActorData:
"""解析收集品行。用占位 Sphere/Cube 表示。"""
name = clean_cell(row.get("Actor 名称", row.get("Actor名称", "")))
location = parse_location(
row.get("位置 (X,Y,Z)", row.get("位置", ""))
)
rarity = clean_cell(row.get("稀有度", "普通"))
ref = clean_cell(row.get("引用", ""))
# 稀有用金色,普通用白色
material = "MI_DevYellow" if rarity == "稀有" else "MI_DevWhite"
return ActorData(
actor_name=name,
asset_key="CirclePlatform",
category="COLL",
zone_id=zone,
location=location,
scale=(0.03, 0.03, 0.03),
material=material,
folder=self._get_folder(zone, "COLL"),
rarity=rarity,
source_ref=ref,
note="收集品占位标记",
)
# ----------------------------------------------------------
# Phase C — 全局物件
# ----------------------------------------------------------
def _parse_phase_global_objects(self, lines: list[str]):
"""解析全局物件(路灯、垃圾桶等)。"""
g_start = self._find_section_start(lines, "全局物件")
g_end = self._find_section_start(lines, "Phase D")
if g_start < 0:
return
if g_end < 0:
g_end = len(lines)
section_lines = lines[g_start:g_end]
i = 0
while i < len(section_lines):
line = section_lines[i].strip()
if '|' in line:
table_lines = []
while i < len(section_lines) and '|' in section_lines[i]:
table_lines.append(section_lines[i])
i += 1
rows = parse_table(table_lines)
if rows:
for row in rows:
actor = self._parse_global_row(row)
if actor:
self.actors.append(actor)
continue
i += 1
def _parse_global_row(self, row: dict) -> Optional[ActorData]:
"""解析全局物件行。"""
name = clean_cell(row.get("Actor 名称", row.get("Actor名称", "")))
if not name:
return None
asset_key = clean_cell(row.get("资产", ""))
location = parse_location(row.get("位置 (X,Y,Z)", row.get("位置", "")))
material = clean_cell(row.get("材质", ""))
if material in ('—', '–', '-'):
material = ""
# 从 Actor 名称推断类别
category = "PROP"
if "_LGT_" in name:
category = "LGT"
elif "_PROP_" in name:
category = "PROP"
zone_id = self._infer_zone_from_name(name)
return ActorData(
actor_name=name,
asset_key=asset_key,
category=category,
zone_id=zone_id,
location=location,
material=material,
folder=self._get_folder(zone_id, category),
)
# ----------------------------------------------------------
# Phase D — 光照
# ----------------------------------------------------------
def _parse_phase_lighting(self, lines: list[str]):
"""解析光照配置。"""
d_start = self._find_section_start(lines, "Phase D")
d_end = self._find_section_start(lines, "Phase E")
if d_start < 0:
return
if d_end < 0:
d_end = len(lines)
section_lines = lines[d_start:d_end]
# 全局光源:DirectionalLight + SkyLight
self._parse_global_lights(section_lines)
# 路灯点光源
self._parse_street_lights(section_lines)
def _parse_global_lights(self, lines: list[str]):
"""解析全局光源表格。"""
text = "\n".join(lines)
# Directional Light 和 Sky Light
if "Directional Light" in text or "DirectionalLight" in text:
self.actors.append(ActorData(
actor_name="LVL_Global_DirectionalLight",
asset_key="",
asset_type="directional_light",
category="LGT",
zone_id="GLOBAL",
location=(0, 0, 500),
rotation=(-45, 315, 0),
folder=self._get_folder("GLOBAL", "LGT"),
note="色温~5500K暖白;从西南照射;强度中等",
))
if "Sky Light" in text or "SkyLight" in text:
self.actors.append(ActorData(
actor_name="LVL_Global_SkyLight",
asset_key="",
asset_type="sky_light",
category="LGT",
zone_id="GLOBAL",
location=(0, 0, 0),
folder=self._get_folder("GLOBAL", "LGT"),
note="强度低-中;环境补光",
))
def _parse_street_lights(self, lines: list[str]):
"""解析路灯点光源表格。"""
i = 0
while i < len(lines):
line = lines[i].strip()
# 检测表格起始(含 | 的行)
if '|' in line:
table_lines = []
while i < len(lines) and '|' in lines[i]:
table_lines.append(lines[i])
i += 1
# 检查表格内容是否包含 Point Light
table_text = "\n".join(table_lines)
if "Point" in table_text or "point_light" in table_text.lower():
rows = parse_table(table_lines)
for row in rows:
name = clean_cell(row.get("Actor 名称", row.get("Actor名称", "")))
loc_col = row.get("位置 (X,Y,Z)", row.get("位置", ""))
location = parse_location(loc_col)
if name:
self.actors.append(ActorData(
actor_name=name,
asset_key="",
asset_type="point_light",
category="LGT",
zone_id="Z3",
location=location,
folder=self._get_folder("GLOBAL", "LGT"),
note="暖黄色,低强度",
))
continue
i += 1
# ----------------------------------------------------------
# Player Start
# ----------------------------------------------------------
def _add_player_start(self):
"""添加 Player Start Actor。"""
self.actors.append(ActorData(
actor_name="LVL_PlayerStart",
asset_key="",
asset_type="player_start",
category="PLAY",
zone_id="GLOBAL",
location=(500, 720, 0),
rotation=(0, 0, 0),
folder=self._get_folder("GLOBAL", "PLAY"),
note="Z1 院内院门内侧约60cm,面向+Y(北)",
))
# ----------------------------------------------------------
# 辅助方法
# ----------------------------------------------------------
def _find_section_start(self, lines: list[str], keyword: str) -> int:
"""找到包含关键词的标题行。"""
for i, line in enumerate(lines):
if keyword in line:
return i
return -1
def _infer_zone_from_name(self, name: str) -> str:
"""从 Actor 名称推断区域 ID。"""
m = re.match(r'LVL_(Z\d+)_', name)
if m:
return m.group(1)
if 'BND' in name:
return "GLOBAL"
if 'Global' in name:
return "GLOBAL"
return "GLOBAL"
def _get_folder(self, zone_id: str, category: str) -> str:
"""获取 Outliner 文件夹路径。"""
from config.zone_config import get_folder_path
return get_folder_path(zone_id, category)
def _build_summary(self) -> dict:
"""构建解析摘要。"""
total = len(self.actors)
by_category = {}
by_zone = {}
asset_keys = set()
for a in self.actors:
by_category[a.category] = by_category.get(a.category, 0) + 1
by_zone[a.zone_id] = by_zone.get(a.zone_id, 0) + 1
if a.asset_key:
asset_keys.add(a.asset_key)
return {
"total_actors": total,
"by_category": by_category,
"by_zone": by_zone,
"unique_assets": len(asset_keys),
"asset_list": sorted(asset_keys),
}
@@ -0,0 +1,186 @@
"""通用 Markdown 管道表格解析器。
将 Markdown 表格文本解析为 list[dict],每个字典代表一行数据。
支持中文列名,处理列数不一致的情况。
"""
import re
from typing import Optional
def parse_table(lines: list[str]) -> list[dict]:
"""解析单个 Markdown 表格。
Args:
lines: 表格的文本行列表(包含表头、分隔行和数据行)。
Returns:
列表,每个元素是一行数据的字典,key 为表头文本。
返回空列表如果输入不是有效表格。
"""
if not lines:
return []
# 找到分隔行(包含 |---| 的行)
sep_idx = -1
for i, line in enumerate(lines):
if re.match(r'\s*\|[\s\-:]+\|', line):
sep_idx = i
break
if sep_idx < 1:
return [] # 没有有效的表头+分隔行
# 提取表头
header_line = lines[sep_idx - 1]
headers = _split_row(header_line)
if not headers:
return []
# 提取数据行(分隔行之后的所有 | 开头的行)
data_rows = []
for line in lines[sep_idx + 1:]:
stripped = line.strip()
if not stripped or not stripped.startswith('|'):
break # 表格结束
cells = _split_row(stripped)
data_rows.append(cells)
# 构建结果
result = []
for row in data_rows:
entry = {}
for j, header in enumerate(headers):
entry[header] = row[j] if j < len(row) else ""
result.append(entry)
return result
def parse_all_tables(text: str) -> list[tuple[list[str], list[dict]]]:
"""解析文本中的所有 Markdown 表格。
Args:
text: 完整的 Markdown 文本。
Returns:
列表,每个元素为 (header, rows) 元组。
header 为表头列表,rows 为 list[dict]。
"""
results = []
lines = text.split('\n')
i = 0
while i < len(lines):
# 寻找潜在的表格起始行(包含 | 的行)
if '|' in lines[i]:
# 收集连续的表格行
table_lines = []
while i < len(lines) and '|' in lines[i]:
table_lines.append(lines[i].strip())
i += 1
# 尝试解析
rows = parse_table(table_lines)
if rows:
# 提取表头
for tl in table_lines:
if re.match(r'\s*\|[\s\-:]+\|', tl):
sep_idx = table_lines.index(tl)
if sep_idx > 0:
headers = _split_row(table_lines[sep_idx - 1])
results.append((headers, rows))
break
continue
i += 1
return results
def find_tables_in_section(text: str, section_start: int, section_end: int) -> list[list[dict]]:
"""在文本的指定范围内找到所有表格。
Args:
text: 完整文本。
section_start: 区域起始行号。
section_end: 区域结束行号。
Returns:
表格列表,每个表格是 list[dict]。
"""
lines = text.split('\n')
section_lines = lines[section_start:section_end]
tables = []
i = 0
while i < len(section_lines):
if '|' in section_lines[i]:
# 收集连续表格行
table_lines = []
while i < len(section_lines) and '|' in section_lines[i]:
table_lines.append(section_lines[i])
i += 1
rows = parse_table(table_lines)
if rows:
tables.append(rows)
continue
i += 1
return tables
def _split_row(line: str) -> list[str]:
"""将 | 分隔的行拆分为单元格列表,去除首尾空格。"""
# 去除首尾空白
line = line.strip()
# 按管道符分割
if not line.startswith('|'):
return []
if not line.endswith('|'):
# 有些行末尾可能没有 |
parts = line.split('|')
else:
parts = line.split('|')
# 去除首尾空元素(管道符分割产生的)
cells = [c.strip() for c in parts]
# 去掉首尾空字符串
while cells and cells[0] == '':
cells.pop(0)
while cells and cells[-1] == '':
cells.pop()
return cells
def parse_location(text: str) -> tuple[float, float, float]:
"""解析位置字符串 '(X,Y,Z)' 为三元组。"""
if not text:
return (0.0, 0.0, 0.0)
m = re.search(r'\(([-\d.]+)\s*[,,]\s*([-\d.]+)\s*[,,]\s*([-\d.]+)\)', text)
if m:
return (float(m.group(1)), float(m.group(2)), float(m.group(3)))
return (0.0, 0.0, 0.0)
def parse_rotation(text: str) -> tuple[float, float, float]:
"""解析旋转字符串 '(P,Y,R)' 为三元组(度数)。"""
return parse_location(text)
def parse_scale(text: str) -> tuple[float, float, float]:
"""解析缩放字符串 '(X,Y,Z)' 为三元组。'—' 或空返回 (1,1,1)。"""
if not text or text.strip() in ('—', '–', '-', '无', ''):
return (1.0, 1.0, 1.0)
return parse_location(text)
def clean_cell(text: str) -> str:
"""清理单元格文本:去首尾空白、去反引号。"""
if not text:
return ""
text = text.strip()
# 去除 Markdown 反引号包裹
if text.startswith('`') and text.endswith('`'):
text = text[1:-1]
return text.strip()
@@ -0,0 +1,159 @@
"""搭建报告生成器。
从解析数据和构建日志生成 Markdown 格式的搭建报告,
格式与 docs/technical/关卡-v01-搭建报告.md 一致。
"""
import json
from pathlib import Path
from datetime import datetime
from typing import Optional
def generate_report(
level_data: dict,
build_log: Optional[list[dict]] = None,
output_path: str = "",
) -> str:
"""生成搭建报告 Markdown。
Args:
level_data: 解析器输出的关卡数据(含 metadata/actors/summary)。
build_log: 构建日志(含每个 Actor 的成功/失败状态)。可选。
output_path: 报告输出路径。为空则只返回字符串。
Returns:
Markdown 格式的搭建报告文本。
"""
actors = level_data.get("actors", [])
summary = level_data.get("summary", {})
metadata = level_data.get("metadata", {})
lines = []
lines.append("# 白模搭建报告 — 自动生成")
lines.append("")
lines.append(f"> 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
lines.append(f"> 数据来源: {metadata.get('source', '未知')}")
lines.append(f"> 场景尺寸: {metadata.get('scene_size', ['?','?'])[0]}cm × {metadata.get('scene_size', ['?','?'])[1]}cm")
lines.append("")
# ---- 搭建概要 ----
lines.append("## 一、搭建概要")
lines.append("")
total = summary.get("total_actors", len(actors))
by_category = summary.get("by_category", {})
lines.append(f"- **总 Actor 数**: {total}")
for cat in ["STR", "PROP", "INTR", "COLL", "BND", "LGT", "PLAY"]:
count = by_category.get(cat, 0)
if count > 0:
cat_name = {"STR": "结构体", "PROP": "布景", "INTR": "交互物",
"COLL": "收集品", "BND": "边界", "LGT": "光源", "PLAY": "PlayerStart"}
lines.append(f"- {cat_name.get(cat, cat)} ({cat}): {count}")
if build_log:
ok = sum(1 for b in build_log if b.get("status") == "ok")
failed = sum(1 for b in build_log if b.get("status") != "ok")
lines.append(f"- 构建成功: {ok}")
lines.append(f"- 构建失败: {failed}")
lines.append("")
# ---- 逐区域对照 ----
lines.append("## 二、逐区域对照")
lines.append("")
by_zone = summary.get("by_zone", {})
zone_names = {
"Z1": "家", "Z2": "小卖部", "Z3": "主街道",
"Z4": "空地", "Z5": "巷子", "Z6": "水塘边", "Z7": "老居民楼",
"GLOBAL": "全局",
}
for zone_id in ["Z1", "Z2", "Z3", "Z4", "Z5", "Z6", "Z7", "GLOBAL"]:
zone_actors = [a for a in actors if a.get("zone_id") == zone_id]
if not zone_actors:
continue
zone_name = zone_names.get(zone_id, zone_id)
lines.append(f"### {zone_id} {zone_name}")
lines.append("")
lines.append(f"共 {len(zone_actors)} 个 Actor")
lines.append("")
lines.append("| # | Actor 名称 | 资产 | 位置 | 缩放 | 材质 | 状态 |")
lines.append("|---|-----------|------|------|------|------|------|")
for j, a in enumerate(zone_actors):
name = a.get("actor_name", "")
asset = a.get("asset_key", "—")
loc = a.get("location", [0, 0, 0])
loc_str = f"({loc[0]:.0f},{loc[1]:.0f},{loc[2]:.0f})"
scale = a.get("scale", [1, 1, 1])
if scale == [1, 1, 1] or scale == (1, 1, 1):
scale_str = "—"
else:
scale_str = f"({scale[0]:.2f},{scale[1]:.2f},{scale[2]:.2f})"
mat = a.get("material", "—") or "—"
# 状态
status = "✅"
if build_log:
log_entry = next((b for b in build_log if b.get("name") == name), None)
if log_entry:
status = "✅" if log_entry.get("status") == "ok" else f"❌ {log_entry.get('reason', '')}"
lines.append(f"| {j+1} | {name} | {asset} | {loc_str} | {scale_str} | {mat} | {status} |")
lines.append("")
# ---- 资产使用统计 ----
lines.append("## 三、资产使用统计")
lines.append("")
asset_usage = {}
for a in actors:
key = a.get("asset_key", "")
if key:
asset_usage[key] = asset_usage.get(key, 0) + 1
lines.append("| 资产名 | 使用次数 |")
lines.append("|--------|---------|")
for key in sorted(asset_usage.keys(), key=lambda k: -asset_usage[k]):
lines.append(f"| {key} | {asset_usage[key]} |")
lines.append("")
# ---- 缩放记录 ----
lines.append("## 四、缩放记录")
lines.append("")
lines.append("| Actor 名称 | 资产 | 缩放 | 实际尺寸 (cm) |")
lines.append("|-----------|------|------|-------------|")
for a in actors:
scale = a.get("scale", [1, 1, 1])
if scale != [1, 1, 1] and scale != (1, 1, 1):
name = a.get("actor_name", "")
asset_key = a.get("asset_key", "")
# 计算实际尺寸
from config.asset_mapping import MESH_ASSETS
default_size = MESH_ASSETS.get(asset_key, {}).get("default_size", (100, 100, 100))
if default_size:
actual = (
f"{default_size[0]*scale[0]:.0f}×"
f"{default_size[1]*scale[1]:.0f}×"
f"{default_size[2]*scale[2]:.0f}"
)
else:
actual = "(默认尺寸未知)"
lines.append(f"| {name} | {asset_key} | ({scale[0]:.2f},{scale[1]:.2f},{scale[2]:.2f}) | {actual} |")
lines.append("")
report = "\n".join(lines)
# 写入文件
if output_path:
Path(output_path).write_text(report, encoding="utf-8")
print(f"报告已写入: {output_path}")
return report
@@ -0,0 +1,103 @@
"""离线资产验证模块。
检查解析数据中的资产引用是否有效:
1. asset_key 是否在 asset_mapping 中定义
2. 材质名是否在 material_mapping 中定义
3. Content/ 目录下 .uasset 文件是否存在(可选)
"""
import logging
from pathlib import Path
from dataclasses import dataclass
from config.asset_mapping import MESH_ASSETS
from config.material_mapping import MATERIAL_PATHS
logger = logging.getLogger("level_builder.validator")
@dataclass
class ValidationResult:
"""验证结果。"""
total_actors: int = 0
valid_actors: int = 0
warnings: list[str] = None
errors: list[str] = None
missing_assets: set[str] = None
missing_materials: set[str] = None
def __post_init__(self):
if self.warnings is None:
self.warnings = []
if self.errors is None:
self.errors = []
if self.missing_assets is None:
self.missing_assets = set()
if self.missing_materials is None:
self.missing_materials = set()
@property
def is_valid(self) -> bool:
return len(self.errors) == 0
def validate_actors(actors: list[dict], project_root: str = "") -> ValidationResult:
"""验证 Actor 列表中的资产引用。
Args:
actors: Actor 数据列表(来自解析器的 JSON 输出)。
project_root: 项目根目录路径(用于文件存在性检查)。
Returns:
ValidationResult 包含所有验证信息。
"""
result = ValidationResult(total_actors=len(actors))
for i, actor in enumerate(actors):
name = actor.get("actor_name", f"<unnamed #{i}>")
asset_key = actor.get("asset_key", "")
material = actor.get("material", "")
asset_type = actor.get("asset_type", "static_mesh")
# 跳过非 mesh 类型(光照、PlayerStart 等)
if asset_type != "static_mesh":
result.valid_actors += 1
continue
# 检查 asset_key
if not asset_key:
result.warnings.append(f"{name}: 无 asset_key(可能是光照/特殊 Actor)")
result.valid_actors += 1
continue
if asset_key not in MESH_ASSETS:
result.errors.append(f"{name}: 未知资产 '{asset_key}'")
result.missing_assets.add(asset_key)
continue
# 检查材质
if material and material not in MATERIAL_PATHS:
result.warnings.append(f"{name}: 未知材质 '{material}'")
result.missing_materials.add(material)
# 检查 .uasset 文件存在性(如果提供了项目根目录)
if project_root:
mesh_info = MESH_ASSETS[asset_key]
asset_path = mesh_info["path"]
# /Game/BlockingStarterPack/Meshes/.../SM_Xxx.SM_Xxx → Content/BlockingStarterPack/Meshes/.../SM_Xxx.uasset
fs_path = asset_path.replace("/Game/", "Content/").split(".")[0] + ".uasset"
full_path = Path(project_root) / fs_path
if not full_path.exists():
result.warnings.append(f"{name}: 资产文件不存在 '{fs_path}'")
result.valid_actors += 1
# 汇总日志
if result.missing_assets:
logger.error(f"缺失资产定义: {sorted(result.missing_assets)}")
if result.missing_materials:
logger.warning(f"缺失材质定义: {sorted(result.missing_materials)}")
logger.info(f"验证完成: {result.valid_actors}/{result.total_actors} 有效, "
f"{len(result.errors)} 错误, {len(result.warnings)} 警告")
return result