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>
This commit is contained in:
@@ -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)
|
||||
@@ -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`。
|
||||
|
||||
---
|
||||
|
||||
|
||||
+17
-18
@@ -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` — 视觉方向参考(集成美术时阅读)
|
||||
|
||||
---
|
||||
|
||||
|
||||
+10
-14
@@ -30,9 +30,6 @@ memory: project
|
||||
|
||||
## 2. 参考文档与项目管理工具
|
||||
|
||||
开始工作前,必须先阅读:
|
||||
- `.claude/refs/development-workflow.md` — 流水线定义、阶段划分、质量门标准(**首先阅读**)
|
||||
|
||||
### 项目管理工具
|
||||
|
||||
进度管理(任务列表、流水线状态、角色汇报)全部通过项目管理工具追踪。当前使用 **Gitea**(Issues + Milestones),具体 CLI 命令、认证方式和标签体系详见:
|
||||
@@ -40,9 +37,9 @@ memory: project
|
||||
> **`.claude/skills/gitea-collab.md`** — Gitea 协作工具完整参考
|
||||
|
||||
**保留在本地的文档**(不在项目管理工具中管理):
|
||||
- `docs/plans/prototype-roadmap.md` — 阶段路线图(信息量太大)
|
||||
- `docs/plans/reviews/<task-ID>-review.md` — 审查记录(详细结构化文档)
|
||||
- `docs/plans/work-log.md` — 工作日志
|
||||
- `docs/plans/原型阶段-路线图.md` — 阶段路线图(信息量太大)
|
||||
- `docs/plans/reviews/<task-ID>-审查记录.md` — 审查记录(详细结构化文档)
|
||||
- `docs/plans/工作日志.md` — 工作日志
|
||||
- 所有设计文档 (`docs/design/`)、技术文档 (`docs/technical/`)、美术文档 (`docs/art/`)
|
||||
|
||||
**重要规则**:你的记忆只是参考信息,不能替代实际状态验证。"记忆说文件 X 存在" ≠ "X 现在存在"——使用前必须验证。
|
||||
@@ -87,7 +84,7 @@ memory: project
|
||||
|
||||
### 审查后强制操作(无论谁执行审查)
|
||||
|
||||
- [ ] 在 `docs/plans/reviews/<task-ID>-review.md` 写了审查记录
|
||||
- [ ] 在 `docs/plans/reviews/<task-ID>-审查记录.md` 写了审查记录
|
||||
- [ ] 在对应 Issue 上发布了审查结论评论(通过/不通过 + 摘要)
|
||||
- [ ] 更新了 Issue 的 gate:* 标签(通过→pass,不通过→fail)
|
||||
- [ ] **通过时**:关闭 Issue 并附完成说明
|
||||
@@ -108,9 +105,8 @@ memory: project
|
||||
2. **获取活跃任务列表** → 所有进行中的任务
|
||||
3. **读取流水线追踪器状态** → 理解流水线位置
|
||||
4. **读取近期角色汇报** → 了解最新进展
|
||||
5. **检查本地审查记录**(如有):检查 `docs/plans/reviews/` 中的近期质量门审查
|
||||
6. **交叉核对**流水线检查清单,确认当前流水线位置和阻塞点
|
||||
7. **向用户报告**:当前进度、阻塞点、建议的下一步行动
|
||||
5. **交叉核对**流水线检查清单,确认当前流水线位置和阻塞点
|
||||
6. **向用户报告**:当前进度、阻塞点、建议的下一步行动
|
||||
|
||||
> 具体 CLI 命令参见 `.claude/skills/gitea-collab.md`
|
||||
|
||||
@@ -190,7 +186,7 @@ memory: project
|
||||
### 必须获得用户确认:
|
||||
- 关闭 Issue(完成任务)和创建新 Issue
|
||||
- 创建新里程碑或关闭现有里程碑
|
||||
- 修改 `roadmap.md` 或 `prototype-roadmap.md`
|
||||
- 修改 `路线图.md` 或 `原型阶段-路线图.md`
|
||||
- 质量门不通过后退回工作
|
||||
- 重大偏差处理(回退或开新版本)
|
||||
- 阶段转换
|
||||
@@ -234,8 +230,8 @@ memory: project
|
||||
|------|------|---------|
|
||||
| 任务追踪 | **项目管理工具 Issues**(唯一真实来源) | 任务状态变化 |
|
||||
| 流水线进度 | **Pipeline Tracker** | 流水线步骤变化 |
|
||||
| 阶段路线图 | `docs/plans/<stage>-roadmap.md` | 阶段开始、迭代调整 |
|
||||
| 审查记录 | `docs/plans/reviews/<task-ID>-review.md` | 每次质量门审查 |
|
||||
| 阶段路线图 | `docs/plans/<阶段>-路线图.md` | 阶段开始、迭代调整 |
|
||||
| 审查记录 | `docs/plans/reviews/<task-ID>-审查记录.md` | 每次质量门审查 |
|
||||
|
||||
> 审查记录格式参见 `.claude/skills/quality-gate-review.md`
|
||||
|
||||
@@ -338,5 +334,5 @@ memory: project
|
||||
# PM Agent 记忆索引
|
||||
|
||||
- [流水线阶段判断 2026-06-03](pipeline-status-20260603.md) — v0.1 处于阶段④执行案完善中,质量门未通过
|
||||
- [Roadmap 对齐问题](roadmap-alignment.md) — roadmap.md 缺少 v0.5-v0.7,需与 game-vision.md 对齐
|
||||
- [Roadmap 对齐问题](roadmap-alignment.md) — 路线图.md 缺少 v0.5-v0.7,需与游戏愿景.md 对齐
|
||||
- [设计文档质量审查](design-doc-review.md) — 交互系统技术越界、收集系统缺v0.1执行细节、边界情况不足
|
||||
|
||||
@@ -31,7 +31,6 @@ memory: project
|
||||
开始工作前,必须先阅读:
|
||||
- `.claude/skills/quality-gate-review.md` — **4 道质量门标准、阶段对应标准、审查流程**(核心技能)
|
||||
- `.claude/skills/gitea-collab.md` — Gitea CLI 命令和操作规范
|
||||
- `.claude/refs/development-workflow.md` — 流水线定义和阶段划分
|
||||
|
||||
---
|
||||
|
||||
@@ -56,7 +55,7 @@ memory: project
|
||||
### 步骤 4:执行强制操作
|
||||
**必须**完成以下所有操作(缺一不可):
|
||||
|
||||
1. ✅ 在 `docs/plans/reviews/<task-ID>-review.md` 写审查记录
|
||||
1. ✅ 在 `docs/plans/reviews/<task-ID>-审查记录.md` 写审查记录
|
||||
2. ✅ 在对应 Issue 上发布审查结论评论(通过/不通过 + 摘要)
|
||||
3. ✅ 更新 Issue 的 gate:* 标签
|
||||
4. ✅ **通过时**:关闭 Issue(附完成说明)
|
||||
@@ -72,7 +71,7 @@ node scripts/gitea.js --role pm issue comment <number> --body "## 质量门审
|
||||
|
||||
**质量门**:[执行案/程序设计/美术资产/程序实现]
|
||||
**审查日期**:YYYY-MM-DD
|
||||
**审查记录**:docs/plans/reviews/<task-ID>-review.md
|
||||
**审查记录**:docs/plans/reviews/<task-ID>-审查记录.md
|
||||
|
||||
### 结论
|
||||
[一句话总结]
|
||||
@@ -89,7 +88,7 @@ node scripts/gitea.js --role pm issue comment <number> --body "## 质量门审
|
||||
|
||||
**质量门**:[执行案/程序设计/美术资产/程序实现]
|
||||
**审查日期**:YYYY-MM-DD
|
||||
**审查记录**:docs/plans/reviews/<task-ID>-review.md
|
||||
**审查记录**:docs/plans/reviews/<task-ID>-审查记录.md
|
||||
|
||||
### 结论
|
||||
[一句话总结]
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
|
||||
#### 产出
|
||||
|
||||
**玩法草案文档**(`docs/design/concept.md`),包含以下 7 个字段:
|
||||
**玩法草案文档**(`docs/design/概念草案.md`),包含以下 7 个字段:
|
||||
|
||||
1. **一句话描述** — 用一句话说清楚这是什么游戏
|
||||
2. **玩家核心游玩内容与感受** — 玩家在游戏中的核心操作和活动,以及游戏希望带给玩家的情感基调、视觉风格和整体感受
|
||||
@@ -76,7 +76,7 @@
|
||||
6. **市场定位与三大卖点** — 游戏在市场中的定位(品类、平台、价位),以及最能吸引玩家的三个核心卖点
|
||||
7. **参考** — 灵感来源(其他游戏、电影、书、某种经历)
|
||||
|
||||
**美术风格指南**(`docs/art/style-guide.md`)— 确定视觉方向。
|
||||
**美术风格指南**(`docs/art/美术风格指南.md`)— 确定视觉方向。
|
||||
|
||||
**技术可行性评估** — 评估核心玩法在目标引擎/平台上的实现可行性。
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
|
||||
| 层级 | 定位 | 比喻 | 特点 |
|
||||
|------|------|------|------|
|
||||
| **草案** (concept.md) | 给游戏**定调** | 灵魂 | 回答"这个游戏是什么",简略但方向明确 |
|
||||
| **草案** (概念草案.md) | 给游戏**定调** | 灵魂 | 回答"这个游戏是什么",简略但方向明确 |
|
||||
| **设计案** | 给游戏**定骨架** | 骨架 | 基于草案的原则展开系统设计,定义有哪些系统、系统关系、核心行为 |
|
||||
| **执行案** | 给游戏**填血肉** | 血肉 | 细化到程序可直接实现的交互规则和行为细节 |
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
|
||||
#### 阶段路线图
|
||||
|
||||
原型阶段开始时,产出**阶段路线图**(`docs/plans/prototype-roadmap.md`):
|
||||
原型阶段开始时,产出**阶段路线图**(`docs/plans/原型阶段-路线图.md`):
|
||||
|
||||
- 计划分几个版本迭代
|
||||
- 每个版本探索哪个方向(验证什么假设)
|
||||
@@ -149,10 +149,10 @@
|
||||
|
||||
```
|
||||
docs/design/prototype/
|
||||
player-controller.md # 设计案
|
||||
player-controller-execution.md # 执行案(含迭代版本表头)
|
||||
interaction-system.md
|
||||
interaction-system-execution.md
|
||||
玩家控制器.md # 设计案
|
||||
玩家控制器-执行案.md # 执行案(含迭代版本表头)
|
||||
交互系统.md
|
||||
交互系统-执行案.md
|
||||
...
|
||||
```
|
||||
|
||||
@@ -195,7 +195,7 @@ docs/design/prototype/
|
||||
|
||||
#### 阶段路线图
|
||||
|
||||
批量生产阶段开始时,产出**阶段路线图**(`docs/plans/production-roadmap.md`):
|
||||
批量生产阶段开始时,产出**阶段路线图**(`docs/plans/批量生产阶段-路线图.md`):
|
||||
|
||||
- 计划分几个版本迭代
|
||||
- 每个版本的主要内容范围
|
||||
@@ -212,8 +212,8 @@ docs/design/prototype/
|
||||
|
||||
```
|
||||
docs/design/production/
|
||||
player-controller.md # 在原型基础上更新
|
||||
player-controller-execution.md # 继续迭代,追加新的迭代记录
|
||||
玩家控制器.md # 在原型基础上更新
|
||||
玩家控制器-执行案.md # 继续迭代,追加新的迭代记录
|
||||
...
|
||||
```
|
||||
|
||||
@@ -357,7 +357,7 @@ docs/design/production/
|
||||
6. **版本间的递进关系**:为什么先做这些后做那些
|
||||
7. **每个版本的验收标准**:怎样算"这个版本做完了"
|
||||
|
||||
**产出位置**:`docs/plans/prototype-roadmap.md` 或 `docs/plans/production-roadmap.md`
|
||||
**产出位置**:`docs/plans/原型阶段-路线图.md` 或 `docs/plans/批量生产阶段-路线图.md`
|
||||
|
||||
**版本编号规则**:
|
||||
- 原型阶段:`v0.1`、`v0.2`、`v0.3`……
|
||||
@@ -432,16 +432,16 @@ docs/design/production/
|
||||
|
||||
**产出形式**:
|
||||
- 每个系统一份执行案,放在 `docs/design/<阶段>/` 目录下
|
||||
- 文件命名:`<系统名>-execution.md`(如 `player-controller-execution.md`)
|
||||
- 文件命名:`<系统名>-执行案.md`(如 `玩家控制器-执行案.md`)
|
||||
- 不按版本分文件,通过迭代记录表头追踪变更
|
||||
- **关卡执行案**(如有场景搭建需求):文件命名 `level-v<XX>-<name>-execution.md`,产出模板参见 `.claude/refs/level-execution-template.md`
|
||||
- **关卡执行案**(如有场景搭建需求):文件命名 `关卡-v<XX>-<名称>-执行案.md`,产出模板参见 `.claude/refs/关卡执行案模板.md`
|
||||
|
||||
**关卡执行案**:
|
||||
- 当版本开发涉及场景搭建时,策划额外产出一份关卡执行案
|
||||
- 关卡执行案将场景设计案的氛围描述转化为空间精度搭建指南(坐标、尺寸、BSP 资产映射)
|
||||
- 使用混合坐标体系:区域用场景绝对坐标,区域内物件用相对偏移坐标
|
||||
- 交互物和收集品的位置标注必须与交互系统/收集系统执行案交叉引用
|
||||
- 完整规范参见 `docs/design/prototype/level-design-workflow.md`
|
||||
- 完整规范参见 `docs/design/prototype/关卡设计工作流.md`
|
||||
|
||||
---
|
||||
|
||||
@@ -468,7 +468,7 @@ docs/design/production/
|
||||
- **不通过** → 退回策划修改,列出具体问题清单
|
||||
- **争议** → PM 与策划对判定有分歧时,由用户最终裁决
|
||||
|
||||
**产出**:PM 审查记录(记录在 `docs/plans/reviews/<任务ID>-review.md`)
|
||||
**产出**:PM 审查记录(记录在 `docs/plans/reviews/<任务ID>-审查记录.md`)
|
||||
|
||||
---
|
||||
|
||||
@@ -495,7 +495,7 @@ docs/design/production/
|
||||
| 数据流 | 允许硬编码 | 数据应配置化 |
|
||||
| 美术需求 | 允许简化描述 | 必须具体到尺寸和数量 |
|
||||
|
||||
**产出位置**:`docs/technical/<系统名>-tech.md`
|
||||
**产出位置**:`docs/technical/<系统名>-技术方案.md`
|
||||
|
||||
---
|
||||
|
||||
@@ -534,7 +534,7 @@ docs/design/production/
|
||||
|
||||
**产出位置**:
|
||||
- 资产放 `Content/` 对应目录
|
||||
- 清单记录在 `docs/art/asset-manifest.md` 或直接在技术方案中更新
|
||||
- 清单记录在 `docs/art/资产清单.md` 或直接在技术方案中更新
|
||||
|
||||
---
|
||||
|
||||
@@ -564,13 +564,13 @@ docs/design/production/
|
||||
|
||||
**白模搭建子任务(6a)**:
|
||||
当版本开发涉及场景搭建时,⑥的第一个子任务是白模关卡搭建:
|
||||
- 读取关卡执行案 `level-v<XX>-<name>-execution.md` 和场景设计案
|
||||
- 读取关卡执行案 `关卡-v<XX>-<名称>-执行案.md` 和场景设计案
|
||||
- 使用 `Content/BlockingStarterPack/` 资产在 UE5 编辑器中搭建白模关卡
|
||||
- 按区域分文件夹组织 Actor(命名规范:`LVL_<ZoneID>_<Category>_<Description>`)
|
||||
- 完成比例验证(角色可通过门洞、摄像机不穿墙、流线通行)
|
||||
- 产出搭建报告 `docs/technical/level-v<XX>-build-report.md` 记录偏差
|
||||
- 产出搭建报告 `docs/technical/关卡-v<XX>-搭建报告.md` 记录偏差
|
||||
- **搭建报告必须以评论形式发布到对应任务 Issue**(与本地文件内容一致)
|
||||
- 完整搭建流程参见 `docs/design/prototype/level-design-workflow.md` 第四章
|
||||
- 完整搭建流程参见 `docs/design/prototype/关卡设计工作流.md` 第四章
|
||||
|
||||
**阶段差异**:
|
||||
|
||||
@@ -611,7 +611,7 @@ docs/design/production/
|
||||
- **不通过(重大)** → PM 提出处理建议(回退步骤或开新版本),由用户最终决定
|
||||
- **争议** → PM 与程序对判定有分歧时,由用户最终裁决
|
||||
|
||||
**产出**:PM 审查记录(记录在 `docs/plans/reviews/<任务ID>-review.md`)
|
||||
**产出**:PM 审查记录(记录在 `docs/plans/reviews/<任务ID>-审查记录.md`)
|
||||
|
||||
---
|
||||
|
||||
@@ -656,14 +656,14 @@ PM 在每次 `/plan` 调用时,先判断宏观阶段,再针对**当前版本
|
||||
|
||||
| 检查项 | 判断方式 | 结果 |
|
||||
|--------|----------|------|
|
||||
| 项目处于哪个开发阶段? | 检查 `concept.md` → 检查核心玩法是否已有可玩原型 → 检查内容是否已完整 | 草案 / 原型 / 批量生产 / 测试发布 |
|
||||
| 项目处于哪个开发阶段? | 检查 `概念草案.md` → 检查核心玩法是否已有可玩原型 → 检查内容是否已完整 | 草案 / 原型 / 批量生产 / 测试发布 |
|
||||
|
||||
### 版本流水线检查
|
||||
|
||||
| 检查项 | 检查方式 | 通过条件 |
|
||||
|--------|----------|----------|
|
||||
| ① 设计案是否存在且完整? | 检查 `docs/design/<阶段>/` 中的设计文档和索引 | 覆盖所有主要系统,有索引串联 |
|
||||
| ② 阶段路线图是否存在? | 检查 `docs/plans/<阶段>-roadmap.md` | 版本列表、每版本目标、递进关系明确 |
|
||||
| ② 阶段路线图是否存在? | 检查 `docs/plans/<阶段>-路线图.md` | 版本列表、每版本目标、递进关系明确 |
|
||||
| ③ 执行案是否齐全? | 检查 `docs/design/<阶段>/` 对应执行案 | 每个涉及系统的核心行为规则都已明确 |
|
||||
| **PM 审查**:执行案质量门 | 对照质量标准逐项检查 | 完整性、清晰性、边界情况通过 |
|
||||
| ④ 程序设计方案是否齐全? | 检查 `docs/technical/` 对应文档 | 每个执行案都有对应技术方案 |
|
||||
|
||||
@@ -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`)= **氛围愿景** — 描述感觉、氛围、叙事意图
|
||||
- 关卡执行案 = **空间精度搭建指南** — 坐标、尺寸、资产映射
|
||||
- 两者是互补关系,不是替代关系。程序搭建时两个文档都需要参考。
|
||||
|
||||
@@ -75,7 +75,7 @@ description: "Gitea 协作工具 — CLI 命令速查、认证机制、Assignee
|
||||
| `gate:impl-pass` | 程序实现验收门通过 |
|
||||
| `gate:impl-fail` | 程序实现验收门未通过 |
|
||||
|
||||
> **注**:旧 Issue 上可能仍有 `pipeline:*` 和 `role:*` 标签,这些已不再使用,保留不管即可。
|
||||
> **注**:`pipeline:*` 和 `role:*` 标签已废弃并从 Gitea 上删除。如旧 Issue 上仍残留这些标签,忽略即可。
|
||||
|
||||
---
|
||||
|
||||
@@ -178,7 +178,7 @@ node scripts/gitea.js test
|
||||
| 角色归属 | Assignee | 通过 `--assignees` 指派 |
|
||||
| 流水线步骤 | Pipeline Tracker | Meta Issue 记录全局流水线状态 |
|
||||
| 角色汇报 | Issue 评论 | 各角色在对应 Issue 下评论 |
|
||||
| 审查记录 | Issue 评论(摘要+链接)+ 本地 `docs/plans/reviews/` | 本地保留详细结构化文档 |
|
||||
| 审查记录 | Issue 评论(摘要+链接)+ 本地 `docs/plans/reviews/` | 本地文件名格式 `<任务ID>-审查记录.md` |
|
||||
| 任务归档 | close Issue | 不需要本地 archive 文件 |
|
||||
|
||||
---
|
||||
@@ -266,7 +266,7 @@ node scripts/gitea.js --role pm issue comment <number> --body "## 质量门审
|
||||
|
||||
**质量门**:[类型]
|
||||
**审查日期**:YYYY-MM-DD
|
||||
**审查记录**:docs/plans/reviews/<task-ID>-review.md
|
||||
**审查记录**:docs/plans/reviews/<task-ID>-审查记录.md
|
||||
|
||||
### 结论
|
||||
[一句话总结]
|
||||
@@ -282,7 +282,7 @@ 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"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -315,7 +315,7 @@ node scripts/gitea.js --role pm issue comments <number>
|
||||
# 创建 Issue(必须指定 assignees)
|
||||
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..." \
|
||||
--body "## 任务信息\n- **任务ID**: D-001\n- **所属版本**: v0.1\n- **角色**: 策划\n- **预期产出**: docs/design/prototype/交互系统-执行案.md\n..." \
|
||||
--milestone <id> \
|
||||
--assignees ai-designer
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ description: "质量门审查技能 — 4 道质量门标准、阶段对应标
|
||||
- 根据当前开发阶段选择对应标准(原型 vs 批量生产,见"阶段对应标准")
|
||||
|
||||
### 3. 写审查记录(本地文件)
|
||||
- 路径:`docs/plans/reviews/<task-ID>-review.md`
|
||||
- 路径:`docs/plans/reviews/<task-ID>-审查记录.md`
|
||||
- 格式见下方"审查记录格式"
|
||||
|
||||
### 4. Issue 评论(强制)
|
||||
@@ -117,7 +117,7 @@ description: "质量门审查技能 — 4 道质量门标准、阶段对应标
|
||||
|
||||
## 审查记录格式
|
||||
|
||||
文件路径:`docs/plans/reviews/<task-ID>-review.md`
|
||||
文件路径:`docs/plans/reviews/<task-ID>-审查记录.md`
|
||||
|
||||
```markdown
|
||||
# [任务 ID] 审查报告
|
||||
@@ -148,7 +148,7 @@ description: "质量门审查技能 — 4 道质量门标准、阶段对应标
|
||||
|
||||
审查完成后,**必须**逐项确认以下操作:
|
||||
|
||||
- [ ] 在 `docs/plans/reviews/<task-ID>-review.md` 写了审查记录
|
||||
- [ ] 在 `docs/plans/reviews/<task-ID>-审查记录.md` 写了审查记录
|
||||
- [ ] 在对应 Issue 上发布了审查结论评论(通过/不通过 + 摘要)
|
||||
- [ ] 更新了 Issue 的 gate:* 标签(通过→pass,不通过→fail)
|
||||
- [ ] **通过时**:关闭 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>
|
||||
|
||||
+8
-1
@@ -30,6 +30,13 @@
|
||||
{
|
||||
"Name": "GameplayStateTree",
|
||||
"Enabled": true
|
||||
},
|
||||
{
|
||||
"Name": "PythonScriptPlugin",
|
||||
"Enabled": true,
|
||||
"TargetAllowList": [
|
||||
"Editor"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
"""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
|
||||
|
||||
# 将项目根目录加入 sys.path 以支持 config 导入
|
||||
_SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
_TOOLS_DIR = _SCRIPT_DIR.parent.parent.parent
|
||||
if str(_TOOLS_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(_TOOLS_DIR))
|
||||
|
||||
from tools.level_builder.config.asset_mapping import MESH_ASSETS
|
||||
from tools.level_builder.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]))
|
||||
|
||||
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]))
|
||||
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]))
|
||||
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
|
||||
@@ -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"},
|
||||
}
|
||||
@@ -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},
|
||||
],
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user