merge: 合并main分支,解决冲突并统一交互输入架构
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
# 策划 Agent Memory Index
|
# 策划 Agent Memory Index
|
||||||
|
|
||||||
- [千禧年怀旧文化灵感](nostalgia-culture-inspirations.md) — 电视剧/广播体操/广场舞/电话热线游戏的灵感记录与共识
|
- [千禧年怀旧文化灵感](nostalgia-culture-inspirations.md) — 电视剧/广播体操/广场舞/电话热线/零食/小孩游戏/乡下活动灵感汇总
|
||||||
|
- [建筑与区域规划](building-area-plan.md) — 城市主场景完整建筑清单、可进入室内、新增设施共识
|
||||||
|
- [v0.1执行案状态](v01-execution-status.md) — 三份执行案已通过二次质量门审查,流水线可推进至④程序设计
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
name: building-area-plan
|
||||||
|
description: 城市主场景建筑与区域规划共识,包含完整建筑清单、可进入室内、新增设施
|
||||||
|
metadata:
|
||||||
|
type: project
|
||||||
|
---
|
||||||
|
|
||||||
|
## 城市主场景建筑清单(2026-06-10 确认)
|
||||||
|
|
||||||
|
来源:头脑风暴-2026-06-10-建筑与区域规划.md
|
||||||
|
|
||||||
|
### 关键变更(vs 原场景设计)
|
||||||
|
|
||||||
|
- 小伙伴家:原"全部不可进入" → 4家可进入(简化室内)
|
||||||
|
- 包机房:合并进街机店后屋
|
||||||
|
- 废品回收:固定摊位 + 流动收废品车
|
||||||
|
- 秘密基地:多处散布(工地围挡后、平房区死角等)
|
||||||
|
- 新增店铺:粮油店、服装店/裁缝铺、鞋店(主街东侧)、储蓄所(西侧)
|
||||||
|
- 新增设施:棋牌室(纯氛围)、公交站牌(纯装饰)、公共厕所、自行车棚、信报箱
|
||||||
|
|
||||||
|
### 主街布局(东西排列)
|
||||||
|
|
||||||
|
东→:学校操场 | 小卖部 | 粮油店 | 书店 | 服装店 | 鞋店 · 公交站 · 棋牌室 | 储蓄所 | 录像出租 | 网吧 | 街机店 ←西
|
||||||
|
|
||||||
|
### 可进入室内(~12个)
|
||||||
|
|
||||||
|
完整:主角家、合并小卖部、书店、街机店、网吧、录像出租点
|
||||||
|
简化:死党A家(小区内)、死党B家(平房区)、老板儿子家(小卖部后屋)、女班长家(主角家同楼)
|
||||||
|
|
||||||
|
### 不做的元素
|
||||||
|
|
||||||
|
菜地(留乡下)、装修、游泳(河边不下水)
|
||||||
|
|
||||||
|
### How to apply:
|
||||||
|
- 场景与世界观.md 已更新为最新清单
|
||||||
|
- 新增店铺和设施在后续版本逐步添加(见版本映射)
|
||||||
|
- 概念草案不变(冻结),变更记录在系统设计案中
|
||||||
@@ -54,3 +54,34 @@ metadata:
|
|||||||
- 广播体操和广场舞适合作为场景氛围事件
|
- 广播体操和广场舞适合作为场景氛围事件
|
||||||
- 迈皋桥考据直接用于场景设计的"半城半乡"氛围塑造
|
- 迈皋桥考据直接用于场景设计的"半城半乡"氛围塑造
|
||||||
- 华电子弟学校、工厂宿舍区、菜地、老街都是高还原度的场景元素
|
- 华电子弟学校、工厂宿舍区、菜地、老街都是高还原度的场景元素
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-06-10 灵感补充
|
||||||
|
|
||||||
|
来源:头脑风暴-2026-06-10-建筑与区域规划.md
|
||||||
|
|
||||||
|
### 重点新发现(⭐⭐⭐)
|
||||||
|
|
||||||
|
1. **钻狗洞的秘密基地** — 多处散布(工地围挡后、平房区死角),高探索叙事价值
|
||||||
|
2. **叠纸飞机** — 纸张收集+折叠选择+飞行结果三层玩法
|
||||||
|
3. **不知道名字的小哥哥** — 一次性邂逅NPC,极高情感潜力微型叙事线
|
||||||
|
4. **体力系统+家庭动态事件** — 吃饭补充体力→不吃炒饭→换速冻水饺→给饭钱
|
||||||
|
5. **爷爷奶奶家亲戚小孩** — 乡下NPC角色体系(之前乡下NPC完全空白)
|
||||||
|
|
||||||
|
### 新零食/小吃(具体品牌品种)
|
||||||
|
|
||||||
|
大大泡泡糖、秀逗(超酸糖)、无花果丝、老鼠屎(盐津葡萄)、跳跳糖、烤鱿鱼、烤里脊
|
||||||
|
|
||||||
|
### 新小孩游戏
|
||||||
|
|
||||||
|
双人拍手/互推游戏、猜在哪个手里、跑步比赛、叠纸飞机
|
||||||
|
|
||||||
|
### 新人物/社交
|
||||||
|
|
||||||
|
- 北方来南方的同学(与"转学生"角色互补)
|
||||||
|
- 等着去别人家(串门文化)
|
||||||
|
|
||||||
|
### 新乡下活动
|
||||||
|
|
||||||
|
钓鱼(鱼竿)、游泳、挤牛奶、喂鸡掏鸡蛋、菜地浇水、打水漂
|
||||||
|
|||||||
@@ -2,3 +2,4 @@
|
|||||||
|
|
||||||
- [Gitea Token Permission](gitea-token-permission.md) — reviewer token 仅可评论,标签操作需用 PM token
|
- [Gitea Token Permission](gitea-token-permission.md) — reviewer token 仅可评论,标签操作需用 PM token
|
||||||
- [P-006 Review](p006-review-note.md) — Issue #17 白模搭建审查通过记录
|
- [P-006 Review](p006-review-note.md) — Issue #17 白模搭建审查通过记录
|
||||||
|
- [P-009 Review](p009-review-note.md) — Issue #20 收集系统程序设计审查通过记录
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
name: p009-review-note
|
||||||
|
description: Issue #20 P-009 收集系统程序设计审查通过记录
|
||||||
|
metadata:
|
||||||
|
type: project
|
||||||
|
---
|
||||||
|
|
||||||
|
## P-009 收集系统程序设计审查 (2026-06-10)
|
||||||
|
|
||||||
|
- Issue: #20
|
||||||
|
- 审查类型: 程序设计(质量门 2)
|
||||||
|
- 产出文件: `docs/technical/收集系统-技术方案.md`
|
||||||
|
- 结果: **通过**
|
||||||
|
|
||||||
|
### 审查要点
|
||||||
|
- 技术方案文档路径实际为中文文件名 `收集系统-技术方案.md`(非 Issue 中标注的 `collection-system-tech.md`)
|
||||||
|
- 执行案文件名也是中文:`收集系统-执行案.md`(非 `collection-system-execution.md`)
|
||||||
|
- 6 个检查维度全部通过:执行案覆盖度、美术资源需求、架构清晰度、数据流、技术风险、工作量与拆分建议
|
||||||
|
- 标签操作:移除了 gate:art-fail(30) 和 status:in-review(14),添加了 review:pass(33)
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
> 本模板供策划产出关卡执行案时使用。关卡执行案是场景设计案的"空间精度版本"——将氛围描述转化为带有坐标、尺寸和资产建议的结构化搭建指南,供程序在 UE5 编辑器中搭建白模。
|
> 本模板供策划产出关卡执行案时使用。关卡执行案是场景设计案的"空间精度版本"——将氛围描述转化为带有坐标、尺寸和资产建议的结构化搭建指南,供程序在 UE5 编辑器中搭建白模。
|
||||||
>
|
>
|
||||||
|
> **注意**:交互物和收集品的放置**不在本模板范围内**,由交互系统/收集系统后续在白盒关卡上叠加。
|
||||||
|
>
|
||||||
> 文件命名:`关卡-v<XX>-<名称>-执行案.md`(如 `关卡-v01-城郊街区-执行案.md`)
|
> 文件命名:`关卡-v<XX>-<名称>-执行案.md`(如 `关卡-v01-城郊街区-执行案.md`)
|
||||||
> 存放位置:`docs/design/<阶段>/`(与其他执行案同目录)
|
> 存放位置:`docs/design/<阶段>/`(与其他执行案同目录)
|
||||||
|
|
||||||
@@ -131,30 +133,18 @@
|
|||||||
| Zx-P02 | | | | | |
|
| Zx-P02 | | | | | |
|
||||||
| ... | | | | | |
|
| ... | | | | | |
|
||||||
|
|
||||||
#### 3.x.4 可交互物体标记
|
#### 3.x.4 组合资产标记
|
||||||
|
|
||||||
<!-- 引用交互系统执行案中的交互物,此处只标注空间位置 -->
|
<!-- 需要多个 Primitives 拼搭的物件(无直接 BSP 匹配)。如无此类物件可删除本节。 -->
|
||||||
|
|
||||||
| 交互编号 | 物件名称 | 交互类型 | 相对原点偏移 (ΔX,ΔY,ΔZ) | 触发半径 (cm) | 引用 |
|
| 编号 | 物件名称 | 期望尺寸 (约, cm) | 推荐拼搭方案 | 说明 |
|
||||||
|---------|---------|---------|----------------------|-------------|------|
|
|------|---------|-----------------|-------------|------|
|
||||||
| Zx-I01 | <!-- 如 旧冰箱 --> | <!-- 如 观察 --> | (200,400,10) | <!-- 如 150 --> | 交互系统-执行案 #n |
|
| Zx-C01 | <!-- 如 旧收音机 --> | <!-- 如 20x15x12 --> | <!-- 如 Cube(0.2,0.15,0.12) + Cylinder天线 --> | <!-- 如 放在柜台上 --> |
|
||||||
| Zx-I02 | | | | | |
|
|
||||||
| ... | | | | | |
|
|
||||||
|
|
||||||
> 交互类型参照交互系统执行案中的分类(观察/拾取/操作/对话等)。
|
|
||||||
> "引用"列填写交互系统执行案中对应交互物的编号,确保跨文档一致。
|
|
||||||
|
|
||||||
#### 3.x.5 收集品标记
|
|
||||||
|
|
||||||
<!-- 引用收集系统执行案中的收集品,此处只标注空间位置 -->
|
|
||||||
|
|
||||||
| 收集编号 | 物品名称 | 稀有度 | 相对原点偏移 (ΔX,ΔY,ΔZ) | 引用 |
|
|
||||||
|---------|---------|--------|----------------------|------|
|
|
||||||
| Zx-C01 | <!-- 如 彩色弹珠 --> | <!-- 如 普通/稀有 --> | (500,200,0) | 收集系统-执行案 #n |
|
|
||||||
| Zx-C02 | | | | |
|
| Zx-C02 | | | | |
|
||||||
| ... | | | | | |
|
| ... | | | | | |
|
||||||
|
|
||||||
> 稀有度影响拾取物的视觉效果标识(普通=白色光,稀有=金色光),具体表现由收集系统技术方案定义。
|
> 组合资产将在搭建指南生成阶段展开为多个 Primitive Actor,并在组合资产清单中记录拼搭方案。
|
||||||
|
> 用户确认后的组合资产方案将回馈到 asset-catalog 模板库,供后续关卡直接引用。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -183,14 +173,6 @@
|
|||||||
| G-E02 | | | | | |
|
| G-E02 | | | | | |
|
||||||
| ... | | | | | |
|
| ... | | | | | |
|
||||||
|
|
||||||
### 4.3 收集品位置汇总
|
|
||||||
|
|
||||||
| # | 物品名称 | 所在区域 | 绝对坐标 (X,Y,Z) | 稀有度 | 引用 |
|
|
||||||
|---|---------|---------|-----------------|--------|------|
|
|
||||||
| 1 | | | | | 收集系统-执行案 #n |
|
|
||||||
| 2 | | | | | |
|
|
||||||
| ... | | | | | |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 五、场景边界与遮挡
|
## 五、场景边界与遮挡
|
||||||
@@ -225,10 +207,10 @@
|
|||||||
### 策划填写要点
|
### 策划填写要点
|
||||||
|
|
||||||
1. **先确定区域坐标布局**(第二章),确保所有区域在总尺寸内不重叠、流线合理
|
1. **先确定区域坐标布局**(第二章),确保所有区域在总尺寸内不重叠、流线合理
|
||||||
2. **逐区域填写**(第三章),按"结构体 → 布景 → 交互物 → 收集品"的顺序
|
2. **逐区域填写**(第三章),按"结构体 → 布景 → 组合资产"的顺序
|
||||||
3. **BSP 资产建议是方向性指引**:写资产类别名和组合方式即可(如 "Wall_400x300 × 2"),不需要写文件路径
|
3. **BSP 资产建议是方向性指引**:写资产类别名和组合方式即可(如 "Wall_400x300 × 2"),不需要写文件路径
|
||||||
4. **尺寸以 cm 为单位**,对应 UE5 的 1 uu = 1 cm
|
4. **尺寸以 cm 为单位**,对应 UE5 的 1 uu = 1 cm
|
||||||
5. **交叉引用**:交互物和收集品必须引用对应系统执行案中的编号,保持跨文档一致
|
5. **组合资产**:无法用单个 BSP 资产匹配的物件,在 3.x.4 中记录拼搭方案
|
||||||
|
|
||||||
### 不要填写的内容
|
### 不要填写的内容
|
||||||
|
|
||||||
@@ -237,6 +219,7 @@
|
|||||||
- ❌ Blueprint 类名
|
- ❌ Blueprint 类名
|
||||||
- ❌ 精确的硬编码参数值(标注"建议值,可调"并附范围)
|
- ❌ 精确的硬编码参数值(标注"建议值,可调"并附范围)
|
||||||
- ❌ 代码或脚本
|
- ❌ 代码或脚本
|
||||||
|
- ❌ 交互物和收集品位置(由系统后续叠加)
|
||||||
|
|
||||||
### 与场景设计案的关系
|
### 与场景设计案的关系
|
||||||
|
|
||||||
|
|||||||
@@ -23,25 +23,33 @@ AChildHoodCharacter::AChildHoodCharacter()
|
|||||||
bUseControllerRotationYaw = false;
|
bUseControllerRotationYaw = false;
|
||||||
bUseControllerRotationRoll = false;
|
bUseControllerRotationRoll = false;
|
||||||
|
|
||||||
// Configure character movement
|
// Configure character movement — v0.1 player controller tech design parameters
|
||||||
GetCharacterMovement()->bOrientRotationToMovement = true;
|
auto* CMC = GetCharacterMovement();
|
||||||
GetCharacterMovement()->RotationRate = FRotator(0.0f, 500.0f, 0.0f);
|
CMC->bOrientRotationToMovement = true; // 角色朝移动方向旋转
|
||||||
|
CMC->RotationRate = FRotator(0.0f, 720.0f, 0.0f); // 转向速度 720°/s(远快于鼠标 150°/s)
|
||||||
// Note: For faster iteration times these variables, and many more, can be tweaked in the Character Blueprint
|
CMC->MaxWalkSpeed = 450.0f; // 跑步速度 4.5 m/s
|
||||||
// instead of recompiling to adjust them
|
CMC->MaxAcceleration = 2250.0f; // 起步加速 0.2s (450/0.2)
|
||||||
GetCharacterMovement()->JumpZVelocity = 500.f;
|
CMC->BrakingDecelerationWalking = 1500.0f; // 停止减速 0.3s (450/0.3)
|
||||||
GetCharacterMovement()->AirControl = 0.35f;
|
CMC->BrakingDecelerationFalling = 1500.0f;
|
||||||
GetCharacterMovement()->MaxWalkSpeed = 500.f;
|
CMC->MinAnalogWalkSpeed = 20.f;
|
||||||
GetCharacterMovement()->MinAnalogWalkSpeed = 20.f;
|
|
||||||
GetCharacterMovement()->BrakingDecelerationWalking = 2000.f;
|
|
||||||
GetCharacterMovement()->BrakingDecelerationFalling = 1500.0f;
|
|
||||||
|
|
||||||
// Create a camera boom (pulls in towards the player if there is a collision)
|
// Create a camera boom (pulls in towards the player if there is a collision)
|
||||||
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
|
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
|
||||||
CameraBoom->SetupAttachment(RootComponent);
|
CameraBoom->SetupAttachment(RootComponent);
|
||||||
CameraBoom->TargetArmLength = 400.0f;
|
CameraBoom->TargetArmLength = 350.0f; // 摄像机距离 3.5m
|
||||||
|
CameraBoom->SocketOffset = FVector(0.f, 0.f, 80.f); // 摄像机高度 0.8m(头部上方)
|
||||||
CameraBoom->bUsePawnControlRotation = true;
|
CameraBoom->bUsePawnControlRotation = true;
|
||||||
|
|
||||||
|
// 摄像机软跟随
|
||||||
|
CameraBoom->bEnableCameraLag = true;
|
||||||
|
CameraBoom->CameraLagSpeed = 6.0f;
|
||||||
|
CameraBoom->bEnableCameraRotationLag = false;
|
||||||
|
|
||||||
|
// 摄像机碰撞处理
|
||||||
|
CameraBoom->bDoCollisionTest = true;
|
||||||
|
CameraBoom->ProbeSize = 12.0f;
|
||||||
|
CameraBoom->ProbeChannel = ECC_Camera;
|
||||||
|
|
||||||
// Create a follow camera
|
// Create a follow camera
|
||||||
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
|
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
|
||||||
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
|
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
|
||||||
@@ -56,16 +64,26 @@ void AChildHoodCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInput
|
|||||||
// Set up action bindings
|
// Set up action bindings
|
||||||
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent)) {
|
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent)) {
|
||||||
|
|
||||||
// Jumping
|
|
||||||
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &ACharacter::Jump);
|
|
||||||
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
|
|
||||||
|
|
||||||
// Moving
|
// Moving
|
||||||
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AChildHoodCharacter::Move);
|
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AChildHoodCharacter::Move);
|
||||||
|
|
||||||
|
// Looking (gamepad + mouse)
|
||||||
|
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AChildHoodCharacter::Look);
|
||||||
EnhancedInputComponent->BindAction(MouseLookAction, ETriggerEvent::Triggered, this, &AChildHoodCharacter::Look);
|
EnhancedInputComponent->BindAction(MouseLookAction, ETriggerEvent::Triggered, this, &AChildHoodCharacter::Look);
|
||||||
|
|
||||||
// Looking
|
// Interaction — delegates to InteractionManager
|
||||||
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AChildHoodCharacter::Look);
|
if (InteractAction)
|
||||||
|
{
|
||||||
|
EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Started, this, &AChildHoodCharacter::OnInteract);
|
||||||
|
}
|
||||||
|
if (CancelAction)
|
||||||
|
{
|
||||||
|
EnhancedInputComponent->BindAction(CancelAction, ETriggerEvent::Started, this, &AChildHoodCharacter::OnCancel);
|
||||||
|
}
|
||||||
|
if (SwitchInteractAction)
|
||||||
|
{
|
||||||
|
EnhancedInputComponent->BindAction(SwitchInteractAction, ETriggerEvent::Started, this, &AChildHoodCharacter::OnSwitchInteract);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -91,6 +109,34 @@ void AChildHoodCharacter::Look(const FInputActionValue& Value)
|
|||||||
DoLook(LookAxisVector.X, LookAxisVector.Y);
|
DoLook(LookAxisVector.X, LookAxisVector.Y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AChildHoodCharacter::OnInteract(const FInputActionValue& Value)
|
||||||
|
{
|
||||||
|
if (UInteractionManager* IM = GetInteractionManager())
|
||||||
|
{
|
||||||
|
// E 键双重功能:Free 发起交互,Active 结束交互
|
||||||
|
IM->BeginInteract();
|
||||||
|
IM->CancelInteract();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AChildHoodCharacter::OnCancel(const FInputActionValue& Value)
|
||||||
|
{
|
||||||
|
if (UInteractionManager* IM = GetInteractionManager())
|
||||||
|
{
|
||||||
|
IM->CancelInteract();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AChildHoodCharacter::OnSwitchInteract(const FInputActionValue& Value)
|
||||||
|
{
|
||||||
|
if (UInteractionManager* IM = GetInteractionManager())
|
||||||
|
{
|
||||||
|
float DirectionValue = Value.Get<float>();
|
||||||
|
int32 Direction = (DirectionValue >= 0.f) ? 1 : -1;
|
||||||
|
IM->SwitchTarget(Direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void AChildHoodCharacter::DoMove(float Right, float Forward)
|
void AChildHoodCharacter::DoMove(float Right, float Forward)
|
||||||
{
|
{
|
||||||
// 交互中屏蔽移动
|
// 交互中屏蔽移动
|
||||||
@@ -127,18 +173,6 @@ void AChildHoodCharacter::DoLook(float Yaw, float Pitch)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AChildHoodCharacter::DoJumpStart()
|
|
||||||
{
|
|
||||||
// signal the character to jump
|
|
||||||
Jump();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AChildHoodCharacter::DoJumpEnd()
|
|
||||||
{
|
|
||||||
// signal the character to stop jumping
|
|
||||||
StopJumping();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 交互系统方法
|
// 交互系统方法
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -157,3 +191,9 @@ bool AChildHoodCharacter::IsInteracting() const
|
|||||||
{
|
{
|
||||||
return bIsInteracting;
|
return bIsInteracting;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool AChildHoodCharacter::IsRunning() const
|
||||||
|
{
|
||||||
|
return GetVelocity().SizeSquared() > 100.f // 速度 > 10 cm/s 阈值
|
||||||
|
&& GetCharacterMovement()->IsMovingOnGround();
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,10 +34,6 @@ class AChildHoodCharacter : public ACharacter
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|
||||||
/** Jump Input Action */
|
|
||||||
UPROPERTY(EditAnywhere, Category="Input")
|
|
||||||
UInputAction* JumpAction;
|
|
||||||
|
|
||||||
/** Move Input Action */
|
/** Move Input Action */
|
||||||
UPROPERTY(EditAnywhere, Category="Input")
|
UPROPERTY(EditAnywhere, Category="Input")
|
||||||
UInputAction* MoveAction;
|
UInputAction* MoveAction;
|
||||||
@@ -50,11 +46,23 @@ protected:
|
|||||||
UPROPERTY(EditAnywhere, Category="Input")
|
UPROPERTY(EditAnywhere, Category="Input")
|
||||||
UInputAction* MouseLookAction;
|
UInputAction* MouseLookAction;
|
||||||
|
|
||||||
|
/** Interact Input Action (E key) — delegates to interaction system */
|
||||||
|
UPROPERTY(EditAnywhere, Category="Input")
|
||||||
|
UInputAction* InteractAction;
|
||||||
|
|
||||||
|
/** Cancel Input Action (ESC key) — delegates to interaction system */
|
||||||
|
UPROPERTY(EditAnywhere, Category="Input")
|
||||||
|
UInputAction* CancelAction;
|
||||||
|
|
||||||
|
/** Switch Interact Target Input Action (Tab / Mouse Wheel) — delegates to interaction system */
|
||||||
|
UPROPERTY(EditAnywhere, Category="Input")
|
||||||
|
UInputAction* SwitchInteractAction;
|
||||||
|
|
||||||
/** 交互管理器组件 */
|
/** 交互管理器组件 */
|
||||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Interaction", meta = (AllowPrivateAccess = "true"))
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Interaction", meta = (AllowPrivateAccess = "true"))
|
||||||
UInteractionManager* InteractionManager;
|
UInteractionManager* InteractionManager;
|
||||||
|
|
||||||
/** 是否处于交互状态(交互期间屏蔽移动) */
|
/** Whether the character is currently in an interaction */
|
||||||
bool bIsInteracting = false;
|
bool bIsInteracting = false;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -66,14 +74,18 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Interaction")
|
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Interaction")
|
||||||
UInteractionManager* GetInteractionManager() const;
|
UInteractionManager* GetInteractionManager() const;
|
||||||
|
|
||||||
/** 设置交互状态(InteractionManager 调用,控制移动屏蔽) */
|
/** Set interaction state — called by interaction system */
|
||||||
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||||
void SetInteracting(bool bValue);
|
void SetInteracting(bool bValue);
|
||||||
|
|
||||||
/** 查询当前是否在交互中 */
|
/** Query: is the character currently interacting? */
|
||||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Interaction")
|
UFUNCTION(BlueprintCallable, BlueprintPure, Category="Interaction")
|
||||||
bool IsInteracting() const;
|
bool IsInteracting() const;
|
||||||
|
|
||||||
|
/** Query: is the character currently running (moving on ground)? */
|
||||||
|
UFUNCTION(BlueprintCallable, BlueprintPure, Category="Interaction")
|
||||||
|
bool IsRunning() const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|
||||||
/** Initialize input action bindings */
|
/** Initialize input action bindings */
|
||||||
@@ -87,6 +99,15 @@ protected:
|
|||||||
/** Called for looking input */
|
/** Called for looking input */
|
||||||
void Look(const FInputActionValue& Value);
|
void Look(const FInputActionValue& Value);
|
||||||
|
|
||||||
|
/** Called for interact input (E) — delegates to InteractionManager */
|
||||||
|
void OnInteract(const FInputActionValue& Value);
|
||||||
|
|
||||||
|
/** Called for cancel input (ESC) — delegates to InteractionManager */
|
||||||
|
void OnCancel(const FInputActionValue& Value);
|
||||||
|
|
||||||
|
/** Called for switch interact target input (Tab/Wheel) — delegates to InteractionManager */
|
||||||
|
void OnSwitchInteract(const FInputActionValue& Value);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
/** Handles move inputs from either controls or UI interfaces */
|
/** Handles move inputs from either controls or UI interfaces */
|
||||||
@@ -97,14 +118,6 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category="Input")
|
UFUNCTION(BlueprintCallable, Category="Input")
|
||||||
virtual void DoLook(float Yaw, float Pitch);
|
virtual void DoLook(float Yaw, float Pitch);
|
||||||
|
|
||||||
/** Handles jump pressed inputs from either controls or UI interfaces */
|
|
||||||
UFUNCTION(BlueprintCallable, Category="Input")
|
|
||||||
virtual void DoJumpStart();
|
|
||||||
|
|
||||||
/** Handles jump pressed inputs from either controls or UI interfaces */
|
|
||||||
UFUNCTION(BlueprintCallable, Category="Input")
|
|
||||||
virtual void DoJumpEnd();
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
/** Returns CameraBoom subobject **/
|
/** Returns CameraBoom subobject **/
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||||
|
|
||||||
|
|
||||||
#include "ChildHoodPlayerController.h"
|
#include "ChildHoodPlayerController.h"
|
||||||
#include "ChildHoodCharacter.h"
|
|
||||||
#include "Interaction/InteractionManager.h"
|
|
||||||
#include "EnhancedInputComponent.h"
|
|
||||||
#include "EnhancedInputSubsystems.h"
|
#include "EnhancedInputSubsystems.h"
|
||||||
#include "Engine/LocalPlayer.h"
|
#include "Engine/LocalPlayer.h"
|
||||||
#include "InputMappingContext.h"
|
#include "InputMappingContext.h"
|
||||||
@@ -15,6 +13,19 @@ void AChildHoodPlayerController::BeginPlay()
|
|||||||
{
|
{
|
||||||
Super::BeginPlay();
|
Super::BeginPlay();
|
||||||
|
|
||||||
|
// C3: 设置摄像机俯仰角限制(垂直旋转 +45°/-30°)
|
||||||
|
if (PlayerCameraManager)
|
||||||
|
{
|
||||||
|
PlayerCameraManager->ViewPitchMin = -30.0f; // 向下看最多 30°
|
||||||
|
PlayerCameraManager->ViewPitchMax = 45.0f; // 向上看最多 45°
|
||||||
|
}
|
||||||
|
|
||||||
|
// C1: 初始俯角 -15°(摄像机默认略向下看向角色)
|
||||||
|
if (GetControlRotation().Pitch == 0.f)
|
||||||
|
{
|
||||||
|
SetControlRotation(FRotator(-15.0f, GetControlRotation().Yaw, GetControlRotation().Roll));
|
||||||
|
}
|
||||||
|
|
||||||
// only spawn touch controls on local player controllers
|
// only spawn touch controls on local player controllers
|
||||||
if (ShouldUseTouchControls() && IsLocalPlayerController())
|
if (ShouldUseTouchControls() && IsLocalPlayerController())
|
||||||
{
|
{
|
||||||
@@ -59,28 +70,6 @@ void AChildHoodPlayerController::SetupInputComponent()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绑定交互输入(Enhanced Input Action)
|
|
||||||
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(InputComponent))
|
|
||||||
{
|
|
||||||
// 交互(E 键)
|
|
||||||
if (InteractAction)
|
|
||||||
{
|
|
||||||
EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Started, this, &AChildHoodPlayerController::OnInteractPressed);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 取消(ESC 键)
|
|
||||||
if (CancelAction)
|
|
||||||
{
|
|
||||||
EnhancedInputComponent->BindAction(CancelAction, ETriggerEvent::Started, this, &AChildHoodPlayerController::OnCancelPressed);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换交互目标(Tab/滚轮)
|
|
||||||
if (SwitchInteractAction)
|
|
||||||
{
|
|
||||||
EnhancedInputComponent->BindAction(SwitchInteractAction, ETriggerEvent::Triggered, this, &AChildHoodPlayerController::OnSwitchInteract);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,56 +78,3 @@ bool AChildHoodPlayerController::ShouldUseTouchControls() const
|
|||||||
// are we on a mobile platform? Should we force touch?
|
// are we on a mobile platform? Should we force touch?
|
||||||
return SVirtualJoystick::ShouldDisplayTouchInterface() || bForceTouchControls;
|
return SVirtualJoystick::ShouldDisplayTouchInterface() || bForceTouchControls;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 交互输入回调
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
void AChildHoodPlayerController::OnInteractPressed()
|
|
||||||
{
|
|
||||||
if (APawn* MyPawn = GetPawn())
|
|
||||||
{
|
|
||||||
if (AChildHoodCharacter* Char = Cast<AChildHoodCharacter>(MyPawn))
|
|
||||||
{
|
|
||||||
if (UInteractionManager* IM = Char->GetInteractionManager())
|
|
||||||
{
|
|
||||||
// E 键双重功能:Free 状态发起交互,Active 阶段结束交互
|
|
||||||
// BeginInteract 在非 Free 状态会被拒绝(内部 return)
|
|
||||||
// CancelInteract 在非 Interacting 状态或 AnimLock 阶段会被拒绝
|
|
||||||
IM->BeginInteract();
|
|
||||||
IM->CancelInteract();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void AChildHoodPlayerController::OnCancelPressed()
|
|
||||||
{
|
|
||||||
if (APawn* MyPawn = GetPawn())
|
|
||||||
{
|
|
||||||
if (AChildHoodCharacter* Char = Cast<AChildHoodCharacter>(MyPawn))
|
|
||||||
{
|
|
||||||
if (UInteractionManager* IM = Char->GetInteractionManager())
|
|
||||||
{
|
|
||||||
IM->CancelInteract();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void AChildHoodPlayerController::OnSwitchInteract(const FInputActionValue& Value)
|
|
||||||
{
|
|
||||||
if (APawn* MyPawn = GetPawn())
|
|
||||||
{
|
|
||||||
if (AChildHoodCharacter* Char = Cast<AChildHoodCharacter>(MyPawn))
|
|
||||||
{
|
|
||||||
if (UInteractionManager* IM = Char->GetInteractionManager())
|
|
||||||
{
|
|
||||||
// 读取 float 值作为方向:正值=下一个,负值=上一个
|
|
||||||
float DirectionValue = Value.Get<float>();
|
|
||||||
int32 Direction = (DirectionValue >= 0.f) ? 1 : -1;
|
|
||||||
IM->SwitchTarget(Direction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,9 +7,7 @@
|
|||||||
#include "ChildHoodPlayerController.generated.h"
|
#include "ChildHoodPlayerController.generated.h"
|
||||||
|
|
||||||
class UInputMappingContext;
|
class UInputMappingContext;
|
||||||
class UInputAction;
|
|
||||||
class UUserWidget;
|
class UUserWidget;
|
||||||
struct FInputActionValue;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Basic PlayerController class for a third person game
|
* Basic PlayerController class for a third person game
|
||||||
@@ -19,7 +17,7 @@ UCLASS(abstract)
|
|||||||
class AChildHoodPlayerController : public APlayerController
|
class AChildHoodPlayerController : public APlayerController
|
||||||
{
|
{
|
||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|
||||||
/** Input Mapping Contexts */
|
/** Input Mapping Contexts */
|
||||||
@@ -42,20 +40,6 @@ protected:
|
|||||||
UPROPERTY(EditAnywhere, Config, Category = "Input|Touch Controls")
|
UPROPERTY(EditAnywhere, Config, Category = "Input|Touch Controls")
|
||||||
bool bForceTouchControls = false;
|
bool bForceTouchControls = false;
|
||||||
|
|
||||||
// === 交互输入 ===
|
|
||||||
|
|
||||||
/** 交互输入动作(E 键) */
|
|
||||||
UPROPERTY(EditAnywhere, Category = "Input|Interaction")
|
|
||||||
UInputAction* InteractAction;
|
|
||||||
|
|
||||||
/** 取消输入动作(ESC 键) */
|
|
||||||
UPROPERTY(EditAnywhere, Category = "Input|Interaction")
|
|
||||||
UInputAction* CancelAction;
|
|
||||||
|
|
||||||
/** 切换交互目标输入动作(Tab/滚轮) */
|
|
||||||
UPROPERTY(EditAnywhere, Category = "Input|Interaction")
|
|
||||||
UInputAction* SwitchInteractAction;
|
|
||||||
|
|
||||||
/** Gameplay initialization */
|
/** Gameplay initialization */
|
||||||
virtual void BeginPlay() override;
|
virtual void BeginPlay() override;
|
||||||
|
|
||||||
@@ -65,14 +49,4 @@ protected:
|
|||||||
/** Returns true if the player should use UMG touch controls */
|
/** Returns true if the player should use UMG touch controls */
|
||||||
bool ShouldUseTouchControls() const;
|
bool ShouldUseTouchControls() const;
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
/** E 键按下回调:发起交互 */
|
|
||||||
void OnInteractPressed();
|
|
||||||
|
|
||||||
/** ESC 键按下回调:取消/结束交互 */
|
|
||||||
void OnCancelPressed();
|
|
||||||
|
|
||||||
/** Tab/滚轮回调:切换交互目标 */
|
|
||||||
void OnSwitchInteract(const FInputActionValue& Value);
|
|
||||||
};
|
};
|
||||||
|
|||||||
+215
-24
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> 完整游戏世界的空间、时代、氛围设计。
|
> 完整游戏世界的空间、时代、氛围设计。
|
||||||
> 覆盖城市主场景 + 乡下场景,以及场景与各系统的关联。
|
> 覆盖城市主场景 + 乡下场景,以及场景与各系统的关联。
|
||||||
> 最后更新:2026-06-01
|
> 最后更新:2026-06-10
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -51,8 +51,8 @@
|
|||||||
|------|------|--------|
|
|------|------|--------|
|
||||||
| 尺度 | ~80m × 60m | ~150m × 120m |
|
| 尺度 | ~80m × 60m | ~150m × 120m |
|
||||||
| 区域数 | 6 个核心 + 1 背景 | 主街 + 生活区 + 边缘三大板块 |
|
| 区域数 | 6 个核心 + 1 背景 | 主街 + 生活区 + 边缘三大板块 |
|
||||||
| 可进入室内 | 0-1 个 | 5-6 个(家 + 店铺 + 网吧) |
|
| 可进入室内 | 0-1 个 | ~12 个(家 + 6 店铺 + 4 小伙伴家 + 网吧) |
|
||||||
| 店铺 | 仅小卖部 | 小卖部、书店、街机店、网吧、录像出租点 |
|
| 店铺 | 仅小卖部 | 小卖部、书店、粮油店、服装店、鞋店、街机店、网吧、录像出租点、储蓄所 |
|
||||||
| 居住形态 | 平房带院子 | 事业单位楼房 + 小区 + 平房区 |
|
| 居住形态 | 平房带院子 | 事业单位楼房 + 小区 + 平房区 |
|
||||||
|
|
||||||
### 2.3 总体布局
|
### 2.3 总体布局
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
|
|
||||||
## 三、商业主街
|
## 三、商业主街
|
||||||
|
|
||||||
主街约 70m 长、5-6m 宽的水泥路,是整个场景最繁华的区域。以中间为界,**西侧是商业娱乐区**(街机、网吧、录像出租),**东侧是传统生活区**(小卖部、书店、学校)。街边散布着各类摊贩。
|
主街约 70m 长、5-6m 宽的水泥路,是整个场景最繁华的区域。以中间为界,**西侧是商业娱乐区**(街机、网吧、录像出租、储蓄所、棋牌室),**东侧是传统生活区**(小卖部、书店、粮油店、服装店、鞋店、学校)。街边散布着各类摊贩和公交站牌。
|
||||||
|
|
||||||
### 3.1 东侧(传统生活)
|
### 3.1 东侧(传统生活)
|
||||||
|
|
||||||
@@ -201,7 +201,65 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### 3.1.3 学校
|
#### 3.1.3 粮油店
|
||||||
|
|
||||||
|
**位置**:主街东部,书店旁边
|
||||||
|
**面积**:小型(~3m × 3m)
|
||||||
|
**可进入**:简化(门口交互为主)
|
||||||
|
|
||||||
|
**外观**:
|
||||||
|
- 窄门面,门口摆着几袋大米和油桶
|
||||||
|
- 招牌手写"粮油"二字,简单直接
|
||||||
|
- 门口有电子秤
|
||||||
|
|
||||||
|
**时代物件**:散装大米、5L 塑料油桶、酱油醋大缸(散装打)、挂面
|
||||||
|
|
||||||
|
**系统关联**:
|
||||||
|
- 氛围营造(城郊生活必需品店铺)
|
||||||
|
- 可能作为零花钱系统的消费点
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3.1.4 服装店/裁缝铺
|
||||||
|
|
||||||
|
**位置**:主街东部,粮油店旁边
|
||||||
|
**面积**:小型(~3m × 3m)
|
||||||
|
**可进入**:简化(门口交互为主)
|
||||||
|
|
||||||
|
**外观**:
|
||||||
|
- 门口挂着几件成衣样品
|
||||||
|
- 招牌写着"XX 服装"或"裁缝"
|
||||||
|
- 老板娘坐在门口
|
||||||
|
|
||||||
|
**时代物件**:成衣、布匹、缝纫机、量衣尺
|
||||||
|
|
||||||
|
**系统关联**:
|
||||||
|
- 氛围营造(裁缝铺是城郊标配)
|
||||||
|
- 简化对话交互
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3.1.5 鞋店
|
||||||
|
|
||||||
|
**位置**:主街东部,服装店旁边
|
||||||
|
**面积**:小型(~3m × 3m)
|
||||||
|
**可进入**:简化(门口交互为主)
|
||||||
|
|
||||||
|
**外观**:
|
||||||
|
- 门口摆着几排鞋(运动鞋为主)
|
||||||
|
- 招牌写着"XX 鞋业"
|
||||||
|
- 墙上挂着运动鞋海报(安踏/李宁风格)
|
||||||
|
|
||||||
|
**时代物件**:运动鞋(安踏/李宁早期款式)、塑料凉鞋、布鞋
|
||||||
|
|
||||||
|
**系统关联**:
|
||||||
|
- 氛围营造(运动鞋是小孩的梦想)
|
||||||
|
- 简化对话交互
|
||||||
|
- 可能与"换装"概念联动
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3.1.6 学校
|
||||||
|
|
||||||
**位置**:主街最东端
|
**位置**:主街最东端
|
||||||
**朝向**:正门面向主街,侧门(小铁门)面向生活区巷子
|
**朝向**:正门面向主街,侧门(小铁门)面向生活区巷子
|
||||||
@@ -279,6 +337,7 @@
|
|||||||
- 经济系统(柜台换币消费)
|
- 经济系统(柜台换币消费)
|
||||||
- 小霸王常驻于此(格斗高手)
|
- 小霸王常驻于此(格斗高手)
|
||||||
- 西侧的"磁石"——孩子们被声音和灯光吸引过来
|
- 西侧的"磁石"——孩子们被声音和灯光吸引过来
|
||||||
|
- **包机房后屋**:门帘隔开的内部空间,提供主机游戏(PS/SS 风格),比街机更沉浸但更贵
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -362,6 +421,58 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
#### 3.2.4 储蓄所
|
||||||
|
|
||||||
|
**位置**:主街西部,录像出租点旁边
|
||||||
|
**面积**:小型(~4m × 3m)
|
||||||
|
**可进入**:简化(柜台交互)
|
||||||
|
|
||||||
|
**外观**:
|
||||||
|
- 正式的门面,比周围店铺更"正经"
|
||||||
|
- 招牌写着"XX 储蓄所"或"XX 信用社"
|
||||||
|
- 门上有铁栅栏门(下班后拉上)
|
||||||
|
|
||||||
|
**时代物件**:存折、手写利率牌、算盘、老式点钞机
|
||||||
|
|
||||||
|
**系统关联**:
|
||||||
|
- 零花钱系统(存取零花钱?待定)
|
||||||
|
- 时代氛围(储蓄所是那个年代的金融地标)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3.2.5 棋牌室
|
||||||
|
|
||||||
|
**位置**:主街西部
|
||||||
|
**面积**:小型
|
||||||
|
**可进入**:❌ 不可进入(大人的场所)
|
||||||
|
|
||||||
|
**外观**:
|
||||||
|
- 半开的门,里面烟雾缭绕
|
||||||
|
- 门口可能坐着抽烟的大人
|
||||||
|
- 招牌写着"棋牌"或"茶室"
|
||||||
|
- 能听到麻将牌碰撞的声音
|
||||||
|
|
||||||
|
**系统关联**:
|
||||||
|
- 纯氛围营造(大人的世界,小孩好奇但不能进)
|
||||||
|
- 声音景观(麻将声作为主街西侧的背景音)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3.2.6 主街设施
|
||||||
|
|
||||||
|
**公交站牌**:
|
||||||
|
- 主街中段的简易公交站牌(铁杆+铁皮站牌)
|
||||||
|
- 公交车会经过但不可乘坐
|
||||||
|
- 纯装饰,增强城郊出行氛围
|
||||||
|
|
||||||
|
**废品回收点**:
|
||||||
|
- 主街路边固定摊位
|
||||||
|
- 一把秤 + 几个编织袋 + 地上分类的废品
|
||||||
|
- 老板娘坐镇,可交互卖废品
|
||||||
|
- 与流动收废品车互补(固定点方便,流动车价格可能不同)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### 3.3 街边摊贩
|
### 3.3 街边摊贩
|
||||||
|
|
||||||
**分布**:散布在主街各处 + 生活区巷子中
|
**分布**:散布在主街各处 + 生活区巷子中
|
||||||
@@ -372,6 +483,8 @@
|
|||||||
|----------|---------|---------|------|
|
|----------|---------|---------|------|
|
||||||
| 冰棍推车 | 主街阴凉处、学校门口 | 泡沫箱+棉被保温,绿豆冰/红豆冰/白糖冰棍 | 夏天标配 |
|
| 冰棍推车 | 主街阴凉处、学校门口 | 泡沫箱+棉被保温,绿豆冰/红豆冰/白糖冰棍 | 夏天标配 |
|
||||||
| 烤肠机 | 小卖部门口、网吧门口 | 电烤机,1-2 元一根 | 简单经济触点 |
|
| 烤肠机 | 小卖部门口、网吧门口 | 电烤机,1-2 元一根 | 简单经济触点 |
|
||||||
|
| 烤鱿鱼 | 主街散布 | 铁板,酱料 | 城郊特色小吃 |
|
||||||
|
| 烤里脊 | 主街散布 | 铁板串烤 | 城郊特色小吃 |
|
||||||
| 糖葫芦 | 随机出现 | 插在草把子上,山楂/草莓/橘子 | 随机事件 |
|
| 糖葫芦 | 随机出现 | 插在草把子上,山楂/草莓/橘子 | 随机事件 |
|
||||||
| 爆米花 | 偶尔出现在主街/空地附近 | 老式黑铁葫芦爆米花机,"嘭"一声 | 随机事件,声音记忆 |
|
| 爆米花 | 偶尔出现在主街/空地附近 | 老式黑铁葫芦爆米花机,"嘭"一声 | 随机事件,声音记忆 |
|
||||||
| 棉花糖 | 偶尔出现在空地/学校附近 | 手摇式棉花糖机 | 随机事件 |
|
| 棉花糖 | 偶尔出现在空地/学校附近 | 手摇式棉花糖机 | 随机事件 |
|
||||||
@@ -396,15 +509,28 @@
|
|||||||
|---|----------|------|--------|---------|
|
|---|----------|------|--------|---------|
|
||||||
| 1 | 合并小卖部 | 8m×5m | ✅ | 经济核心、收集入口、街坊聚集 |
|
| 1 | 合并小卖部 | 8m×5m | ✅ | 经济核心、收集入口、街坊聚集 |
|
||||||
| 2 | 书店 | 5m×4m | ✅ | 漫画/书籍、叙事触点、时代氛围 |
|
| 2 | 书店 | 5m×4m | ✅ | 漫画/书籍、叙事触点、时代氛围 |
|
||||||
| 3 | 学校(仅操场) | 25m×18m | — | 肢体活动、剧情场景 |
|
| 3 | 粮油店 | ~3m×3m | 简化 | 氛围营造、城郊生活必需品 |
|
||||||
|
| 4 | 服装店/裁缝铺 | ~3m×3m | 简化 | 氛围营造、简化对话 |
|
||||||
|
| 5 | 鞋店 | ~3m×3m | 简化 | 氛围营造、运动鞋梦想 |
|
||||||
|
| 6 | 学校(仅操场) | 25m×18m | — | 肢体活动、剧情场景 |
|
||||||
|
|
||||||
**西侧(商业娱乐)**:
|
**西侧(商业娱乐)**:
|
||||||
|
|
||||||
| # | 店铺/设施 | 面积 | 可进入 | 核心功能 |
|
| # | 店铺/设施 | 面积 | 可进入 | 核心功能 |
|
||||||
|---|----------|------|--------|---------|
|
|---|----------|------|--------|---------|
|
||||||
| 4 | 街机店 | 8m×6m | ✅ | 街机小游戏、社交核心 |
|
| 7 | 街机店(含包机房后屋) | 8m×6m | ✅ | 街机小游戏、社交核心、包机房 |
|
||||||
| 5 | 网吧 | 6m×5m | ✅ | PC 小游戏、社交、时代氛围 |
|
| 8 | 网吧 | 6m×5m | ✅ | PC 小游戏、社交、时代氛围 |
|
||||||
| 6 | 录像出租点 | 5m×4m | ✅ | 录像带/磁带、叙事触点、收集 |
|
| 9 | 录像出租点 | 5m×4m | ✅ | 录像带/磁带、叙事触点、收集 |
|
||||||
|
| 10 | 储蓄所 | ~4m×3m | 简化 | 存取零花钱?时代氛围 |
|
||||||
|
| 11 | 棋牌室 | 小型 | ❌ | 纯氛围(大人的场所) |
|
||||||
|
|
||||||
|
**主街设施**:
|
||||||
|
|
||||||
|
| # | 设施 | 性质 |
|
||||||
|
|---|------|------|
|
||||||
|
| 12 | 公交站牌 | 纯装饰 |
|
||||||
|
| 13 | 废品回收点 | 路边固定摊位,可交互卖废品 |
|
||||||
|
| 14 | 街边摊贩 × N | 散布式(烤鱿鱼、烤里脊、冰棍推车、烤肠等) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -430,9 +556,15 @@
|
|||||||
- 楼道:水泥楼梯,声控灯,墙上贴着小广告和水电费通知
|
- 楼道:水泥楼梯,声控灯,墙上贴着小广告和水电费通知
|
||||||
|
|
||||||
**死党 A 的家**:
|
**死党 A 的家**:
|
||||||
- 在某栋楼的某层(不可进入,但外观有标识——比如阳台上挂着四驱车赛道模型)
|
- 在某栋楼的某层(简化室内,可进入)
|
||||||
|
- 外观有标识——比如阳台上挂着四驱车赛道模型
|
||||||
|
- 室内简化:小空间 + 对话为主,展示家境好的氛围(大电视、四驱车跑道、电脑)
|
||||||
- 从楼下能看到他家阳台上摆着的东西
|
- 从楼下能看到他家阳台上摆着的东西
|
||||||
|
|
||||||
|
**小区设施**:
|
||||||
|
- **自行车棚**:水泥柱 + 石棉瓦顶,停着几辆老式自行车(永久牌/凤凰牌)
|
||||||
|
- 信报箱区(铁皮信报箱,嵌在楼道口墙上)
|
||||||
|
|
||||||
**系统关联**:
|
**系统关联**:
|
||||||
- 四驱车系统(死党 A 在小区空地展示/比拼四驱车)
|
- 四驱车系统(死党 A 在小区空地展示/比拼四驱车)
|
||||||
- 社交场所(小区空地是另一个孩子聚集点)
|
- 社交场所(小区空地是另一个孩子聚集点)
|
||||||
@@ -498,6 +630,15 @@
|
|||||||
- 墙上贴着水电费通知、小广告
|
- 墙上贴着水电费通知、小广告
|
||||||
- 楼道里偶尔碰到邻居(背景 NPC)
|
- 楼道里偶尔碰到邻居(背景 NPC)
|
||||||
|
|
||||||
|
**女班长家**:
|
||||||
|
- 同栋楼不同层(简化室内,可进入)
|
||||||
|
- 室内简化:小空间 + 对话为主
|
||||||
|
- 女孩的房间视角,与主角家形成对照
|
||||||
|
|
||||||
|
**楼下设施**:
|
||||||
|
- **信报箱**:铁皮信报箱嵌在楼道口,可能收到通知/信件
|
||||||
|
- **自行车棚**:楼旁小型车棚(如有独立空间)
|
||||||
|
|
||||||
**系统关联**:
|
**系统关联**:
|
||||||
- FC 家用机小游戏(坐在客厅电视前玩)
|
- FC 家用机小游戏(坐在客厅电视前玩)
|
||||||
- 暑假作业叙事线(书桌上的作业)
|
- 暑假作业叙事线(书桌上的作业)
|
||||||
@@ -535,11 +676,16 @@
|
|||||||
| 修车铺 | 修自行车的,门口散落零件 | 扳手、打气筒、旧轮胎 |
|
| 修车铺 | 修自行车的,门口散落零件 | 扳手、打气筒、旧轮胎 |
|
||||||
| 小吃摊 | 下午出摊,卖凉皮、凉面 | 铁皮推车、调料瓶一字排开 |
|
| 小吃摊 | 下午出摊,卖凉皮、凉面 | 铁皮推车、调料瓶一字排开 |
|
||||||
|
|
||||||
这些小店大多不可进入(仅在门口交互),主要是氛围营造和日常事件触发。
|
这些小店大多简化交互(门口对话为主),主要是氛围营造和日常事件触发。其中早餐店可交互买东西。
|
||||||
|
|
||||||
|
**平房区设施**:
|
||||||
|
- **公共厕所**:平房区内的旱厕,城郊生活的标志性设施
|
||||||
|
- **公共水龙头**:巷子角落
|
||||||
|
|
||||||
**死党 B 的家**:
|
**死党 B 的家**:
|
||||||
- 平房区某一间(外观有标识——门口可能放着弹珠罐子或自制玩具)
|
- 平房区某一间(简化室内,可进入)
|
||||||
- 不可进入,但门口有时可以和死党 B 互动
|
- 外观有标识——门口可能放着弹珠罐子或自制玩具
|
||||||
|
- 室内简化:小空间 + 对话为主,展示温馨但简朴的氛围
|
||||||
|
|
||||||
**系统关联**:
|
**系统关联**:
|
||||||
- 对比叙事(小区 vs 平房区的物质差异)
|
- 对比叙事(小区 vs 平房区的物质差异)
|
||||||
@@ -574,11 +720,16 @@
|
|||||||
|
|
||||||
| # | 区域 | 位置 | 类型 | 核心功能 |
|
| # | 区域 | 位置 | 类型 | 核心功能 |
|
||||||
|---|------|------|------|---------|
|
|---|------|------|------|---------|
|
||||||
| 1 | 小区 | 靠东侧 | 正经住宅(3-4 栋 6 层楼 + 中庭) | 死党 A 的家、四驱车场地、对比叙事 |
|
| 1 | 小区 | 靠东侧 | 正经住宅(3-4 栋 6 层楼 + 中庭) | 死党 A 的家(简化室内)、四驱车场地、对比叙事 |
|
||||||
| 2 | 主角家 | 中部 | 事业单位家属楼(独栋 6 层) | 起点/终点、FC 游戏、叙事 |
|
| 2 | 主角家 | 中部 | 事业单位家属楼(独栋 6 层) | 起点/终点、FC 游戏、叙事 |
|
||||||
| 3 | 平房区 | 靠西侧 | 自建平房群 + 生活小店 | 死党 B 的家、弹珠场地、对比叙事 |
|
| 2b | 女班长家 | 同主角家楼 | 同栋不同层(简化室内) | 女班长家、社交 |
|
||||||
|
| 3 | 平房区 | 靠西侧 | 自建平房群 + 生活小店 | 死党 B 的家(简化室内)、弹珠场地、对比叙事 |
|
||||||
| 4 | 巷子通道 | 贯穿 | 2-3 个巷子口 + 内部连接路 | 穿行、日常事件、氛围 |
|
| 4 | 巷子通道 | 贯穿 | 2-3 个巷子口 + 内部连接路 | 穿行、日常事件、氛围 |
|
||||||
|
|
||||||
|
**小区设施**:自行车棚、信报箱
|
||||||
|
**平房区设施**:公共厕所、公共水龙头
|
||||||
|
**平房区小店**:早餐店(可交互)、理发店/修车铺/缝纫店(背景+对话)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 五、边缘区域
|
## 五、边缘区域
|
||||||
@@ -644,6 +795,24 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 5.3 秘密基地(隐藏区域)
|
||||||
|
|
||||||
|
**设计理念**:散布在地图各处的小型隐藏空间,每个都有独特的"钻进去"方式。代表童年探索的乐趣——发现只有小孩才知道的秘密角落。
|
||||||
|
|
||||||
|
| # | 名称 | 位置 | 进入方式 | 氛围 |
|
||||||
|
|---|------|------|---------|------|
|
||||||
|
| H1 | 秘密基地 A | 工地蓝色围挡后面 | 围挡底部有个洞,蹲下钻进去 | 废弃建材、涂鸦、偷藏的零食 |
|
||||||
|
| H2 | 秘密基地 B | 平房区深处的死角 | 穿过一条很窄的巷子 | 老式家具、墙角涂鸦、安静 |
|
||||||
|
| H3 | 秘密基地 C | 待定 | 待定 | 待定 |
|
||||||
|
|
||||||
|
**系统关联**:
|
||||||
|
- 探索奖励(发现秘密基地本身就是一个成就)
|
||||||
|
- 收集品散落(秘密基地里有独特收集品)
|
||||||
|
- 叙事触点(秘密基地里发现前人留下的东西、和朋友在这里约定)
|
||||||
|
- 氛围营造("这是我们的秘密"的专属感)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 六、乡下场景(奶奶家)
|
## 六、乡下场景(奶奶家)
|
||||||
|
|
||||||
### 6.1 定位与尺度
|
### 6.1 定位与尺度
|
||||||
@@ -785,20 +954,29 @@
|
|||||||
```
|
```
|
||||||
┌──────────────────────────────────┐
|
┌──────────────────────────────────┐
|
||||||
│ 大型空地/工地 河边/河堤 │
|
│ 大型空地/工地 河边/河堤 │
|
||||||
|
│ [秘密基地A] │
|
||||||
└──────────┬───────────┬───────────┘
|
└──────────┬───────────┬───────────┘
|
||||||
│ │
|
│ │
|
||||||
┌──────────┴───────────┴───────────┐
|
┌──────────┴───────────┴───────────┐
|
||||||
│ 生活区巷子 │
|
│ 生活区巷子 │
|
||||||
│ [平房区] ── [主角家] ── [小区] │
|
│ [平房区] ── [主角家+女班长家] ── [小区] │
|
||||||
|
│ [秘密基地B] │
|
||||||
|
│ [早餐店·理发店·修车铺·缝纫店] │
|
||||||
|
│ [公共厕所·自行车棚·信报箱] │
|
||||||
└──┬──────────┬──────────┬─────────┘
|
└──┬──────────┬──────────┬─────────┘
|
||||||
│巷子口① │巷子口② │巷子口③
|
│巷子口① │巷子口② │巷子口③
|
||||||
┌──┴──────────┴──────────┴──────────┐
|
┌──┴──────────┴──────────┴──────────┐
|
||||||
│ 商业主街 │
|
│ 商业主街 │
|
||||||
│ │
|
│ │
|
||||||
│ 西侧(娱乐) 东侧(生活) │
|
│ 西侧(娱乐) 东侧(生活) │
|
||||||
│ [街机店] [小卖部] │
|
│ [棋牌室] [小卖部] │
|
||||||
│ [网吧] [书店] │
|
│ [储蓄所] [粮油店] │
|
||||||
│ [录像出租] [学校] │
|
│ [录像出租] [书店] │
|
||||||
|
│ [网吧] [服装店] │
|
||||||
|
│ [街机店] [鞋店] │
|
||||||
|
│ [学校] │
|
||||||
|
│ │
|
||||||
|
│ [公交站牌] [废品回收点] [摊贩×N] │
|
||||||
└────────────────────────────────────┘
|
└────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -858,7 +1036,7 @@
|
|||||||
| **女班长** | 学校操场(跳绳/跳皮筋) | 主街、小卖部门口 | 上午活跃 |
|
| **女班长** | 学校操场(跳绳/跳皮筋) | 主街、小卖部门口 | 上午活跃 |
|
||||||
| **转学生** | 小卖部门口(初来时) | 逐渐扩展到空地、街机店 | 中后期活动范围扩大 |
|
| **转学生** | 小卖部门口(初来时) | 逐渐扩展到空地、街机店 | 中后期活动范围扩大 |
|
||||||
| **残障儿童** | 平房区巷子口(家附近) | 小卖部门口(坐着看大家玩) | 下午出现 |
|
| **残障儿童** | 平房区巷子口(家附近) | 小卖部门口(坐着看大家玩) | 下午出现 |
|
||||||
| **小卖部老板儿子** | 合并小卖部 | 街机店(偷溜去玩) | 上午在店里、下午偷溜 |
|
| **小卖部老板儿子** | 合并小卖部(后屋是家) | 街机店(偷溜去玩) | 上午在店里、下午偷溜 |
|
||||||
| **小霸王** | 街机店(格斗高手) | 空地(找人挑衅) | 下午到傍晚 |
|
| **小霸王** | 街机店(格斗高手) | 空地(找人挑衅) | 下午到傍晚 |
|
||||||
|
|
||||||
### 8.2 设计原则
|
### 8.2 设计原则
|
||||||
@@ -994,10 +1172,17 @@
|
|||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 合并小卖部 | 主街东侧 | 买零食、买文具、买四驱车配件、开卡片 |
|
| 合并小卖部 | 主街东侧 | 买零食、买文具、买四驱车配件、开卡片 |
|
||||||
| 书店 | 主街东侧 | 租借漫画/书籍 |
|
| 书店 | 主街东侧 | 租借漫画/书籍 |
|
||||||
| 街机店 | 主街西侧 | 柜台换币、投币游戏 |
|
| 粮油店 | 主街东侧 | 氛围 + 可能消费 |
|
||||||
|
| 服装店/裁缝铺 | 主街东侧 | 氛围 + 简化对话 |
|
||||||
|
| 鞋店 | 主街东侧 | 氛围 + 简化对话 |
|
||||||
|
| 街机店 | 主街西侧 | 柜台换币、投币游戏、包机房 |
|
||||||
| 网吧 | 主街西侧 | 上网收费 |
|
| 网吧 | 主街西侧 | 上网收费 |
|
||||||
| 录像出租点 | 主街西侧 | 租借录像带/磁带 |
|
| 录像出租点 | 主街西侧 | 租借录像带/磁带 |
|
||||||
|
| 储蓄所 | 主街西侧 | 存取零花钱?待定 |
|
||||||
|
| 废品回收点 | 主街路边 | 卖废品换钱 |
|
||||||
|
| 早餐店 | 平房区 | 买早点(包子油条豆浆) |
|
||||||
| 街边摊贩 | 主街 + 巷子 | 买冰棍、买小吃(比店铺便宜) |
|
| 街边摊贩 | 主街 + 巷子 | 买冰棍、买小吃(比店铺便宜) |
|
||||||
|
| 流动收废品车 | 巷子巡游 | 卖废品换钱(流动价格) |
|
||||||
| 村口小卖部 | 乡下 | 乡下唯一消费点 |
|
| 村口小卖部 | 乡下 | 乡下唯一消费点 |
|
||||||
|
|
||||||
### 10.4 叙事系统 × 场景触发
|
### 10.4 叙事系统 × 场景触发
|
||||||
@@ -1086,9 +1271,9 @@
|
|||||||
| **v0.2** | NPC 放置 + 弹珠场地标识 + NPC 常驻点标识 |
|
| **v0.2** | NPC 放置 + 弹珠场地标识 + NPC 常驻点标识 |
|
||||||
| **v0.3** | 时段光影 + 天气初步 + 学校操场 |
|
| **v0.3** | 时段光影 + 天气初步 + 学校操场 |
|
||||||
| **v0.4** | 街机店室内 + 录像出租点室内 + 叙事触发点布设 + 小区外观 |
|
| **v0.4** | 街机店室内 + 录像出租点室内 + 叙事触发点布设 + 小区外观 |
|
||||||
| **v0.5** | 网吧室内 + 书店室内 + 平房区 + 生活小店 + 工地围挡 + 摊贩体系 |
|
| **v0.5** | 网吧室内 + 书店室内 + 粮油店/服装店/鞋店 + 储蓄所 + 平房区 + 生活小店 + 工地围挡 + 摊贩体系 + 废品回收点 |
|
||||||
| **v0.6** | **乡下场景全部**(奶奶家、小溪、田间路、村口小卖部) |
|
| **v0.6** | **乡下场景全部**(奶奶家、小溪、田间路、村口小卖部) |
|
||||||
| **v0.7** | 全场景整合 + 城乡切换 + 细节打磨 |
|
| **v0.7** | 全场景整合 + 城乡切换 + 4 家小伙伴家(简化室内)+ 秘密基地 + 棋牌室 + 公交站牌 + 细节打磨 |
|
||||||
| **v1.0** | 氛围精修(声音景观、天气效果、光影优化) |
|
| **v1.0** | 氛围精修(声音景观、天气效果、光影优化) |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -1100,7 +1285,13 @@
|
|||||||
- [ ] 乡下阶段的 NPC 具体设计(村里有几个人?什么性格?)
|
- [ ] 乡下阶段的 NPC 具体设计(村里有几个人?什么性格?)
|
||||||
- [ ] 天气系统的详细设计(v0.3 前确定范围)
|
- [ ] 天气系统的详细设计(v0.3 前确定范围)
|
||||||
- [ ] 室内场景的光照方案(自然光 vs 灯光)
|
- [ ] 室内场景的光照方案(自然光 vs 灯光)
|
||||||
- [ ] 平房区生活小店哪些可以进入、哪些仅门口交互
|
|
||||||
- [ ] 网吧的 PC 小游戏内容和家里 FC 的体验差异如何区分
|
- [ ] 网吧的 PC 小游戏内容和家里 FC 的体验差异如何区分
|
||||||
- [ ] 街边摊贩的出现逻辑(固定位置+固定时段?还是随机事件?)
|
- [ ] 街边摊贩的出现逻辑(固定位置+固定时段?还是随机事件?)
|
||||||
- [ ] 主街东西两侧是否有视觉上的渐变差异(东侧更传统朴素、西侧更鲜亮热闹?)
|
- [ ] 主街东西两侧是否有视觉上的渐变差异(东侧更传统朴素、西侧更鲜亮热闹?)
|
||||||
|
- [x] ~~平房区生活小店哪些可以进入、哪些仅门口交互~~ → 早餐店可交互,其他简化对话
|
||||||
|
- [x] ~~小伙伴家是否可进入~~ → 4 家可进入(全部简化室内):死党A家、死党B家、老板儿子家(小卖部后屋)、女班长家(主角家同楼)
|
||||||
|
- [x] ~~包机房安排~~ → 合并进街机店后屋
|
||||||
|
- [x] ~~废品回收点~~ → 主街固定摊位 + 流动收废品车
|
||||||
|
- [ ] 秘密基地 C 的具体位置和进入方式
|
||||||
|
- [ ] 储蓄所是否真正参与零花钱系统(还是纯氛围)
|
||||||
|
- [ ] 服装店/鞋店是否有实际交互(买衣服/换装?还是纯氛围)
|
||||||
|
|||||||
@@ -0,0 +1,281 @@
|
|||||||
|
# 头脑风暴:灵感补充 + 城郊建筑与区域规划
|
||||||
|
|
||||||
|
**日期**:2026-06-10
|
||||||
|
**参与**:starmen1265 × 策划 Agent
|
||||||
|
**状态**:✅ 方向确认完毕
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 讨论一:灵感列表对比与新发现
|
||||||
|
|
||||||
|
用户提供了一份童年记忆灵感列表,对照已有三份头脑风暴记录进行逐一对比。
|
||||||
|
|
||||||
|
### 已有记录(不重复)
|
||||||
|
|
||||||
|
抓虫(乡下抓知了)、跳皮筋、呼啦圈、跳房子、石头剪刀布、警察抓小偷、一二三木头人、天线调台、日立电视、收音机、ADSL拨号上网、排球女将、大哥大、BP机、广播体操、广场舞、废旧报纸卖废品、少年包青天、菜地(迈皋桥地理元素)
|
||||||
|
|
||||||
|
### ⭐ 新发现元素
|
||||||
|
|
||||||
|
#### 乡下乡土活动(奶奶家阶段素材)
|
||||||
|
|
||||||
|
| # | 元素 | 灵感价值 |
|
||||||
|
|---|------|---------|
|
||||||
|
| 1 | 钓鱼(鱼竿,区别于"摸鱼") | ⭐⭐ 乡村独立小游戏潜力 |
|
||||||
|
| 2 | 游泳(夏天河里/水塘) | ⭐⭐ 场景事件+氛围 |
|
||||||
|
| 3 | 挤牛奶 | ⭐ 互动体验 |
|
||||||
|
| 4 | 喂鸡、掏鸡蛋 | ⭐⭐⭐ 高辨识度、互动感强 |
|
||||||
|
| 5 | 菜地浇水 | ⭐ 零花钱/好感度任务 |
|
||||||
|
| 6 | 打水漂 | ⭐⭐ 简单小互动,有物理乐趣 |
|
||||||
|
|
||||||
|
#### 小孩日常小游戏
|
||||||
|
|
||||||
|
| # | 元素 | 灵感价值 |
|
||||||
|
|---|------|---------|
|
||||||
|
| 7 | 双人拍手小游戏(互推游戏) | ⭐⭐ 简单但互动感极强 |
|
||||||
|
| 8 | 钻狗洞的秘密基地 | ⭐⭐⭐ **极佳的探索叙事素材** |
|
||||||
|
| 9 | 猜在哪个手里 | ⭐ 最简单的NPC互动小游戏 |
|
||||||
|
| 10 | 叠纸飞机(不同纸、不同技法) | ⭐⭐⭐ 收集+小游戏双潜力 |
|
||||||
|
| 11 | 跑步比赛 | ⭐ 简单的NPC互动触发 |
|
||||||
|
|
||||||
|
#### 人物与社交
|
||||||
|
|
||||||
|
| # | 元素 | 灵感价值 |
|
||||||
|
|---|------|---------|
|
||||||
|
| 12 | 爷爷奶奶家的各种年龄段亲戚小孩 | ⭐⭐⭐ 乡下NPC角色体系 |
|
||||||
|
| 13 | 北方来南方的同学 | ⭐⭐⭐ 与"转学生"角色互补 |
|
||||||
|
| 14 | 等着去别人家(串门文化) | ⭐⭐ 日常叙事素材 |
|
||||||
|
| 15 | 不知道名字的小哥哥跟你玩了一下午 | ⭐⭐⭐ **极高情感潜力的微型叙事线** |
|
||||||
|
|
||||||
|
#### 食物与零食
|
||||||
|
|
||||||
|
| # | 元素 | 灵感价值 |
|
||||||
|
|---|------|---------|
|
||||||
|
| 16 | 大大泡泡糖 | ⭐⭐ 品牌辨识度极高 |
|
||||||
|
| 17 | 秀逗(超酸的糖) | ⭐⭐ 互动素材——表情反应 |
|
||||||
|
| 18 | 无花果(丝) | ⭐ 零花钱消费场景填充 |
|
||||||
|
| 19 | 老鼠屎(盐津葡萄/话梅粒) | ⭐⭐ 名字本身就是小孩文化缩影 |
|
||||||
|
| 20 | 跳跳糖 | ⭐⭐ 视觉效果潜力 |
|
||||||
|
| 21 | 烤鱿鱼 | ⭐ 街边饮食扩展 |
|
||||||
|
| 22 | 烤里脊 | ⭐ 街边饮食扩展 |
|
||||||
|
| 23 | 炒饭和速冻水饺 | ⭐⭐ 与体力系统直接联动 |
|
||||||
|
|
||||||
|
#### 日常生活与物品
|
||||||
|
|
||||||
|
| # | 元素 | 灵感价值 |
|
||||||
|
|---|------|---------|
|
||||||
|
| 24 | 每天都有吃饭聊天时间 | ⭐⭐ 氛围事件+叙事窗口 |
|
||||||
|
| 25 | 小天鹅(洗衣机品牌) | ⭐ 时代品牌元素 |
|
||||||
|
| 26 | MP3(2000年前后刚开始出现) | ⭐⭐ 时代物品——比随身听更"潮" |
|
||||||
|
| 27 | 装修(邻居家在装修) | ⭐⭐ 场景细节(讨论后决定不做) |
|
||||||
|
| 28 | 墙上涂鸦 | ⭐ 收集/互动素材 |
|
||||||
|
| 29 | 十二生肖 | 待确认具体指什么 |
|
||||||
|
| 30 | 吃遍附近的美食 | ⭐⭐ 探索型目标 |
|
||||||
|
| 31 | 纸壳 | ⭐ 与叠纸飞机联动 |
|
||||||
|
| 32 | 西游记后传(2000年播出) | ⭐⭐ 电视内容补充 |
|
||||||
|
| 33 | 玉河摩托 | 待确认具体指什么 |
|
||||||
|
| 34 | 公交 | ⭐ 场景/移动手段 |
|
||||||
|
|
||||||
|
#### 游戏机制灵感
|
||||||
|
|
||||||
|
**体力系统雏形**:
|
||||||
|
> 吃饭补充体力,饭点之前体力无限。如果不吃饭,体力没跑几步就没了。
|
||||||
|
|
||||||
|
**动态家庭事件链**:
|
||||||
|
> 爸爸妈妈早上会给留两顿炒饭 → 如果都不吃,连续三天,会换成速冻水饺 → 如果还不吃,每天会多给两顿饭钱。
|
||||||
|
|
||||||
|
这两个设计同时做到了:还原真实 × 驱动行为 × 叙事表达。
|
||||||
|
|
||||||
|
#### 重点标记 ⭐⭐⭐
|
||||||
|
|
||||||
|
1. **钻狗洞的秘密基地** — 探索叙事
|
||||||
|
2. **叠纸飞机** — 收集+小游戏
|
||||||
|
3. **爷爷奶奶家的亲戚小孩** — 乡下NPC体系
|
||||||
|
4. **不知道名字的小哥哥** — 情感微型叙事
|
||||||
|
5. **体力系统+家庭动态事件** — 核心玩法机制
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 讨论二:城郊区域建筑与区域规划
|
||||||
|
|
||||||
|
基于全部已记录的活动/事件,讨论城市主场景应有哪些建筑与区域(不含乡下)。
|
||||||
|
|
||||||
|
### 确认的变更(相比现有场景设计)
|
||||||
|
|
||||||
|
| # | 变更项 | 原设计 | 新决定 |
|
||||||
|
|---|--------|--------|--------|
|
||||||
|
| 1 | 小伙伴家 | 全部不可进入 | 4 家可进入(简化室内) |
|
||||||
|
| 2 | 包机房 | 无 | 合并进街机店后屋 |
|
||||||
|
| 3 | 废品回收 | 无 | 固定点 + 流动收废品车 |
|
||||||
|
| 4 | 公交站 | 无 | 主街公交站牌(纯装饰) |
|
||||||
|
| 5 | 秘密基地 | 无 | 多处散布 |
|
||||||
|
| 6 | 棋牌室 | 无 | 新增 |
|
||||||
|
| 7 | 储蓄所 | 无 | 新增 |
|
||||||
|
| 8 | 菜地 | 无 | 不做(留乡下) |
|
||||||
|
| 9 | 装修 | 无 | 不做 |
|
||||||
|
| 10 | 游泳 | 无 | 河边不能下水 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 完整建筑与区域清单
|
||||||
|
|
||||||
|
#### 一、商业主街(东西走向,~70m x 5-6m)
|
||||||
|
|
||||||
|
**东侧(传统生活)**
|
||||||
|
|
||||||
|
| # | 名称 | 尺寸 | 可进入 | 核心功能 | 备注 |
|
||||||
|
|---|------|------|--------|---------|------|
|
||||||
|
| S1 | 合并小卖部 | 8m x 5m | ✅ | 零食/文具/四驱配件/经济核心 | 后半部分是老板儿子家(简化室内) |
|
||||||
|
| S2 | 书店 | 5m x 4m | ✅ | 漫画/书籍租借 | |
|
||||||
|
| S3 | 粮油店 | 小型 | ✅ | 米面油酱醋 | |
|
||||||
|
| S4 | 服装店/裁缝铺 | 小型 | 简化 | 氛围 + 可能买衣服 | |
|
||||||
|
| S5 | 鞋店 | 小型 | 简化 | 氛围 + 运动鞋梦想 | |
|
||||||
|
|
||||||
|
**西侧(娱乐商业)**
|
||||||
|
|
||||||
|
| # | 名称 | 尺寸 | 可进入 | 核心功能 | 备注 |
|
||||||
|
|---|------|------|--------|---------|------|
|
||||||
|
| S6 | 街机店 | 8m x 6m | ✅ | 街机小游戏 + 社交 | 含包机房后屋(门帘隔着) |
|
||||||
|
| S7 | 网吧 | 6m x 5m | ✅ | PC 小游戏 + 社交 | |
|
||||||
|
| S8 | 录像出租点 | 5m x 4m | ✅ | 录像带/磁带租借 | 只出租,无观影厅 |
|
||||||
|
| S9 | 储蓄所 | 小型 | 简化 | 存取零花钱? | |
|
||||||
|
| S10 | 棋牌室 | 小型 | ❌ | 纯氛围(大人的场所) | |
|
||||||
|
|
||||||
|
**主街设施(散布)**
|
||||||
|
|
||||||
|
| # | 名称 | 性质 |
|
||||||
|
|---|------|------|
|
||||||
|
| S11 | 公交站牌 | 纯装饰,公交经过不可乘坐 |
|
||||||
|
| S12 | 废品回收点 | 路边固定摊位,可交互卖废品 |
|
||||||
|
| S13 | 街边摊贩 × N | 散布式(烤鱿鱼、烤里脊、冰棍推车、烤肠等) |
|
||||||
|
|
||||||
|
**主街尽端**
|
||||||
|
|
||||||
|
| # | 名称 | 尺寸 | 可进入 |
|
||||||
|
|---|------|------|--------|
|
||||||
|
| S14 | 学校操场 | 25m x 18m | 仅室外 |
|
||||||
|
|
||||||
|
#### 二、生活区巷子(主街北侧,南北纵深)
|
||||||
|
|
||||||
|
**中部 — 事业单位家属楼**
|
||||||
|
|
||||||
|
| # | 名称 | 性质 | 备注 |
|
||||||
|
|---|------|------|------|
|
||||||
|
| R1 | 主角家 | 6 层家属楼,完整室内(两室一厅) | 起点终点、FC 游戏、电视、电话 |
|
||||||
|
| R2 | 女班长家 | 同栋楼,简化室内 | 同楼不同层 |
|
||||||
|
| R3 | 信报箱 | 楼下设施 | 氛围 + 可能收信/通知 |
|
||||||
|
|
||||||
|
**东侧 — 小区**
|
||||||
|
|
||||||
|
| # | 名称 | 性质 | 备注 |
|
||||||
|
|---|------|------|------|
|
||||||
|
| R4 | 小区 | 3-4 栋 6 层楼 + 中庭空地 | 死党 A 住这里 |
|
||||||
|
| R5 | 死党 A 家 | 小区内某栋,简化室内 | 家境好,有四驱车跑道 |
|
||||||
|
| R6 | 自行车棚 | 小区内设施 | 氛围 |
|
||||||
|
|
||||||
|
**西侧 — 平房区**
|
||||||
|
|
||||||
|
| # | 名称 | 性质 | 可交互 | 备注 |
|
||||||
|
|---|------|------|--------|------|
|
||||||
|
| R7 | 平房区 | 十几二十间平房 + 窄巷子 | — | 死党 B 住这里 |
|
||||||
|
| R8 | 死党 B 家 | 平房区内某间,简化室内 | ✅ | 家境差,温馨 |
|
||||||
|
| R9 | 早餐店 | 平房区沿街 | ✅ 买早点 | 包子油条豆浆 |
|
||||||
|
| R10 | 理发店 | 平房区小店 | 简化对话 | 老式转椅+镜子 |
|
||||||
|
| R11 | 修车铺 | 平房区小店 | 简化对话 | 门口堆轮胎零件 |
|
||||||
|
| R12 | 缝纫店 | 平房区小店 | 简化对话 | 缝纫机响 |
|
||||||
|
| R13 | 公共厕所 | 平房区内 | ❌ | 城郊生活标志 |
|
||||||
|
|
||||||
|
**巷子通道**
|
||||||
|
|
||||||
|
| # | 名称 | 备注 |
|
||||||
|
|---|------|------|
|
||||||
|
| R14 | 巷子口 × 2-3 | 主街与生活区的连接口 |
|
||||||
|
|
||||||
|
#### 三、边缘区域(北端)
|
||||||
|
|
||||||
|
| # | 名称 | 尺寸 | 备注 |
|
||||||
|
|---|------|------|------|
|
||||||
|
| E1 | 大型空地/工地 | ~40m x 30m | 一半空地(弹珠主战场),一半工地(围挡) |
|
||||||
|
| E2 | 河边/河堤 | ~50m x 8m | 不能下水,氛围+打水漂 |
|
||||||
|
|
||||||
|
#### 四、隐藏区域
|
||||||
|
|
||||||
|
| # | 名称 | 位置 | 进入方式 |
|
||||||
|
|---|------|------|---------|
|
||||||
|
| H1 | 秘密基地 A | 工地围挡后面 | 钻洞 |
|
||||||
|
| H2 | 秘密基地 B | 平房区死角 | 穿窄巷 |
|
||||||
|
| H3 | 秘密基地 C | 待定 | 待定 |
|
||||||
|
|
||||||
|
#### 五、移动元素
|
||||||
|
|
||||||
|
| # | 名称 | 路线 | 功能 |
|
||||||
|
|---|------|------|------|
|
||||||
|
| M1 | 流动收废品车 | 巷子内巡游 | 卖废品 |
|
||||||
|
| M2 | 街边摊贩 | 主街散布 | 零食/小吃 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 主街店铺分布(东西排列)
|
||||||
|
|
||||||
|
```
|
||||||
|
← 东(生活) 西(娱乐)→
|
||||||
|
┌──────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ [学校操场] [小卖部] [粮油店] [书店] [服装店] [鞋店] · 公交站 · [棋牌室] [储蓄所] [录像出租] [网吧] [街机店] │
|
||||||
|
│ ↑ 废品回收点(路边) ↑ 摊贩散布 │
|
||||||
|
└──────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 可进入室内完整清单
|
||||||
|
|
||||||
|
| # | 地点 | 规模 | 关联 NPC |
|
||||||
|
|---|------|------|---------|
|
||||||
|
| 1 | 主角家 | 完整(两室一厅) | — |
|
||||||
|
| 2 | 合并小卖部 | 完整 | 老板儿子 |
|
||||||
|
| 3 | 书店 | 中等 | — |
|
||||||
|
| 4 | 街机店(含包机房后屋) | 中等 | 小霸王 |
|
||||||
|
| 5 | 网吧 | 中等 | — |
|
||||||
|
| 6 | 录像出租点 | 中等 | — |
|
||||||
|
| 7 | 死党 A 家 | 简化(小空间+对话) | 死党 A |
|
||||||
|
| 8 | 死党 B 家 | 简化 | 死党 B |
|
||||||
|
| 9 | 老板儿子家 | 简化(小卖部后屋) | 老板儿子 |
|
||||||
|
| 10 | 女班长家 | 简化(主角家同楼) | 女班长 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 讨论共识记录
|
||||||
|
|
||||||
|
1. **菜地**:城市场景不做,留给乡下阶段
|
||||||
|
2. **秘密基地**:多处散布,小型空间(工地围挡后、平房区死角、至少 2-3 处)
|
||||||
|
3. **小伙伴家**:4 家可进入,全部简化室内(小空间 + 对话为主)
|
||||||
|
- 死党 A 家 → 小区内
|
||||||
|
- 死党 B 家 → 平房区内
|
||||||
|
- 老板儿子家 → 小卖部后屋
|
||||||
|
- 女班长家 → 主角家同楼不同层
|
||||||
|
4. **录像厅**:只做出租点,不做观影厅
|
||||||
|
5. **包机房**:合并进街机店后屋
|
||||||
|
6. **废品回收**:主街固定摊位 + 流动收废品车(巷子巡游)
|
||||||
|
7. **装修**:不做
|
||||||
|
8. **公交站**:纯装饰站牌
|
||||||
|
9. **摊贩**:散布式
|
||||||
|
10. **游泳**:河边不能下水
|
||||||
|
11. **新增店铺**:粮油店、服装店/裁缝铺、鞋店 → 都放主街
|
||||||
|
12. **新增设施**:棋牌室(大人场所/纯氛围)、储蓄所、公共厕所、自行车棚、信报箱
|
||||||
|
13. **平房区小店**:早餐店(可交互)、理发店/修车铺/缝纫店(背景+对话)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 统计
|
||||||
|
|
||||||
|
| 类别 | 数量 |
|
||||||
|
|------|------|
|
||||||
|
| 商业主街店铺/设施 | 14 个 |
|
||||||
|
| 生活区建筑/设施 | 14 个 |
|
||||||
|
| 边缘区域 | 2 个 |
|
||||||
|
| 隐藏区域 | 多处 |
|
||||||
|
| 移动元素 | 2 类 |
|
||||||
|
| **可进入室内** | ~12 个 |
|
||||||
|
|
||||||
|
### 与概念草案的关系
|
||||||
|
|
||||||
|
概念草案定义「可进入室内:主角家 + 4 家店铺」。新设计变更为 6 家店铺 + 4 家小伙伴家(简化)+ 主角家 = ~12 个可进入室内。这是扩展,不与概念草案冲突。概念草案冻结,变更记录在系统设计案中。
|
||||||
@@ -0,0 +1,607 @@
|
|||||||
|
# v0.1 白模搭建操作指南 — 城郊小街
|
||||||
|
|
||||||
|
> 本文档由 execution-to-guide 工具自动生成,供 level-builder 搭建白模关卡使用。
|
||||||
|
> 基于: 关卡-v0.1-城郊小街-执行案.md
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 〇、全局信息
|
||||||
|
|
||||||
|
### 场景参数
|
||||||
|
|
||||||
|
| 参数 | 值 |
|
||||||
|
|------|-----|
|
||||||
|
| 场景总尺寸 | 8000cm × 6000cm(80m × 60m) |
|
||||||
|
| 地面高度基准 | Z = 0 |
|
||||||
|
| 主角身高 | ~120cm |
|
||||||
|
| 摄像机高度 | ~100cm |
|
||||||
|
| 时间设定 | 夏日午后 15:00 |
|
||||||
|
| 坐标原点 | 场景西南角地平面 |
|
||||||
|
|
||||||
|
### UE5 路径前缀
|
||||||
|
|
||||||
|
- **Mesh**: `Content/BlockingStarterPack/Meshes/`
|
||||||
|
- **Material**: `Content/BlockingStarterPack/Materials/`
|
||||||
|
- **Material (Basics)**: `Content/BlockingStarterPack/Materials/Blocking/Basics/`
|
||||||
|
|
||||||
|
### 命名规范
|
||||||
|
|
||||||
|
```
|
||||||
|
LVL_<ZoneID>_<Category>_<Description>
|
||||||
|
Category: STR | PROP | COMP | BND | LGT
|
||||||
|
```
|
||||||
|
|
||||||
|
### Outliner 文件夹结构
|
||||||
|
|
||||||
|
```
|
||||||
|
LVL_城郊小街/
|
||||||
|
Z1_家/
|
||||||
|
Structure/ Props/
|
||||||
|
Z2_小卖部/
|
||||||
|
Structure/ Props/
|
||||||
|
Z3_主街道/
|
||||||
|
Structure/ Props/
|
||||||
|
Z4_空地/
|
||||||
|
Structure/ Props/
|
||||||
|
Z5_巷子/
|
||||||
|
Structure/ Props/
|
||||||
|
Z6_水塘边/
|
||||||
|
Structure/ Props/
|
||||||
|
Z7_老居民楼/
|
||||||
|
Structure/ Props/
|
||||||
|
Global/
|
||||||
|
Boundaries/ Lighting/ PlayerStart/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、BSP 资产映射表
|
||||||
|
|
||||||
|
### 建筑 — 墙体
|
||||||
|
|
||||||
|
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||||
|
|-------------|-------------|--------------|
|
||||||
|
| Wall_400x300 | `Architecture/Walls/SM_Wall_400x300` | 400×20×300 |
|
||||||
|
| Wall_200x300 | `Architecture/Walls/SM_Wall_200x300` | 200×20×300 |
|
||||||
|
| Wall_400x100 | `Architecture/Walls/SM_Wall_400x100` | 400×20×100 |
|
||||||
|
| Wall_100x300 | `Architecture/Walls/SM_Wall_100x300` | 100×20×300 |
|
||||||
|
|
||||||
|
### 建筑 — 门墙
|
||||||
|
|
||||||
|
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||||
|
|-------------|-------------|--------------|
|
||||||
|
| WallDoor_200x300 | `Architecture/Walls/SM_WallDoor_200x300` | 200×20×300 |
|
||||||
|
|
||||||
|
### 建筑 — 窗墙
|
||||||
|
|
||||||
|
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||||
|
|-------------|-------------|--------------|
|
||||||
|
| WallWindow_400x300 | `Architecture/Walls/SM_WallWindow_400x300` | 400×20×300 |
|
||||||
|
| WallWindowSmall_200x300 | `Architecture/Walls/SM_WallWindowSmall_200x300` | 200×20×300 |
|
||||||
|
|
||||||
|
### 建筑 — 地板
|
||||||
|
|
||||||
|
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||||
|
|-------------|-------------|--------------|
|
||||||
|
| Floor_300x400 | `Architecture/Floors/SM_Floor_300x400` | 300×400×22 |
|
||||||
|
| Floor_1000x1000 | `Architecture/Floors/SM_Floor_1000x1000` | 1000×1000×22 |
|
||||||
|
| Floor_200x300 | `Architecture/Floors/SM_Floor_200x300` | 200×300×22 |
|
||||||
|
|
||||||
|
### 建筑 — 窗户
|
||||||
|
|
||||||
|
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||||
|
|-------------|-------------|--------------|
|
||||||
|
| FakeWindowsFrame | `Architecture/WindowsSet/SM_FakeWindowsFrame` | 140×14×164 |
|
||||||
|
|
||||||
|
### 建筑 — 围挡
|
||||||
|
|
||||||
|
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||||
|
|-------------|-------------|--------------|
|
||||||
|
| BarrierWall_600 | `Architecture/Barriers/SM_BarrierWall_600` | 600×60×210 |
|
||||||
|
| BarrierWall_200 | `Architecture/Barriers/SM_BarrierWall_200` | 200×60×210 |
|
||||||
|
|
||||||
|
### 建筑 — 围栏
|
||||||
|
|
||||||
|
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||||
|
|-------------|-------------|--------------|
|
||||||
|
| Fence_Small_400 | `Industrials/SM_Fence_Small_400` | 404×6×112 |
|
||||||
|
|
||||||
|
### 建筑 — 横梁
|
||||||
|
|
||||||
|
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||||
|
|-------------|-------------|--------------|
|
||||||
|
| Beam_300 | `Architecture/Beams/SM_Beam_300` | 40×40×300 |
|
||||||
|
|
||||||
|
### 建筑 — 大型建筑
|
||||||
|
|
||||||
|
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||||
|
|-------------|-------------|--------------|
|
||||||
|
| Building_01 | `BigBuildings/SM_Building_01` | 1950×1450×4010 |
|
||||||
|
|
||||||
|
### 家具
|
||||||
|
|
||||||
|
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||||
|
|-------------|-------------|--------------|
|
||||||
|
| Monitor_01 | `Furnitures/SM_Monitor_01` | 54×18×37 |
|
||||||
|
| SimpleTable_01 | `Furnitures/SM_SimpleTable_01` | 161×87×74 |
|
||||||
|
| BedSingle | `Bedroom/SM_BedSingle` | 116×216×68 |
|
||||||
|
| SimpleTable_02 | `Furnitures/SM_SimpleTable_02` | 239×99×74 |
|
||||||
|
|
||||||
|
### 工业/装饰
|
||||||
|
|
||||||
|
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||||
|
|-------------|-------------|--------------|
|
||||||
|
| WoodenBox_Small | `Industrials/SM_WoodenBox_Small` | 70×70×70 |
|
||||||
|
| Bucket_01 | `Decorations/SM_Bucket_01` | 30×27×26 |
|
||||||
|
| Barrel | `Industrials/SM_Barrel` | 73×73×111 |
|
||||||
|
| TrashCans_01 | `Trashs/SM_TrashCans_01` | 53×71×118 |
|
||||||
|
| WoodenPallet_01 | `Industrials/SM_WoodenPallet_01` | 188×188×20 |
|
||||||
|
| WoodenBox_Normal | `Industrials/SM_WoodenBox_Normal` | 90×90×90 |
|
||||||
|
|
||||||
|
### 载具
|
||||||
|
|
||||||
|
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||||
|
|-------------|-------------|--------------|
|
||||||
|
| Car01 | `Vehicles/SM_Car01` | 160×420×180 |
|
||||||
|
|
||||||
|
### 植被
|
||||||
|
|
||||||
|
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||||
|
|-------------|-------------|--------------|
|
||||||
|
| Tree_01 | `Vegetations/SM_Tree_01` | 762×803×845 |
|
||||||
|
|
||||||
|
### 灯具
|
||||||
|
|
||||||
|
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||||
|
|-------------|-------------|--------------|
|
||||||
|
| RoadLamp_01 | `Lights/SM_RoadLamp_01` | 40×244×850 |
|
||||||
|
|
||||||
|
### 装饰
|
||||||
|
|
||||||
|
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||||
|
|-------------|-------------|--------------|
|
||||||
|
| DisplayPanels_400x200 | `DisplayPanels/SM_DisplayPanels_400x200` | 400×8×200 |
|
||||||
|
|
||||||
|
### 其他
|
||||||
|
|
||||||
|
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||||
|
|-------------|-------------|--------------|
|
||||||
|
| WallDoorGarage_400x400 | `` | 0×0×0 |
|
||||||
|
| _composite | `` | 0×0×0 |
|
||||||
|
| Wheel | `` | 0×0×0 |
|
||||||
|
| MI_DevGlassWhite | `` | 0×0×0 |
|
||||||
|
| MI_DevGray | `` | 0×0×0 |
|
||||||
|
| MI_DevWhite | `` | 0×0×0 |
|
||||||
|
| MI_DevBlue | `` | 0×0×0 |
|
||||||
|
| MI_DevYellow | `` | 0×0×0 |
|
||||||
|
| MI_DevGlassBlue | `` | 0×0×0 |
|
||||||
|
| MI_DevFoliage | `` | 0×0×0 |
|
||||||
|
| MI_DevGreen | `` | 0×0×0 |
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、Phase B — 地面与边界
|
||||||
|
|
||||||
|
### B1. 铺设地面
|
||||||
|
|
||||||
|
#### 全局地面(MI_DevGray)
|
||||||
|
|
||||||
|
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 缩放 | 材质 |
|
||||||
|
|---|-----------|------|-------------|------|------|
|
||||||
|
| 1 | LVL_Global_STR_GroundFloor_01 | Floor_1000x1000 | (500,500,0) | — | MI_DevGray |
|
||||||
|
| 2 | LVL_Global_STR_GroundFloor_02 | Floor_1000x1000 | (500,1500,0) | — | MI_DevGray |
|
||||||
|
| 3 | LVL_Global_STR_GroundFloor_03 | Floor_1000x1000 | (500,2500,0) | — | MI_DevGray |
|
||||||
|
| 4 | LVL_Global_STR_GroundFloor_04 | Floor_1000x1000 | (500,3500,0) | — | MI_DevGray |
|
||||||
|
| 5 | LVL_Global_STR_GroundFloor_05 | Floor_1000x1000 | (500,4500,0) | — | MI_DevGray |
|
||||||
|
| 6 | LVL_Global_STR_GroundFloor_06 | Floor_1000x1000 | (500,5500,0) | — | MI_DevGray |
|
||||||
|
| 7 | LVL_Global_STR_GroundFloor_07 | Floor_1000x1000 | (1500,500,0) | — | MI_DevGray |
|
||||||
|
| 8 | LVL_Global_STR_GroundFloor_08 | Floor_1000x1000 | (1500,1500,0) | — | MI_DevGray |
|
||||||
|
| 9 | LVL_Global_STR_GroundFloor_09 | Floor_1000x1000 | (1500,2500,0) | — | MI_DevGray |
|
||||||
|
| 10 | LVL_Global_STR_GroundFloor_10 | Floor_1000x1000 | (1500,3500,0) | — | MI_DevGray |
|
||||||
|
| 11 | LVL_Global_STR_GroundFloor_11 | Floor_1000x1000 | (1500,4500,0) | — | MI_DevGray |
|
||||||
|
| 12 | LVL_Global_STR_GroundFloor_12 | Floor_1000x1000 | (1500,5500,0) | — | MI_DevGray |
|
||||||
|
| 13 | LVL_Global_STR_GroundFloor_13 | Floor_1000x1000 | (2500,500,0) | — | MI_DevGray |
|
||||||
|
| 14 | LVL_Global_STR_GroundFloor_14 | Floor_1000x1000 | (2500,1500,0) | — | MI_DevGray |
|
||||||
|
| 15 | LVL_Global_STR_GroundFloor_15 | Floor_1000x1000 | (2500,2500,0) | — | MI_DevGray |
|
||||||
|
| 16 | LVL_Global_STR_GroundFloor_16 | Floor_1000x1000 | (2500,3500,0) | — | MI_DevGray |
|
||||||
|
| 17 | LVL_Global_STR_GroundFloor_17 | Floor_1000x1000 | (2500,4500,0) | — | MI_DevGray |
|
||||||
|
| 18 | LVL_Global_STR_GroundFloor_18 | Floor_1000x1000 | (2500,5500,0) | — | MI_DevGray |
|
||||||
|
| 19 | LVL_Global_STR_GroundFloor_19 | Floor_1000x1000 | (3500,500,0) | — | MI_DevGray |
|
||||||
|
| 20 | LVL_Global_STR_GroundFloor_20 | Floor_1000x1000 | (3500,1500,0) | — | MI_DevGray |
|
||||||
|
| 21 | LVL_Global_STR_GroundFloor_21 | Floor_1000x1000 | (3500,2500,0) | — | MI_DevGray |
|
||||||
|
| 22 | LVL_Global_STR_GroundFloor_22 | Floor_1000x1000 | (3500,3500,0) | — | MI_DevGray |
|
||||||
|
| 23 | LVL_Global_STR_GroundFloor_23 | Floor_1000x1000 | (3500,4500,0) | — | MI_DevGray |
|
||||||
|
| 24 | LVL_Global_STR_GroundFloor_24 | Floor_1000x1000 | (3500,5500,0) | — | MI_DevGray |
|
||||||
|
| 25 | LVL_Global_STR_GroundFloor_25 | Floor_1000x1000 | (4500,500,0) | — | MI_DevGray |
|
||||||
|
| 26 | LVL_Global_STR_GroundFloor_26 | Floor_1000x1000 | (4500,1500,0) | — | MI_DevGray |
|
||||||
|
| 27 | LVL_Global_STR_GroundFloor_27 | Floor_1000x1000 | (4500,2500,0) | — | MI_DevGray |
|
||||||
|
| 28 | LVL_Global_STR_GroundFloor_28 | Floor_1000x1000 | (4500,3500,0) | — | MI_DevGray |
|
||||||
|
| 29 | LVL_Global_STR_GroundFloor_29 | Floor_1000x1000 | (4500,4500,0) | — | MI_DevGray |
|
||||||
|
| 30 | LVL_Global_STR_GroundFloor_30 | Floor_1000x1000 | (4500,5500,0) | — | MI_DevGray |
|
||||||
|
| 31 | LVL_Global_STR_GroundFloor_31 | Floor_1000x1000 | (5500,500,0) | — | MI_DevGray |
|
||||||
|
| 32 | LVL_Global_STR_GroundFloor_32 | Floor_1000x1000 | (5500,1500,0) | — | MI_DevGray |
|
||||||
|
| 33 | LVL_Global_STR_GroundFloor_33 | Floor_1000x1000 | (5500,2500,0) | — | MI_DevGray |
|
||||||
|
| 34 | LVL_Global_STR_GroundFloor_34 | Floor_1000x1000 | (5500,3500,0) | — | MI_DevGray |
|
||||||
|
| 35 | LVL_Global_STR_GroundFloor_35 | Floor_1000x1000 | (5500,4500,0) | — | MI_DevGray |
|
||||||
|
| 36 | LVL_Global_STR_GroundFloor_36 | Floor_1000x1000 | (5500,5500,0) | — | MI_DevGray |
|
||||||
|
| 37 | LVL_Global_STR_GroundFloor_37 | Floor_1000x1000 | (6500,500,0) | — | MI_DevGray |
|
||||||
|
| 38 | LVL_Global_STR_GroundFloor_38 | Floor_1000x1000 | (6500,1500,0) | — | MI_DevGray |
|
||||||
|
| 39 | LVL_Global_STR_GroundFloor_39 | Floor_1000x1000 | (6500,2500,0) | — | MI_DevGray |
|
||||||
|
| 40 | LVL_Global_STR_GroundFloor_40 | Floor_1000x1000 | (6500,3500,0) | — | MI_DevGray |
|
||||||
|
| 41 | LVL_Global_STR_GroundFloor_41 | Floor_1000x1000 | (6500,4500,0) | — | MI_DevGray |
|
||||||
|
| 42 | LVL_Global_STR_GroundFloor_42 | Floor_1000x1000 | (6500,5500,0) | — | MI_DevGray |
|
||||||
|
| 43 | LVL_Global_STR_GroundFloor_43 | Floor_1000x1000 | (7500,500,0) | — | MI_DevGray |
|
||||||
|
| 44 | LVL_Global_STR_GroundFloor_44 | Floor_1000x1000 | (7500,1500,0) | — | MI_DevGray |
|
||||||
|
| 45 | LVL_Global_STR_GroundFloor_45 | Floor_1000x1000 | (7500,2500,0) | — | MI_DevGray |
|
||||||
|
| 46 | LVL_Global_STR_GroundFloor_46 | Floor_1000x1000 | (7500,3500,0) | — | MI_DevGray |
|
||||||
|
| 47 | LVL_Global_STR_GroundFloor_47 | Floor_1000x1000 | (7500,4500,0) | — | MI_DevGray |
|
||||||
|
| 48 | LVL_Global_STR_GroundFloor_48 | Floor_1000x1000 | (7500,5500,0) | — | MI_DevGray |
|
||||||
|
|
||||||
|
### B2. 放置边界围挡
|
||||||
|
|
||||||
|
#### 西边界
|
||||||
|
|
||||||
|
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 旋转 (P,Y,R) | 缩放 | 材质 |
|
||||||
|
|---|-----------|------|-------------|-------------|------|------|
|
||||||
|
| 1 | LVL_Global_BND_西边界_01 | BarrierWall_600 | (0,300,105) | (0,90,0) | — | MI_DevRed |
|
||||||
|
| 2 | LVL_Global_BND_西边界_02 | BarrierWall_600 | (0,900,105) | (0,90,0) | — | MI_DevRed |
|
||||||
|
| 3 | LVL_Global_BND_西边界_03 | BarrierWall_600 | (0,1500,105) | (0,90,0) | — | MI_DevRed |
|
||||||
|
| 4 | LVL_Global_BND_西边界_04 | BarrierWall_600 | (0,2100,105) | (0,90,0) | — | MI_DevRed |
|
||||||
|
| 5 | LVL_Global_BND_西边界_05 | BarrierWall_600 | (0,2700,105) | (0,90,0) | — | MI_DevRed |
|
||||||
|
| 6 | LVL_Global_BND_西边界_06 | BarrierWall_600 | (0,3300,105) | (0,90,0) | — | MI_DevRed |
|
||||||
|
| 7 | LVL_Global_BND_西边界_07 | BarrierWall_600 | (0,3900,105) | (0,90,0) | — | MI_DevRed |
|
||||||
|
| 8 | LVL_Global_BND_西边界_08 | BarrierWall_600 | (0,4500,105) | (0,90,0) | — | MI_DevRed |
|
||||||
|
| 9 | LVL_Global_BND_西边界_09 | BarrierWall_600 | (0,5100,105) | (0,90,0) | — | MI_DevRed |
|
||||||
|
| 10 | LVL_Global_BND_西边界_10 | BarrierWall_600 | (0,5700,105) | (0,90,0) | — | MI_DevRed |
|
||||||
|
|
||||||
|
#### 南边界
|
||||||
|
|
||||||
|
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 旋转 (P,Y,R) | 缩放 | 材质 |
|
||||||
|
|---|-----------|------|-------------|-------------|------|------|
|
||||||
|
| 1 | LVL_Global_BND_南边界_01 | BarrierWall_600 | (300,0,105) | — | — | 各段标识色 |
|
||||||
|
| 2 | LVL_Global_BND_南边界_02 | BarrierWall_600 | (900,0,105) | — | — | 各段标识色 |
|
||||||
|
| 3 | LVL_Global_BND_南边界_03 | BarrierWall_600 | (1500,0,105) | — | — | 各段标识色 |
|
||||||
|
| 4 | LVL_Global_BND_南边界_04 | BarrierWall_600 | (2100,0,105) | — | — | 各段标识色 |
|
||||||
|
| 5 | LVL_Global_BND_南边界_05 | BarrierWall_600 | (2700,0,105) | — | — | 各段标识色 |
|
||||||
|
| 6 | LVL_Global_BND_南边界_06 | BarrierWall_600 | (3300,0,105) | — | — | 各段标识色 |
|
||||||
|
| 7 | LVL_Global_BND_南边界_07 | BarrierWall_600 | (3900,0,105) | — | — | 各段标识色 |
|
||||||
|
| 8 | LVL_Global_BND_南边界_08 | BarrierWall_600 | (4500,0,105) | — | — | 各段标识色 |
|
||||||
|
| 9 | LVL_Global_BND_南边界_09 | BarrierWall_600 | (5100,0,105) | — | — | 各段标识色 |
|
||||||
|
| 10 | LVL_Global_BND_南边界_10 | BarrierWall_600 | (5700,0,105) | — | — | 各段标识色 |
|
||||||
|
| 11 | LVL_Global_BND_南边界_11 | BarrierWall_600 | (6300,0,105) | — | — | 各段标识色 |
|
||||||
|
| 12 | LVL_Global_BND_南边界_12 | BarrierWall_600 | (6900,0,105) | — | — | 各段标识色 |
|
||||||
|
| 13 | LVL_Global_BND_南边界_13 | BarrierWall_600 | (7500,0,105) | — | — | 各段标识色 |
|
||||||
|
|
||||||
|
#### 东边界
|
||||||
|
|
||||||
|
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 旋转 (P,Y,R) | 缩放 | 材质 |
|
||||||
|
|---|-----------|------|-------------|-------------|------|------|
|
||||||
|
| 1 | LVL_Global_BND_东边界_01 | BarrierWall_600 | (8000,300,105) | (0,90,0) | — | MI_DevBlue |
|
||||||
|
| 2 | LVL_Global_BND_东边界_02 | BarrierWall_600 | (8000,900,105) | (0,90,0) | — | MI_DevBlue |
|
||||||
|
| 3 | LVL_Global_BND_东边界_03 | BarrierWall_600 | (8000,1500,105) | (0,90,0) | — | MI_DevBlue |
|
||||||
|
| 4 | LVL_Global_BND_东边界_04 | BarrierWall_600 | (8000,2100,105) | (0,90,0) | — | MI_DevBlue |
|
||||||
|
| 5 | LVL_Global_BND_东边界_05 | BarrierWall_600 | (8000,2700,105) | (0,90,0) | — | MI_DevBlue |
|
||||||
|
| 6 | LVL_Global_BND_东边界_06 | BarrierWall_600 | (8000,3300,105) | (0,90,0) | — | MI_DevBlue |
|
||||||
|
| 7 | LVL_Global_BND_东边界_07 | BarrierWall_600 | (8000,3900,105) | (0,90,0) | — | MI_DevBlue |
|
||||||
|
| 8 | LVL_Global_BND_东边界_08 | BarrierWall_600 | (8000,4500,105) | (0,90,0) | — | MI_DevBlue |
|
||||||
|
| 9 | LVL_Global_BND_东边界_09 | BarrierWall_600 | (8000,5100,105) | (0,90,0) | — | MI_DevBlue |
|
||||||
|
| 10 | LVL_Global_BND_东边界_10 | BarrierWall_600 | (8000,5700,105) | (0,90,0) | — | MI_DevBlue |
|
||||||
|
|
||||||
|
#### 北边界
|
||||||
|
|
||||||
|
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 旋转 (P,Y,R) | 缩放 | 材质 |
|
||||||
|
|---|-----------|------|-------------|-------------|------|------|
|
||||||
|
| 1 | LVL_Global_BND_北边界_01 | BarrierWall_600 | (300,6000,105) | — | — | MI_DevBlue |
|
||||||
|
| 2 | LVL_Global_BND_北边界_02 | BarrierWall_600 | (900,6000,105) | — | — | MI_DevBlue |
|
||||||
|
| 3 | LVL_Global_BND_北边界_03 | BarrierWall_600 | (1500,6000,105) | — | — | MI_DevBlue |
|
||||||
|
| 4 | LVL_Global_BND_北边界_04 | BarrierWall_600 | (2100,6000,105) | — | — | MI_DevBlue |
|
||||||
|
| 5 | LVL_Global_BND_北边界_05 | BarrierWall_600 | (2700,6000,105) | — | — | MI_DevBlue |
|
||||||
|
| 6 | LVL_Global_BND_北边界_06 | BarrierWall_600 | (3300,6000,105) | — | — | MI_DevBlue |
|
||||||
|
| 7 | LVL_Global_BND_北边界_07 | BarrierWall_600 | (3900,6000,105) | — | — | MI_DevBlue |
|
||||||
|
| 8 | LVL_Global_BND_北边界_08 | BarrierWall_600 | (4500,6000,105) | — | — | MI_DevBlue |
|
||||||
|
| 9 | LVL_Global_BND_北边界_09 | BarrierWall_600 | (5100,6000,105) | — | — | MI_DevBlue |
|
||||||
|
| 10 | LVL_Global_BND_北边界_10 | BarrierWall_600 | (5700,6000,105) | — | — | MI_DevBlue |
|
||||||
|
| 11 | LVL_Global_BND_北边界_11 | BarrierWall_600 | (6300,6000,105) | — | — | MI_DevBlue |
|
||||||
|
| 12 | LVL_Global_BND_北边界_12 | BarrierWall_600 | (6900,6000,105) | — | — | MI_DevBlue |
|
||||||
|
| 13 | LVL_Global_BND_北边界_13 | BarrierWall_600 | (7500,6000,105) | — | — | MI_DevBlue |
|
||||||
|
|
||||||
|
#### Z6 水域
|
||||||
|
|
||||||
|
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 旋转 (P,Y,R) | 缩放 | 材质 |
|
||||||
|
|---|-----------|------|-------------|-------------|------|------|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、Phase C — 逐区域搭建
|
||||||
|
|
||||||
|
### C1. Z1 — 家
|
||||||
|
|
||||||
|
#### C1.1 结构体(Structure)
|
||||||
|
|
||||||
|
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 旋转 (P,Y,R) | 缩放 | 材质 |
|
||||||
|
|---|-----------|------|-------------|-------------|------|------|
|
||||||
|
| | LVL_Z1_STR_South院Wall_01 | Wall_400x300 | (0,0,0) | — | — | MI_DevRed |
|
||||||
|
| | LVL_Z1_STR_South院Wall_02 | Wall_400x300 | (400,0,0) | — | — | MI_DevRed |
|
||||||
|
| | LVL_Z1_STR_South院Wall_03 | Wall_200x300 | (800,0,0) | — | — | MI_DevRed |
|
||||||
|
| | LVL_Z1_STR_West院Wall_01 | Wall_400x300 | (0,0,0) | (0,-90,0) | — | MI_DevRed |
|
||||||
|
| | LVL_Z1_STR_West院Wall_02 | Wall_400x300 | (400,0,0) | (0,-90,0) | — | MI_DevRed |
|
||||||
|
| | LVL_Z1_STR_East院WallTop半段_01 | Wall_400x300 | (980,0,0) | (0,90,0) | — | MI_DevRed |
|
||||||
|
| | LVL_Z1_STR_East院WallTop半段_02 | Wall_400x300 | (1380,0,0) | (0,90,0) | — | MI_DevRed |
|
||||||
|
| | LVL_Z1_STR_North院Wall含院Door_01 | Cube | (0,780,0) | (0,180,0) | — | MI_DevRed |
|
||||||
|
| | LVL_Z1_STR_房屋SouthWall含Window_01 | WallWindow_400x300 | (200,30,0) | — | — | MI_DevWhite |
|
||||||
|
| | LVL_Z1_STR_房屋SouthWall含Window_02 | WallWindowSmall_200x300 | (600,30,0) | — | — | MI_DevWhite |
|
||||||
|
| | LVL_Z1_STR_房屋WestWall_01 | Wall_400x300 | (200,30,0) | (0,-90,0) | — | MI_DevWhite |
|
||||||
|
| | LVL_Z1_STR_房屋WestWall_02 | Wall_400x100 | (600,30,0) | (0,-90,0) | — | MI_DevWhite |
|
||||||
|
| | LVL_Z1_STR_房屋EastWall_01 | Wall_400x300 | (780,30,0) | (0,90,0) | — | MI_DevWhite |
|
||||||
|
| | LVL_Z1_STR_房屋EastWall_02 | Wall_400x100 | (1180,30,0) | (0,90,0) | — | MI_DevWhite |
|
||||||
|
| | LVL_Z1_STR_房屋NorthWall含Door洞_01 | WallDoor_200x300 | (200,410,0) | (0,180,0) | — | MI_DevWhite |
|
||||||
|
| | LVL_Z1_STR_房屋NorthWall含Door洞_02 | Wall_400x300 | (400,410,0) | (0,180,0) | — | MI_DevWhite |
|
||||||
|
| | LVL_Z1_STR_房屋屋Roof_01 | Floor_300x400 | (190,25,280) | — | — | MI_DevBrown |
|
||||||
|
| | LVL_Z1_STR_房屋屋Roof_02 | Floor_300x400 | (490,25,280) | — | — | MI_DevBrown |
|
||||||
|
| | LVL_Z1_STR_院内Floor板_01 | Floor_1000x1000 | (0,0,0) | — | — | MI_DevGray |
|
||||||
|
|
||||||
|
#### C1.2 布景物(Props)
|
||||||
|
|
||||||
|
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 缩放 | 材质 | 说明 |
|
||||||
|
|---|-----------|------|-------------|------|------|------|
|
||||||
|
| | LVL_Z1_PROP_旧自行Car_01 | _composite | (100,650,0) | — | | 靠西院墙停放,后座铁丝筐用 Cube 替代 |
|
||||||
|
| | LVL_Z1_PROP_旧自行Car_02 | Wheel | (100,650,0) | — | | 靠西院墙停放,后座铁丝筐用 Cube 替代 |
|
||||||
|
| | LVL_Z1_PROP_晾衣杆_01 | Beam_300 | (500,600,0) | — | | 挂衣服用 Cube 薄片替代 |
|
||||||
|
| | LVL_Z1_PROP_晾衣杆_02 | Cylinder | (500,600,0) | — | | 挂衣服用 Cube 薄片替代 |
|
||||||
|
| | LVL_Z1_PROP_杂物堆_01 | WoodenBox_Small | (850,100,0) | — | | 东南角 |
|
||||||
|
| | LVL_Z1_PROP_杂物堆_02 | WoodenBox_Small | (890,100,0) | — | | 东南角 |
|
||||||
|
| | LVL_Z1_PROP_杂物堆_03 | Bucket_01 | (930,100,0) | — | | 东南角 |
|
||||||
|
| | LVL_Z1_PROP_Door垫_01 | Cube | (480,740,0) | — | | 院门内侧地面 |
|
||||||
|
| | LVL_Z1_PROP_老式冰Box_01 | Barrel | (720,80,0) | — | | 房屋内东南角,靠东墙 |
|
||||||
|
| | LVL_Z1_PROP_大脑袋电视_01 | Monitor_01 | (320,80,45) | — | | 房屋内西半部,放在书桌上 |
|
||||||
|
| | LVL_Z1_PROP_书Table_01 | SimpleTable_01 | (300,70,0) | — | | 房屋内靠西墙 |
|
||||||
|
| | LVL_Z1_PROP_Bed_01 | BedSingle | (350,250,0) | — | | 房屋内中部偏南 |
|
||||||
|
| | LVL_Z1_PROP_日历_01 | Cube | (500,40,180) | — | | 挂在房屋南墙内侧 |
|
||||||
|
| | LVL_Z1_PROP_电视Cabinet_01 | SimpleTable_02 | (320,55,0) | — | | 电视放在其上(v0.1 可与书桌合并) |
|
||||||
|
|
||||||
|
### C2. Z2 — 小卖部
|
||||||
|
|
||||||
|
#### C2.1 结构体(Structure)
|
||||||
|
|
||||||
|
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 旋转 (P,Y,R) | 缩放 | 材质 |
|
||||||
|
|---|-----------|------|-------------|-------------|------|------|
|
||||||
|
| | LVL_Z2_STR_SouthWall_01 | Wall_400x300 | (1000,200,0) | — | — | MI_DevOrange |
|
||||||
|
| | LVL_Z2_STR_SouthWall_02 | Wall_200x300 | (1400,200,0) | — | — | MI_DevOrange |
|
||||||
|
| | LVL_Z2_STR_WestWall_01 | Wall_400x300 | (1000,200,0) | (0,-90,0) | — | MI_DevOrange |
|
||||||
|
| | LVL_Z2_STR_WestWall_02 | Wall_400x100 | (1400,200,0) | (0,-90,0) | — | MI_DevOrange |
|
||||||
|
| | LVL_Z2_STR_EastWall_01 | Wall_400x300 | (1580,200,0) | (0,90,0) | — | MI_DevOrange |
|
||||||
|
| | LVL_Z2_STR_EastWall_02 | Wall_400x100 | (1980,200,0) | (0,90,0) | — | MI_DevOrange |
|
||||||
|
| | LVL_Z2_STR_NorthWall含Door洞_01 | WallDoorGarage_400x400 | (1000,580,0) | (0,180,0) | — | MI_DevOrange |
|
||||||
|
| | LVL_Z2_STR_NorthWall含Door洞_02 | Wall_200x300 | (1000,580,0) | (0,180,0) | — | MI_DevOrange |
|
||||||
|
| | LVL_Z2_STR_屋Roof_01 | Floor_300x400 | (990,190,280) | — | — | MI_DevBrown |
|
||||||
|
| | LVL_Z2_STR_屋Roof_02 | Floor_300x400 | (1290,190,280) | — | — | MI_DevBrown |
|
||||||
|
| | LVL_Z2_STR_Floor板_01 | Floor_1000x1000 | (1000,200,0) | — | — | MI_DevGray |
|
||||||
|
| | LVL_Z2_STR_遮阳棚前廊Roof_01 | Floor_200x300 | (1000,600,260) | — | — | MI_DevWhite |
|
||||||
|
| | LVL_Z2_STR_遮阳棚前廊Roof_02 | Floor_200x300 | (1200,600,260) | — | — | MI_DevWhite |
|
||||||
|
|
||||||
|
#### C2.2 布景物(Props)
|
||||||
|
|
||||||
|
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 缩放 | 材质 | 说明 |
|
||||||
|
|---|-----------|------|-------------|------|------|------|
|
||||||
|
| | LVL_Z2_PROP_玻璃CabinetCounter_01 | Cube | (1200,500,0) | — | | 店内中偏北,面向门口 |
|
||||||
|
| | LVL_Z2_PROP_玻璃CabinetCounter_02 | MI_DevGlassWhite | (1300,500,0) | — | | 店内中偏北,面向门口 |
|
||||||
|
| | LVL_Z2_PROP_零食货Shelf靠EastWall_01 | Fence_Small_400 | (1520,300,0) | — | | 东墙内侧,多层货架 |
|
||||||
|
| | LVL_Z2_PROP_零食货Shelf靠EastWall_02 | Fence_Small_400 | (1920,300,0) | — | | 东墙内侧,多层货架 |
|
||||||
|
| | LVL_Z2_PROP_零食货Shelf靠EastWall_03 | MI_DevGray | (2320,300,0) | — | | 东墙内侧,多层货架 |
|
||||||
|
| | LVL_Z2_PROP_零食货Shelf靠SouthWall_01 | Fence_Small_400 | (1100,230,0) | — | | 南墙内侧 |
|
||||||
|
| | LVL_Z2_PROP_零食货Shelf靠SouthWall_02 | MI_DevGray | (1500,230,0) | — | | 南墙内侧 |
|
||||||
|
| | LVL_Z2_PROP_老式冰Cabinet_01 | Cube | (1050,400,0) | — | | 西南角,顶盖掀开式 |
|
||||||
|
| | LVL_Z2_PROP_老式冰Cabinet_02 | MI_DevWhite | (1150,400,0) | — | | 西南角,顶盖掀开式 |
|
||||||
|
| | LVL_Z2_PROP_小电视_01 | Monitor_01 | (1350,540,90) | — | | 放在柜台角落 |
|
||||||
|
| | LVL_Z2_PROP_招牌_01 | Cube | (1150,610,280) | — | | 北墙外侧上方,蓝底白字"百货" |
|
||||||
|
| | LVL_Z2_PROP_招牌_02 | MI_DevBlue | (1250,610,280) | — | | 北墙外侧上方,蓝底白字"百货" |
|
||||||
|
| | LVL_Z2_PROP_卡片Shelf_01 | Cube | (1400,550,0) | — | | 柜台旁地面 |
|
||||||
|
|
||||||
|
### C3. Z3 — 主街道
|
||||||
|
|
||||||
|
#### C3.2 布景物(Props)
|
||||||
|
|
||||||
|
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 缩放 | 材质 | 说明 |
|
||||||
|
|---|-----------|------|-------------|------|------|------|
|
||||||
|
| | LVL_Z3_PROP_公用电话亭_01 | Cube | (1800,1180,0) | — | | 街道北侧靠墙 |
|
||||||
|
| | LVL_Z3_PROP_公用电话亭_02 | Cylinder | (1900,1180,0) | — | | 街道北侧靠墙 |
|
||||||
|
| | LVL_Z3_PROP_老式自行Car_01 | Car01 | (2800,880,0) | (0.50,0.50,0.50) | | 街道南侧靠墙停放 |
|
||||||
|
| | LVL_Z3_PROP_晾衣杆_01 | Beam_300 | (1200,1250,0) | — | | 街道北侧,伸出墙面的晾衣杆 |
|
||||||
|
| | LVL_Z3_PROP_晾衣杆_02 | Cylinder | (1200,1250,0) | — | | 街道北侧,伸出墙面的晾衣杆 |
|
||||||
|
| | LVL_Z3_PROP_Bottom水道盖_01 | CirclePlatform | (800,1050,0) | — | | 街道路面中央偏西 |
|
||||||
|
| | LVL_Z3_PROP_WallTop涂鸦区域_01 | Cube | (3200,1270,100) | — | | 北墙 C 段墙面中部 |
|
||||||
|
| | LVL_Z3_PROP_WallTop涂鸦区域_02 | MI_DevYellow | (3300,1270,100) | — | | 北墙 C 段墙面中部 |
|
||||||
|
| | LVL_Z3_PROP_广告Wall_01 | DisplayPanels_400x200 | (3800,1270,0) | — | | 北墙 D 段起始处,手绘广告 |
|
||||||
|
| | LVL_Z3_PROP_垃圾Barrel_01 | TrashCans_01 | (4200,880,0) | — | | 街道南侧 |
|
||||||
|
| | LVL_Z3_PROP_电线杆West_01 | Cylinder | (1000,1050,0) | — | | 街道中轴 |
|
||||||
|
| | LVL_Z3_PROP_电线杆East_01 | Cylinder | (3500,1050,0) | — | | 街道中轴 |
|
||||||
|
|
||||||
|
### C4. Z4 — 空地
|
||||||
|
|
||||||
|
#### C4.1 结构体(Structure)
|
||||||
|
|
||||||
|
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 旋转 (P,Y,R) | 缩放 | 材质 |
|
||||||
|
|---|-----------|------|-------------|-------------|------|------|
|
||||||
|
| | LVL_Z4_STR_Floor面_01 | Cube | (3500,1300,0) | — | — | MI_DevBrown |
|
||||||
|
| | LVL_Z4_STR_East边界Wall挡_01 | Cube | (4980,1300,0) | (0,90,0) | — | MI_DevBlue |
|
||||||
|
| | LVL_Z4_STR_North边界Wall挡_01 | Cube | (3500,2480,0) | (0,180,0) | — | MI_DevBlue |
|
||||||
|
| | LVL_Z4_STR_West边界矮Wall_01 | Cube | (3500,1300,0) | (0,-90,0) | — | MI_DevGray |
|
||||||
|
|
||||||
|
#### C4.2 布景物(Props)
|
||||||
|
|
||||||
|
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 缩放 | 材质 | 说明 |
|
||||||
|
|---|-----------|------|-------------|------|------|------|
|
||||||
|
| | LVL_Z4_PROP_大梧桐Tree_01 | Tree_01 | (4250,1900,0) | — | | 空地偏北中央 |
|
||||||
|
| | LVL_Z4_PROP_散落砖块_01 | WoodenBox_Small | (3700,1600,0) | — | | 空地西半部 |
|
||||||
|
| | LVL_Z4_PROP_散落砖块_02 | WoodenBox_Small | (3708,1600,0) | — | | 空地西半部 |
|
||||||
|
| | LVL_Z4_PROP_散落砖块_03 | WoodenBox_Small | (3716,1600,0) | — | | 空地西半部 |
|
||||||
|
| | LVL_Z4_PROP_散落砖块_04 | WoodenBox_Small | (3724,1600,0) | — | | 空地西半部 |
|
||||||
|
| | LVL_Z4_PROP_散落砖块_05 | WoodenBox_Small | (3732,1600,0) | — | | 空地西半部 |
|
||||||
|
| | LVL_Z4_PROP_积水坑_01 | Cube | (4600,1500,-5) | — | | 空地东南角,嵌入地面 |
|
||||||
|
| | LVL_Z4_PROP_积水坑_02 | MI_DevGlassBlue | (4700,1500,-5) | — | | 空地东南角,嵌入地面 |
|
||||||
|
| | LVL_Z4_PROP_弹珠圈痕迹_01 | CirclePlatform | (4100,1800,0) | — | | 空地中偏北,地面上的圆圈 |
|
||||||
|
| | LVL_Z4_PROP_弹珠圈痕迹_02 | MI_DevGray | (4100,1800,0) | — | | 空地中偏北,地面上的圆圈 |
|
||||||
|
| | LVL_Z4_PROP_废弃建材_01 | WoodenPallet_01 | (4800,2300,0) | — | | 空地东北角 |
|
||||||
|
| | LVL_Z4_PROP_废弃建材_02 | WoodenBox_Normal | (4800,2300,0) | — | | 空地东北角 |
|
||||||
|
|
||||||
|
### C5. Z5 — 巷子
|
||||||
|
|
||||||
|
#### C5.1 结构体(Structure)
|
||||||
|
|
||||||
|
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 旋转 (P,Y,R) | 缩放 | 材质 |
|
||||||
|
|---|-----------|------|-------------|-------------|------|------|
|
||||||
|
| | LVL_Z5_STR_WestWall_01 | Wall_400x300 | (2300,1300,0) | (0,-90,0) | — | MI_DevWhite |
|
||||||
|
| | LVL_Z5_STR_WestWall_02 | Wall_400x300 | (2700,1300,0) | (0,-90,0) | — | MI_DevWhite |
|
||||||
|
| | LVL_Z5_STR_WestWall_03 | Wall_400x300 | (3100,1300,0) | (0,-90,0) | — | MI_DevWhite |
|
||||||
|
| | LVL_Z5_STR_WestWall_04 | Wall_400x300 | (3500,1300,0) | (0,-90,0) | — | MI_DevWhite |
|
||||||
|
| | LVL_Z5_STR_WestWall_05 | Wall_400x300 | (3900,1300,0) | (0,-90,0) | — | MI_DevWhite |
|
||||||
|
| | LVL_Z5_STR_EastWall_01 | Wall_400x300 | (2580,1300,0) | (0,90,0) | — | MI_DevWhite |
|
||||||
|
| | LVL_Z5_STR_EastWall_02 | Wall_400x300 | (2980,1300,0) | (0,90,0) | — | MI_DevWhite |
|
||||||
|
| | LVL_Z5_STR_EastWall_03 | Wall_400x300 | (3380,1300,0) | (0,90,0) | — | MI_DevWhite |
|
||||||
|
| | LVL_Z5_STR_EastWall_04 | Wall_400x300 | (3780,1300,0) | (0,90,0) | — | MI_DevWhite |
|
||||||
|
| | LVL_Z5_STR_EastWall_05 | Wall_400x300 | (4180,1300,0) | (0,90,0) | — | MI_DevWhite |
|
||||||
|
| | LVL_Z5_STR_Floor面_01 | Floor_1000x1000 | (2300,1300,0) | — | — | MI_DevGray |
|
||||||
|
| | LVL_Z5_STR_Floor面_02 | Floor_1000x1000 | (3300,1300,0) | — | — | MI_DevGray |
|
||||||
|
|
||||||
|
#### C5.2 布景物(Props)
|
||||||
|
|
||||||
|
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 缩放 | 材质 | 说明 |
|
||||||
|
|---|-----------|------|-------------|------|------|------|
|
||||||
|
| | LVL_Z5_PROP_废纸Box_01 | WoodenBox_Small | (2380,2900,0) | — | | 巷子深处(北段)靠西墙 |
|
||||||
|
| | LVL_Z5_PROP_蜘蛛网_01 | Cube | (2305,2300,250) | — | | 西墙高处 |
|
||||||
|
| | LVL_Z5_PROP_老Window户WestWall_01 | FakeWindowsFrame | (2310,2100,150) | — | | 西墙中段,不可交互 |
|
||||||
|
| | LVL_Z5_PROP_花盆WindowCounter_01 | Cylinder | (2340,2100,150) | — | | 窗台上的花盆 |
|
||||||
|
| | LVL_Z5_PROP_青苔Floor面细节_01 | Cube | (2400,2700,0) | — | | 巷子北段地面 |
|
||||||
|
| | LVL_Z5_PROP_青苔Floor面细节_02 | MI_DevFoliage | (2500,2700,0) | — | | 巷子北段地面 |
|
||||||
|
|
||||||
|
### C6. Z6 — 水塘边
|
||||||
|
|
||||||
|
#### C6.1 结构体(Structure)
|
||||||
|
|
||||||
|
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 旋转 (P,Y,R) | 缩放 | 材质 |
|
||||||
|
|---|-----------|------|-------------|-------------|------|------|
|
||||||
|
| | LVL_Z6_STR_水面_01 | Floor_1000x1000 | (1750,3650,-20) | — | — | MI_DevSkyBlue |
|
||||||
|
| | LVL_Z6_STR_水面_02 | Floor_200x300 | (2750,3650,-20) | — | — | MI_DevSkyBlue |
|
||||||
|
| | LVL_Z6_STR_岸边Floor面South_01 | Floor_1000x1000 | (1600,3300,0) | — | — | MI_DevBrown |
|
||||||
|
| | LVL_Z6_STR_岸边Floor面South_02 | Floor_200x300 | (2600,3300,0) | — | — | MI_DevBrown |
|
||||||
|
| | LVL_Z6_STR_岸边Floor面East_01 | Floor_1000x1000 | (2950,3650,0) | (0,90,0) | — | MI_DevBrown |
|
||||||
|
| | LVL_Z6_STR_岸边Floor面West_01 | Floor_1000x1000 | (1600,3650,0) | (0,-90,0) | — | MI_DevBrown |
|
||||||
|
| | LVL_Z6_STR_East边界Wall挡_01 | BarrierWall_600 | (3080,3300,0) | (0,90,0) | — | MI_DevBlue |
|
||||||
|
| | LVL_Z6_STR_East边界Wall挡_02 | BarrierWall_600 | (3680,3300,0) | (0,90,0) | — | MI_DevBlue |
|
||||||
|
| | LVL_Z6_STR_North边界Wall挡_01 | BarrierWall_600 | (1600,4680,0) | (0,180,0) | — | MI_DevBlue |
|
||||||
|
| | LVL_Z6_STR_North边界Wall挡_02 | BarrierWall_600 | (2200,4680,0) | (0,180,0) | — | MI_DevBlue |
|
||||||
|
| | LVL_Z6_STR_North边界Wall挡_03 | BarrierWall_200 | (2800,4680,0) | (0,180,0) | — | MI_DevBlue |
|
||||||
|
|
||||||
|
#### C6.2 布景物(Props)
|
||||||
|
|
||||||
|
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 缩放 | 材质 | 说明 |
|
||||||
|
|---|-----------|------|-------------|------|------|------|
|
||||||
|
| | LVL_Z6_PROP_芦苇丛South岸Left_01 | Cylinder | (1800,3600,0) | — | | 水塘南岸边 |
|
||||||
|
| | LVL_Z6_PROP_芦苇丛South岸Left_02 | Cylinder | (1900,3600,0) | — | | 水塘南岸边 |
|
||||||
|
| | LVL_Z6_PROP_芦苇丛South岸Left_03 | Cylinder | (2000,3600,0) | — | | 水塘南岸边 |
|
||||||
|
| | LVL_Z6_PROP_芦苇丛South岸Left_04 | Cylinder | (2100,3600,0) | — | | 水塘南岸边 |
|
||||||
|
| | LVL_Z6_PROP_芦苇丛South岸Left_05 | Cylinder | (2200,3600,0) | — | | 水塘南岸边 |
|
||||||
|
| | LVL_Z6_PROP_芦苇丛South岸Left_06 | MI_DevFoliage | (2300,3600,0) | — | | 水塘南岸边 |
|
||||||
|
| | LVL_Z6_PROP_芦苇丛South岸Right_01 | Cylinder | (2700,3600,0) | — | | 水塘南岸边 |
|
||||||
|
| | LVL_Z6_PROP_芦苇丛South岸Right_02 | Cylinder | (2800,3600,0) | — | | 水塘南岸边 |
|
||||||
|
| | LVL_Z6_PROP_芦苇丛South岸Right_03 | Cylinder | (2900,3600,0) | — | | 水塘南岸边 |
|
||||||
|
| | LVL_Z6_PROP_芦苇丛South岸Right_04 | Cylinder | (3000,3600,0) | — | | 水塘南岸边 |
|
||||||
|
| | LVL_Z6_PROP_芦苇丛South岸Right_05 | Cylinder | (3100,3600,0) | — | | 水塘南岸边 |
|
||||||
|
| | LVL_Z6_PROP_芦苇丛South岸Right_06 | MI_DevFoliage | (3200,3600,0) | — | | 水塘南岸边 |
|
||||||
|
| | LVL_Z6_PROP_芦苇丛East岸_01 | Cylinder | (2900,4100,0) | — | | |
|
||||||
|
| | LVL_Z6_PROP_芦苇丛East岸_02 | Cylinder | (3000,4100,0) | — | | |
|
||||||
|
| | LVL_Z6_PROP_芦苇丛East岸_03 | Cylinder | (3100,4100,0) | — | | |
|
||||||
|
| | LVL_Z6_PROP_芦苇丛East岸_04 | MI_DevFoliage | (3200,4100,0) | — | | |
|
||||||
|
| | LVL_Z6_PROP_荷叶_01 | CirclePlatform | (2200,3900,-18) | — | | 水面上 |
|
||||||
|
| | LVL_Z6_PROP_荷叶_02 | CirclePlatform | (2207,3900,-18) | — | | 水面上 |
|
||||||
|
| | LVL_Z6_PROP_荷叶_03 | CirclePlatform | (2215,3900,-18) | — | | 水面上 |
|
||||||
|
| | LVL_Z6_PROP_荷叶_04 | CirclePlatform | (2222,3900,-18) | — | | 水面上 |
|
||||||
|
| | LVL_Z6_PROP_荷叶_05 | MI_DevGreen | (2230,3900,-18) | — | | 水面上 |
|
||||||
|
| | LVL_Z6_PROP_青蛙_01 | Cube | (2100,3680,0) | — | | 南岸石头旁 |
|
||||||
|
| | LVL_Z6_PROP_青蛙_02 | MI_DevGreen | (2200,3680,0) | — | | 南岸石头旁 |
|
||||||
|
|
||||||
|
### C7. Z7 — 老居民楼
|
||||||
|
|
||||||
|
#### C7.1 结构体(Structure)
|
||||||
|
|
||||||
|
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 旋转 (P,Y,R) | 缩放 | 材质 |
|
||||||
|
|---|-----------|------|-------------|-------------|------|------|
|
||||||
|
| | LVL_Z7_STR_居民楼主体_01 | Building_01 | (5800,200,0) | — | — | MI_DevGray |
|
||||||
|
| | LVL_Z7_STR_碰撞WallWall_01 | Wall_400x300 | (5750,150,0) | — | — | MI_DevGray |
|
||||||
|
| | LVL_Z7_STR_碰撞WallWall_02 | Wall_400x300 | (6150,150,0) | — | — | MI_DevGray |
|
||||||
|
| | LVL_Z7_STR_碰撞WallWall_03 | Wall_400x300 | (6550,150,0) | — | — | MI_DevGray |
|
||||||
|
| | LVL_Z7_STR_碰撞WallWall_04 | Wall_100x300 | (6950,150,0) | — | — | MI_DevGray |
|
||||||
|
| | LVL_Z7_STR_碰撞WallWall侧_01 | Wall_400x300 | (5750,150,0) | — | — | MI_DevGray |
|
||||||
|
| | LVL_Z7_STR_碰撞WallWall侧_02 | Wall_400x300 | (6150,150,0) | — | — | MI_DevGray |
|
||||||
|
|
||||||
|
#### C7.2 布景物(Props)
|
||||||
|
|
||||||
|
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 缩放 | 材质 | 说明 |
|
||||||
|
|---|-----------|------|-------------|------|------|------|
|
||||||
|
| | LVL_Z7_PROP_晾衣杆阳Counter_01 | Beam_300 | (6100,190,800) | — | | 不同楼层阳台外伸 |
|
||||||
|
| | LVL_Z7_PROP_晾衣杆阳Counter_02 | Beam_300 | (6133,190,800) | — | | 不同楼层阳台外伸 |
|
||||||
|
| | LVL_Z7_PROP_晾衣杆阳Counter_03 | Beam_300 | (6166,190,800) | — | | 不同楼层阳台外伸 |
|
||||||
|
| | LVL_Z7_PROP_垃圾Barrel_01 | TrashCans_01 | (6000,140,0) | — | | 楼下地面 |
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、Phase C — 全局物件
|
||||||
|
|
||||||
|
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 缩放 | 材质 | 说明 |
|
||||||
|
|---|-----------|------|-------------|------|------|------|
|
||||||
|
| | LVL_Global_PROP_路Lamp_01 | RoadLamp_01 | (1500,1050,0) | — | | 街道中轴线偏南 |
|
||||||
|
| | LVL_Global_PROP_路Lamp_02 | RoadLamp_01 | (1500,1050,0) | — | | 街道中轴线偏南 |
|
||||||
|
| | LVL_Global_PROP_路Lamp_03 | RoadLamp_01 | (1500,1050,0) | — | | 街道中轴线偏南 |
|
||||||
|
| | LVL_Global_PROP_垃圾Barrel_01 | TrashCans_01 | (2000,850,0) | — | | 街道南侧靠墙 |
|
||||||
|
| | LVL_Global_PROP_垃圾Barrel_01 | MI_DevGreen | (2000,850,0) | — | | 街道南侧靠墙 |
|
||||||
|
| | LVL_Global_PROP_垃圾Barrel_02 | TrashCans_01 | (2000,850,0) | — | | 街道南侧靠墙 |
|
||||||
|
| | LVL_Global_PROP_垃圾Barrel_02 | MI_DevGreen | (2000,850,0) | — | | 街道南侧靠墙 |
|
||||||
|
| | LVL_Global_PROP_电线杆_01 | Cylinder | (1000,1050,0) | — | | 街道中轴,与路灯交错 |
|
||||||
|
| | LVL_Global_PROP_电线杆_02 | Cylinder | (1000,1050,0) | — | | 街道中轴,与路灯交错 |
|
||||||
|
| | LVL_Global_PROP_施工Wall挡South边界补充_01 | BarrierWall_600 | (0,0,0) | — | | 街道南侧背后的南场景边界 |
|
||||||
|
| | LVL_Global_PROP_施工Wall挡South边界补充_02 | MI_DevBlue | (0,0,0) | — | | 街道南侧背后的南场景边界 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、Phase D — 光照
|
||||||
|
|
||||||
|
### D1. 定向光(Directional Light)
|
||||||
|
|
||||||
|
| 类型 | 位置/方向 | 参数 |
|
||||||
|
|------|---------|------|
|
||||||
|
| Directional Light | 从西南方向照射,仰角约 45°(建议值,可调,范围 30°-60°) | 色温:暖白 ~5500K(建议值,可调);强度:中等(建议值,可调) |
|
||||||
|
> 夏日午后主光源
|
||||||
|
|
||||||
|
### D2. 天空光(Sky Light)
|
||||||
|
|
||||||
|
| 类型 | 位置/方向 | 参数 |
|
||||||
|
|------|---------|------|
|
||||||
|
| Sky Light | 全局 | 强度:低-中(建议值,可调) |
|
||||||
|
> 环境补光,避免纯黑阴影
|
||||||
|
|
||||||
|
### D3. 路灯点光源
|
||||||
|
|
||||||
|
> v0.1 为装饰性光源,午后光照下几乎不可见,数量: 3
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、Phase E — 验证清单
|
||||||
|
|
||||||
|
| # | 检查项 | 状态 |
|
||||||
|
|---|--------|------|
|
||||||
|
| 1 | 场景尺寸 8000×6000cm 正确 | ⬜ |
|
||||||
|
| 2 | 7 个区域均已搭建 | ⬜ |
|
||||||
|
| 3 | 所有边界围挡无缺口 | ⬜ |
|
||||||
|
| 4 | 玩家起点可到达 | ⬜ |
|
||||||
|
| 5 | 所有区域可通行 | ⬜ |
|
||||||
|
| 6 | 建筑入口与流线对齐 | ⬜ |
|
||||||
|
| 7 | 光照方向与场景一致 | ⬜ |
|
||||||
|
| 8 | Actor 总数: 161 | ⬜ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> 搭建指南生成完成。共 7 个区域,161 个 Actor。
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# execution-to-guide
|
||||||
|
|
||||||
|
关卡执行案 → 搭建指南 转换器。
|
||||||
|
|
||||||
|
将 `docs/design/关卡-vXX-<名称>-执行案.md` 自动转换为 `docs/technical/关卡-vXX-搭建指南.md`,供 `level-builder` 工具解析并在 UE5 中搭建白模关卡。
|
||||||
|
|
||||||
|
## 用法
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 基本用法(自动推断输出路径到 docs/technical/)
|
||||||
|
cd tools/execution-to-guide
|
||||||
|
python converter.py -i ../../docs/design/关卡-v01-城郊小街-执行案.md
|
||||||
|
|
||||||
|
# 指定输出路径
|
||||||
|
python converter.py -i ../../docs/design/关卡-v01-城郊小街-执行案.md -o ../../docs/technical/关卡-v01-搭建指南.md
|
||||||
|
|
||||||
|
# 详细日志
|
||||||
|
python converter.py -i <执行案.md> -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## 依赖
|
||||||
|
|
||||||
|
- `tools/level-builder/config/` 下的 `asset_mapping.py`、`material_mapping.py`、`substitution_rules.py`
|
||||||
|
- `tools/level-builder/parsers/markdown_table.py`
|
||||||
|
|
||||||
|
工具会自动从上级 `level-builder` 目录加载这些依赖。
|
||||||
|
|
||||||
|
## 模块说明
|
||||||
|
|
||||||
|
| 文件 | 功能 |
|
||||||
|
|------|------|
|
||||||
|
| `converter.py` | 主入口,编排解析→展开→生成流程 |
|
||||||
|
| `execution_parser.py` | 解析执行案 Markdown,提取区域/结构体/布景/组合资产/边界/光照 |
|
||||||
|
| `asset_expander.py` | 将 BSP 资产建议字符串展开为 Actor 规格(坐标计算、旋转推断、资产映射) |
|
||||||
|
| `guide_writer.py` | 生成搭建指南 Markdown(匹配 `build_guide_parser.py` 的格式预期) |
|
||||||
|
| `composite_writer.py` | 生成组合资产清单 + JSON 模板(无直接 BSP 匹配的物件) |
|
||||||
|
|
||||||
|
## 数据流
|
||||||
|
|
||||||
|
```
|
||||||
|
关卡执行案 (.md)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
execution_parser.py → ExecutionPlanData
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
asset_expander.py → list[ActorSpec] (展开 BSP 建议 + 坐标计算)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
guide_writer.py → 搭建指南 (.md) (精确匹配 build_guide_parser 格式)
|
||||||
|
composite_writer.py → 组合资产清单 (.md + .json)
|
||||||
|
```
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# execution-to-guide 工具包
|
||||||
@@ -0,0 +1,423 @@
|
|||||||
|
"""BSP 资产建议展开器。
|
||||||
|
|
||||||
|
将执行案中的"BSP 资产建议"字符串拆分为可放置的 Actor 规格。
|
||||||
|
|
||||||
|
核心转换:
|
||||||
|
- "Wall_400x300 × 2" → 2 个 Actor,间隔 400cm
|
||||||
|
- "Wall_400x300 + Wall_200x300" → 2 种资产各 1 个
|
||||||
|
- "Cube 缩放(0.3,0.3,1.6)" → 1 个 Cube,带缩放参数
|
||||||
|
- 区域原点 + 相对偏移 → 绝对坐标
|
||||||
|
- 构件名称/方位 → 旋转推断
|
||||||
|
|
||||||
|
同时复用 level-builder 的 asset_mapping / material_mapping / substitution_rules。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import math
|
||||||
|
import logging
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
# 允许从包内或直接运行
|
||||||
|
from pathlib import Path
|
||||||
|
try:
|
||||||
|
from config.asset_mapping import MESH_ASSETS, MESH_ROOT, get_pivot_compensation
|
||||||
|
from config.material_mapping import MATERIAL_PATHS
|
||||||
|
from config.substitution_rules import SUBSTITUTION_RULES
|
||||||
|
except ImportError:
|
||||||
|
import sys
|
||||||
|
_lb = str(Path(__file__).resolve().parent.parent / "level-builder")
|
||||||
|
if _lb not in sys.path:
|
||||||
|
sys.path.insert(0, _lb)
|
||||||
|
from config.asset_mapping import MESH_ASSETS, MESH_ROOT, get_pivot_compensation
|
||||||
|
from config.material_mapping import MATERIAL_PATHS
|
||||||
|
from config.substitution_rules import SUBSTITUTION_RULES
|
||||||
|
|
||||||
|
logger = logging.getLogger("execution_to_guide.expander")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 数据结构
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ActorSpec:
|
||||||
|
"""展开后的单个 Actor 规格。"""
|
||||||
|
actor_name: str # LVL_Z1_STR_SouthWall_01
|
||||||
|
asset_key: str # Wall_400x300
|
||||||
|
asset_path: str # UE5 资产路径
|
||||||
|
location: tuple # (X, Y, Z) 绝对坐标 cm
|
||||||
|
rotation: tuple # (Pitch, Yaw, Roll) 度
|
||||||
|
scale: tuple # (X, Y, Z) 缩放
|
||||||
|
material: str # MI_DevRed
|
||||||
|
material_path: str # UE5 材质路径
|
||||||
|
category: str # STR | PROP | COMP
|
||||||
|
zone_id: str # Z1 | GLOBAL
|
||||||
|
folder: str # Outliner 文件夹路径
|
||||||
|
note: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 方向→旋转映射
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# 从构件名称中的方位词推断旋转
|
||||||
|
# UE5: Yaw 绕 Z 轴旋转,沿 X 正方向为 0°
|
||||||
|
# 南墙(沿 X 轴)= Yaw 0°,东墙(沿 Y 轴)= Yaw 90°
|
||||||
|
DIRECTION_YAW = {
|
||||||
|
"南": 0.0, # 沿 X 轴,面朝南(-Y)
|
||||||
|
"东": 90.0, # 沿 Y 轴,面朝东(+X)
|
||||||
|
"西": -90.0, # 沿 Y 轴,面朝西(-X)
|
||||||
|
"北": 180.0, # 沿 X 轴,面朝北(+Y)
|
||||||
|
"左": 90.0,
|
||||||
|
"右": -90.0,
|
||||||
|
"上": 0.0,
|
||||||
|
"下": 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def infer_rotation_from_name(name: str) -> tuple:
|
||||||
|
"""从构件名称推断旋转。
|
||||||
|
|
||||||
|
查找名称中的方位词(南/东/西/北),返回对应的 (Pitch, Yaw, Roll)。
|
||||||
|
如果没有方位词,返回 (0, 0, 0)。
|
||||||
|
"""
|
||||||
|
for direction, yaw in DIRECTION_YAW.items():
|
||||||
|
if direction in name:
|
||||||
|
return (0.0, yaw, 0.0)
|
||||||
|
return (0.0, 0.0, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def infer_rotation_from_offset(offset: tuple, zone_size: tuple) -> tuple:
|
||||||
|
"""从偏移位置推断旋转(备用方案)。
|
||||||
|
|
||||||
|
根据物件在区域中的位置判断朝向:
|
||||||
|
- 偏移 Y ≈ 0 → 南墙,Y ≈ max → 北墙
|
||||||
|
- 偏移 X ≈ 0 → 西墙,X ≈ max → 东墙
|
||||||
|
"""
|
||||||
|
dx, dy, dz = offset
|
||||||
|
w, d = zone_size[0], zone_size[1] if len(zone_size) > 1 else 0
|
||||||
|
|
||||||
|
if w > 0 and d > 0:
|
||||||
|
# 判断更接近哪条边
|
||||||
|
x_ratio = dx / w if w else 0
|
||||||
|
y_ratio = dy / d if d else 0
|
||||||
|
|
||||||
|
if y_ratio < 0.1:
|
||||||
|
return (0.0, 0.0, 0.0) # 南
|
||||||
|
elif y_ratio > 0.9:
|
||||||
|
return (0.0, 180.0, 0.0) # 北
|
||||||
|
elif x_ratio < 0.1:
|
||||||
|
return (0.0, -90.0, 0.0) # 西
|
||||||
|
elif x_ratio > 0.9:
|
||||||
|
return (0.0, 90.0, 0.0) # 东
|
||||||
|
|
||||||
|
return (0.0, 0.0, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# BSP 建议解析
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def parse_bsp_suggestion(bsp_text: str) -> list[dict]:
|
||||||
|
"""解析 BSP 资产建议字符串为资产列表。
|
||||||
|
|
||||||
|
支持的格式:
|
||||||
|
- "Wall_400x300" → [{"key": "Wall_400x300", "count": 1}]
|
||||||
|
- "Wall_400x300 × 2" → [{"key": "Wall_400x300", "count": 2}]
|
||||||
|
- "Wall_400x300 × 2 + Wall_200x300" → [{"key": "Wall_400x300", "count": 2}, {"key": "Wall_200x300", "count": 1}]
|
||||||
|
- "Cube 缩放(0.3,0.3,1.6)" → [{"key": "Cube", "count": 1, "scale": (0.3,0.3,1.6)}]
|
||||||
|
- "Car01 缩放50%" → [{"key": "Car01", "count": 1, "scale": (0.5,0.5,0.5)}]
|
||||||
|
- "Floor_1000x1000 缩放(8,6,1)" → [{"key": "Floor_1000x1000", "count": 1, "scale": (8,6,1)}]
|
||||||
|
- "Primitives 组合" → [{"key": "_composite", "count": 1}]
|
||||||
|
- "BarrierWall_600" → [{"key": "BarrierWall_600", "count": 1}]
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list of dicts, each with:
|
||||||
|
key: asset key string
|
||||||
|
count: int
|
||||||
|
scale: optional tuple (X, Y, Z)
|
||||||
|
"""
|
||||||
|
if not bsp_text or not bsp_text.strip():
|
||||||
|
return []
|
||||||
|
|
||||||
|
bsp_text = bsp_text.strip()
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# 按 + 分割多个资产段
|
||||||
|
segments = re.split(r'\s*\+\s*', bsp_text)
|
||||||
|
|
||||||
|
for seg in segments:
|
||||||
|
seg = seg.strip()
|
||||||
|
if not seg:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 检测 Primitives 组合标记
|
||||||
|
if "Primitives 组合" in seg or "组合" in seg and not re.search(r'[A-Z]', seg):
|
||||||
|
results.append({"key": "_composite", "count": 1})
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 提取资产 key
|
||||||
|
key_match = re.match(r'([A-Za-z_]\w*(?:\d+x\d+)?)', seg)
|
||||||
|
if not key_match:
|
||||||
|
continue
|
||||||
|
|
||||||
|
asset_key = key_match.group(1)
|
||||||
|
remaining = seg[key_match.end():]
|
||||||
|
|
||||||
|
# 提取数量:× 2 或 *2 或 x2
|
||||||
|
count = 1
|
||||||
|
count_match = re.search(r'[×x*]\s*(\d+)', remaining)
|
||||||
|
if count_match:
|
||||||
|
count = int(count_match.group(1))
|
||||||
|
|
||||||
|
# 提取缩放:缩放(X,Y,Z) 或 缩放NN%
|
||||||
|
scale = None
|
||||||
|
scale_match = re.search(r'缩放\s*\(([\d.]+)\s*[,,]\s*([\d.]+)\s*[,,]\s*([\d.]+)\)', remaining)
|
||||||
|
if scale_match:
|
||||||
|
scale = (float(scale_match.group(1)), float(scale_match.group(2)), float(scale_match.group(3)))
|
||||||
|
else:
|
||||||
|
pct_match = re.search(r'缩放\s*(\d+)%', remaining)
|
||||||
|
if pct_match:
|
||||||
|
s = int(pct_match.group(1)) / 100.0
|
||||||
|
scale = (s, s, s)
|
||||||
|
|
||||||
|
results.append({"key": asset_key, "count": count, "scale": scale})
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 资产展开
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
class AssetExpander:
|
||||||
|
"""将执行案条目展开为 Actor 规格。"""
|
||||||
|
|
||||||
|
def __init__(self, zones: list):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
zones: list[ZoneInfo] 区域信息列表
|
||||||
|
"""
|
||||||
|
self.zones = {z.zone_id: z for z in zones}
|
||||||
|
self.actor_counters = {} # zone_id -> {category: counter}
|
||||||
|
|
||||||
|
def expand_structure(self, item, zone_id: str) -> list[ActorSpec]:
|
||||||
|
"""展开结构体条目。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
item: StructureItem
|
||||||
|
zone_id: 区域 ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
展开后的 ActorSpec 列表
|
||||||
|
"""
|
||||||
|
zone = self.zones.get(zone_id)
|
||||||
|
if not zone:
|
||||||
|
logger.warning(f"未找到区域 {zone_id},跳过结构体 {item.item_id}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
return self._expand_item(
|
||||||
|
item_id=item.item_id,
|
||||||
|
name=item.name,
|
||||||
|
size=item.size,
|
||||||
|
offset=item.offset,
|
||||||
|
bsp_suggestion=item.bsp_suggestion,
|
||||||
|
tag_color=item.tag_color,
|
||||||
|
zone_id=zone_id,
|
||||||
|
zone=zone,
|
||||||
|
category="STR",
|
||||||
|
note="",
|
||||||
|
)
|
||||||
|
|
||||||
|
def expand_prop(self, item, zone_id: str) -> list[ActorSpec]:
|
||||||
|
"""展开布景物条目。"""
|
||||||
|
zone = self.zones.get(zone_id)
|
||||||
|
if not zone:
|
||||||
|
logger.warning(f"未找到区域 {zone_id},跳过布景 {item.item_id}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
return self._expand_item(
|
||||||
|
item_id=item.item_id,
|
||||||
|
name=item.name,
|
||||||
|
size=item.size,
|
||||||
|
offset=item.offset,
|
||||||
|
bsp_suggestion=item.bsp_suggestion,
|
||||||
|
tag_color="",
|
||||||
|
zone_id=zone_id,
|
||||||
|
zone=zone,
|
||||||
|
category="PROP",
|
||||||
|
note=item.note,
|
||||||
|
)
|
||||||
|
|
||||||
|
def expand_global(self, item) -> list[ActorSpec]:
|
||||||
|
"""展开全局物件条目。全局物件使用绝对坐标,不需要区域原点。"""
|
||||||
|
parts = parse_bsp_suggestion(item.bsp_suggestion)
|
||||||
|
if not parts:
|
||||||
|
# 尝试 substitution_rules
|
||||||
|
sub = SUBSTITUTION_RULES.get(item.name)
|
||||||
|
if sub:
|
||||||
|
parts = [{"key": sub["asset"], "count": item.count, "scale": sub.get("scale")}]
|
||||||
|
else:
|
||||||
|
parts = [{"key": "Cube", "count": item.count, "scale": None}]
|
||||||
|
|
||||||
|
actors = []
|
||||||
|
for i in range(item.count):
|
||||||
|
for part in parts:
|
||||||
|
asset_key = part["key"]
|
||||||
|
if asset_key == "_composite":
|
||||||
|
continue
|
||||||
|
|
||||||
|
actor_name = f"LVL_Global_PROP_{self._slugify(item.name)}"
|
||||||
|
if item.count > 1:
|
||||||
|
actor_name += f"_{i + 1:02d}"
|
||||||
|
elif len(parts) > 1:
|
||||||
|
actor_name += f"_{parts.index(part) + 1:02d}"
|
||||||
|
|
||||||
|
scale = part.get("scale")
|
||||||
|
if scale is None:
|
||||||
|
scale = (1.0, 1.0, 1.0)
|
||||||
|
|
||||||
|
asset_path = MESH_ASSETS.get(asset_key, {}).get("path", "")
|
||||||
|
material = ""
|
||||||
|
mat_path = ""
|
||||||
|
|
||||||
|
actors.append(ActorSpec(
|
||||||
|
actor_name=actor_name,
|
||||||
|
asset_key=asset_key,
|
||||||
|
asset_path=asset_path,
|
||||||
|
location=item.location,
|
||||||
|
rotation=(0.0, 0.0, 0.0),
|
||||||
|
scale=scale,
|
||||||
|
material=material,
|
||||||
|
material_path=mat_path,
|
||||||
|
category="PROP",
|
||||||
|
zone_id="GLOBAL",
|
||||||
|
folder=self._get_folder("GLOBAL", "PROP"),
|
||||||
|
note=item.note,
|
||||||
|
))
|
||||||
|
|
||||||
|
return actors
|
||||||
|
|
||||||
|
def _expand_item(self, item_id: str, name: str, size: tuple, offset: tuple,
|
||||||
|
bsp_suggestion: str, tag_color: str, zone_id: str,
|
||||||
|
zone, category: str, note: str) -> list[ActorSpec]:
|
||||||
|
"""通用展开逻辑。"""
|
||||||
|
parts = parse_bsp_suggestion(bsp_suggestion)
|
||||||
|
if not parts:
|
||||||
|
# 尝试 substitution_rules
|
||||||
|
sub = SUBSTITUTION_RULES.get(name)
|
||||||
|
if sub:
|
||||||
|
parts = [{"key": sub["asset"], "count": 1, "scale": sub.get("scale")}]
|
||||||
|
if not tag_color and sub.get("material"):
|
||||||
|
tag_color = sub["material"]
|
||||||
|
else:
|
||||||
|
parts = [{"key": "Cube", "count": 1, "scale": None}]
|
||||||
|
|
||||||
|
actors = []
|
||||||
|
# 跟踪资产段的累积偏移(用于 × N 的资产沿主轴排列)
|
||||||
|
cum_offset_x = 0.0
|
||||||
|
|
||||||
|
for part in parts:
|
||||||
|
asset_key = part["key"]
|
||||||
|
count = part.get("count", 1)
|
||||||
|
part_scale = part.get("scale")
|
||||||
|
|
||||||
|
for idx in range(count):
|
||||||
|
# 计算绝对位置
|
||||||
|
abs_x = zone.origin[0] + offset[0] + cum_offset_x
|
||||||
|
abs_y = zone.origin[1] + offset[1]
|
||||||
|
abs_z = zone.origin[2] + offset[2]
|
||||||
|
|
||||||
|
# 确定缩放
|
||||||
|
if part_scale:
|
||||||
|
scale = part_scale
|
||||||
|
else:
|
||||||
|
scale = (1.0, 1.0, 1.0)
|
||||||
|
|
||||||
|
# 确定旋转
|
||||||
|
rotation = infer_rotation_from_name(name)
|
||||||
|
|
||||||
|
# 确定资产路径
|
||||||
|
asset_info = MESH_ASSETS.get(asset_key, {})
|
||||||
|
asset_path = asset_info.get("path", "")
|
||||||
|
|
||||||
|
# 确定材质
|
||||||
|
material = tag_color
|
||||||
|
mat_path = MATERIAL_PATHS.get(material, "") if material else ""
|
||||||
|
|
||||||
|
# 生成 Actor 名称
|
||||||
|
actor_name = self._make_actor_name(zone_id, category, name, len(actors) + 1)
|
||||||
|
|
||||||
|
actors.append(ActorSpec(
|
||||||
|
actor_name=actor_name,
|
||||||
|
asset_key=asset_key,
|
||||||
|
asset_path=asset_path,
|
||||||
|
location=(abs_x, abs_y, abs_z),
|
||||||
|
rotation=rotation,
|
||||||
|
scale=scale,
|
||||||
|
material=material,
|
||||||
|
material_path=mat_path,
|
||||||
|
category=category,
|
||||||
|
zone_id=zone_id,
|
||||||
|
folder=self._get_folder(zone_id, category),
|
||||||
|
note=note,
|
||||||
|
))
|
||||||
|
|
||||||
|
# 累积偏移:沿资产主轴方向排列
|
||||||
|
if asset_info:
|
||||||
|
default_size = asset_info.get("default_size", (0, 0, 0))
|
||||||
|
if default_size and default_size[0] > 0:
|
||||||
|
cum_offset_x += default_size[0] * scale[0]
|
||||||
|
elif size and size[0] > 0 and count > 1:
|
||||||
|
cum_offset_x += size[0] / count
|
||||||
|
|
||||||
|
return actors
|
||||||
|
|
||||||
|
def _make_actor_name(self, zone_id: str, category: str, name: str, seq: int) -> str:
|
||||||
|
"""生成 Actor 名称:LVL_<ZoneID>_<CAT>_<Slug>_<Seq>"""
|
||||||
|
slug = self._slugify(name)
|
||||||
|
return f"LVL_{zone_id}_{category}_{slug}_{seq:02d}"
|
||||||
|
|
||||||
|
def _slugify(self, name: str) -> str:
|
||||||
|
"""将中文名称转为 Actor 名称用的英文缩写。"""
|
||||||
|
# 中文方位→英文
|
||||||
|
_DIR_MAP = {"南": "South", "北": "North", "东": "East", "西": "West",
|
||||||
|
"左": "Left", "右": "Right", "上": "Top", "下": "Bottom"}
|
||||||
|
_TYPE_MAP = {
|
||||||
|
"墙": "Wall", "围": "Wall", "地": "Floor", "顶": "Roof",
|
||||||
|
"门": "Door", "窗": "Window", "栏": "Rail", "柱": "Pillar",
|
||||||
|
"梁": "Beam", "台": "Counter", "架": "Shelf", "柜": "Cabinet",
|
||||||
|
"桌": "Table", "椅": "Chair", "床": "Bed", "箱": "Box",
|
||||||
|
"树": "Tree", "灯": "Lamp", "车": "Car", "桶": "Barrel",
|
||||||
|
}
|
||||||
|
|
||||||
|
result = ""
|
||||||
|
for ch in name:
|
||||||
|
if ch in _DIR_MAP:
|
||||||
|
result += _DIR_MAP[ch]
|
||||||
|
elif ch in _TYPE_MAP:
|
||||||
|
result += _TYPE_MAP[ch]
|
||||||
|
elif ch.isalnum():
|
||||||
|
result += ch
|
||||||
|
# 跳过其他中文字符
|
||||||
|
|
||||||
|
return result if result else "Item"
|
||||||
|
|
||||||
|
def _get_folder(self, zone_id: str, category: str) -> str:
|
||||||
|
"""获取 Outliner 文件夹路径。"""
|
||||||
|
try:
|
||||||
|
from config.zone_config import get_folder_path
|
||||||
|
return get_folder_path(zone_id, category)
|
||||||
|
except ImportError:
|
||||||
|
zone = self.zones.get(zone_id)
|
||||||
|
zone_name = zone.zone_name if zone else "Unknown"
|
||||||
|
return f"LVL_Level/{zone_id}_{zone_name}/{category}"
|
||||||
|
|
||||||
|
def _next_seq(self, zone_id: str, category: str) -> int:
|
||||||
|
"""获取下一个序号。"""
|
||||||
|
key = f"{zone_id}_{category}"
|
||||||
|
if key not in self.actor_counters:
|
||||||
|
self.actor_counters[key] = 0
|
||||||
|
self.actor_counters[key] += 1
|
||||||
|
return self.actor_counters[key]
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
"""组合资产清单 + 模板生成器。
|
||||||
|
|
||||||
|
将执行案中的组合资产标记展开为:
|
||||||
|
1. 组合资产清单 Markdown(供用户审查)
|
||||||
|
2. 组合资产模板 JSON(确认后回馈到 asset-catalog 模板库)
|
||||||
|
|
||||||
|
组合资产 = 无法用单个 BSP StaticMesh 匹配的物件,需要多个 Primitives 拼搭。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from dataclasses import dataclass, field, asdict
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from execution_parser import ExecutionPlanData, CompositeItem
|
||||||
|
|
||||||
|
logger = logging.getLogger("execution_to_guide.composite")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 数据结构
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CompositePart:
|
||||||
|
"""组合资产的一个构件。"""
|
||||||
|
primitive: str # Cube / Cylinder / CirclePlatform
|
||||||
|
scale: tuple # (X, Y, Z)
|
||||||
|
offset: tuple # (X, Y, Z) 相对组合资产原点 cm
|
||||||
|
material: str # MI_DevXxx
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CompositeTemplate:
|
||||||
|
"""组合资产模板(确认后回馈到 asset-catalog)。"""
|
||||||
|
template_name: str # 英文标识 OldRadio
|
||||||
|
display_name: str # 中文名 旧收音机
|
||||||
|
source_level: str # 来源版本 v02
|
||||||
|
parts: list # list[CompositePart]
|
||||||
|
total_bounds: dict # {"size": [W, D, H]}
|
||||||
|
tags: list = field(default_factory=list) # ["prop", "electronics"]
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 拼搭方案解析
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def parse_build_plan(plan_text: str, target_size: tuple) -> list[CompositePart]:
|
||||||
|
"""解析拼搭方案字符串为构件列表。
|
||||||
|
|
||||||
|
支持的格式:
|
||||||
|
- "Cube(0.2,0.15,0.12) + Cylinder天线"
|
||||||
|
- "Cube主体 + Cylinder(0.02,0.02,0.15)天线 offset(0.05,0,0.12)"
|
||||||
|
|
||||||
|
如果方案不够具体,生成一个默认的 Cube 占位。
|
||||||
|
"""
|
||||||
|
parts = []
|
||||||
|
if not plan_text:
|
||||||
|
# 默认:单个 Cube
|
||||||
|
w, d, h = target_size if target_size != (0, 0, 0) else (20, 20, 20)
|
||||||
|
parts.append(CompositePart(
|
||||||
|
primitive="Cube",
|
||||||
|
scale=(w / 100, d / 100, h / 100),
|
||||||
|
offset=(0, 0, 0),
|
||||||
|
material="MI_DevBrown",
|
||||||
|
))
|
||||||
|
return parts
|
||||||
|
|
||||||
|
# 按 + 分割
|
||||||
|
segments = re.split(r'\s*\+\s*', plan_text)
|
||||||
|
for seg in segments:
|
||||||
|
seg = seg.strip()
|
||||||
|
if not seg:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 提取 Primitives 类型
|
||||||
|
prim_match = re.match(r'(Cube|Cylinder|CirclePlatform)', seg)
|
||||||
|
prim = prim_match.group(1) if prim_match else "Cube"
|
||||||
|
remaining = seg[prim_match.end():] if prim_match else seg
|
||||||
|
|
||||||
|
# 提取缩放
|
||||||
|
scale_match = re.search(r'\(([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\)', remaining)
|
||||||
|
if scale_match:
|
||||||
|
scale = (float(scale_match.group(1)), float(scale_match.group(2)), float(scale_match.group(3)))
|
||||||
|
else:
|
||||||
|
scale = (0.1, 0.1, 0.1)
|
||||||
|
|
||||||
|
# 提取偏移
|
||||||
|
offset_match = re.search(r'offset\s*\(([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\)', remaining, re.IGNORECASE)
|
||||||
|
offset = (float(offset_match.group(1)), float(offset_match.group(2)), float(offset_match.group(3))) if offset_match else (0, 0, 0)
|
||||||
|
|
||||||
|
# 确定材质
|
||||||
|
material = "MI_DevBrown"
|
||||||
|
if "天线" in seg:
|
||||||
|
material = "MI_DevGray"
|
||||||
|
elif "玻璃" in seg or "镜" in seg:
|
||||||
|
material = "MI_DevGlassWhite"
|
||||||
|
elif "绿" in seg:
|
||||||
|
material = "MI_DevGreen"
|
||||||
|
elif "红" in seg:
|
||||||
|
material = "MI_DevRed"
|
||||||
|
|
||||||
|
parts.append(CompositePart(
|
||||||
|
primitive=prim,
|
||||||
|
scale=scale,
|
||||||
|
offset=offset,
|
||||||
|
material=material,
|
||||||
|
))
|
||||||
|
|
||||||
|
return parts if parts else [CompositePart(
|
||||||
|
primitive="Cube",
|
||||||
|
scale=(0.1, 0.1, 0.1),
|
||||||
|
offset=(0, 0, 0),
|
||||||
|
material="MI_DevBrown",
|
||||||
|
)]
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 组合资产文档生成
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
class CompositeWriter:
|
||||||
|
"""组合资产清单 + 模板生成器。"""
|
||||||
|
|
||||||
|
def __init__(self, plan_data: ExecutionPlanData):
|
||||||
|
self.plan = plan_data
|
||||||
|
self.templates: list[CompositeTemplate] = []
|
||||||
|
|
||||||
|
def has_composites(self) -> bool:
|
||||||
|
"""是否有组合资产。"""
|
||||||
|
return sum(len(v) for v in self.plan.zone_composites.values()) > 0
|
||||||
|
|
||||||
|
def generate_markdown(self) -> str:
|
||||||
|
"""生成组合资产清单 Markdown。"""
|
||||||
|
lines = [
|
||||||
|
f"# v{self.plan.version} 组合资产清单 — {self.plan.scene_name}",
|
||||||
|
"",
|
||||||
|
"> 本文档列出了执行案中需要用 Primitives 拼搭的组合资产。",
|
||||||
|
"> 请审查每个拼搭方案,确认后可回馈到 asset-catalog 模板库。",
|
||||||
|
"",
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
|
||||||
|
for zone_id, composites in self.plan.zone_composites.items():
|
||||||
|
if not composites:
|
||||||
|
continue
|
||||||
|
|
||||||
|
zone_name = self._get_zone_name(zone_id)
|
||||||
|
lines.append(f"## {zone_id} {zone_name}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
for comp in composites:
|
||||||
|
lines.append(f"### {comp.item_id} {comp.name}")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(f"**期望尺寸**: {comp.target_size[0]:.0f}×{comp.target_size[1]:.0f}×{comp.target_size[2]:.0f} cm")
|
||||||
|
lines.append(f"**说明**: {comp.note}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# 解析拼搭方案
|
||||||
|
parts = parse_build_plan(comp.build_plan, comp.target_size)
|
||||||
|
|
||||||
|
lines.append("| 构件 | Primitive | 缩放 (X,Y,Z) | 偏移 (X,Y,Z) | 材质 |")
|
||||||
|
lines.append("|------|-----------|-------------|-------------|------|")
|
||||||
|
|
||||||
|
for i, part in enumerate(parts):
|
||||||
|
lines.append(
|
||||||
|
f"| 构件{i + 1} | {part.primitive} | "
|
||||||
|
f"({part.scale[0]:.3f},{part.scale[1]:.3f},{part.scale[2]:.3f}) | "
|
||||||
|
f"({part.offset[0]:.1f},{part.offset[1]:.1f},{part.offset[2]:.1f}) | "
|
||||||
|
f"{part.material} |"
|
||||||
|
)
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# 生成模板
|
||||||
|
template = self._make_template(comp, parts)
|
||||||
|
self.templates.append(template)
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def generate_templates_json(self) -> str:
|
||||||
|
"""生成组合资产模板 JSON(回馈到 asset-catalog)。"""
|
||||||
|
data = {
|
||||||
|
"version": "1.0",
|
||||||
|
"source_level": self.plan.version,
|
||||||
|
"composite_templates": [asdict(t) for t in self.templates],
|
||||||
|
}
|
||||||
|
# 将 tuple 转为 list 以便 JSON 序列化
|
||||||
|
def convert_tuples(obj):
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return {k: convert_tuples(v) for k, v in obj.items()}
|
||||||
|
elif isinstance(obj, list):
|
||||||
|
return [convert_tuples(i) for i in obj]
|
||||||
|
elif isinstance(obj, tuple):
|
||||||
|
return list(obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
data = convert_tuples(data)
|
||||||
|
return json.dumps(data, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
def write_to_files(self, md_path: str, json_path: str = None):
|
||||||
|
"""写入 Markdown 和 JSON 文件。"""
|
||||||
|
# Markdown
|
||||||
|
md_content = self.generate_markdown()
|
||||||
|
md = Path(md_path)
|
||||||
|
md.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
md.write_text(md_content, encoding="utf-8")
|
||||||
|
logger.info(f"组合资产清单已写入: {md_path}")
|
||||||
|
|
||||||
|
# JSON(可选)
|
||||||
|
if json_path:
|
||||||
|
json_content = self.generate_templates_json()
|
||||||
|
jp = Path(json_path)
|
||||||
|
jp.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
jp.write_text(json_content, encoding="utf-8")
|
||||||
|
logger.info(f"组合资产模板已写入: {json_path}")
|
||||||
|
|
||||||
|
def _make_template(self, comp: CompositeItem, parts: list[CompositePart]) -> CompositeTemplate:
|
||||||
|
"""从组合资产条目生成模板。"""
|
||||||
|
# 生成英文标识
|
||||||
|
template_name = self._slugify(comp.name)
|
||||||
|
|
||||||
|
# 计算总包围盒
|
||||||
|
max_x = max(p.offset[0] + p.scale[0] * 100 for p in parts) if parts else 0
|
||||||
|
max_y = max(p.offset[1] + p.scale[1] * 100 for p in parts) if parts else 0
|
||||||
|
max_z = max(p.offset[2] + p.scale[2] * 100 for p in parts) if parts else 0
|
||||||
|
|
||||||
|
return CompositeTemplate(
|
||||||
|
template_name=template_name,
|
||||||
|
display_name=comp.name,
|
||||||
|
source_level=self.plan.version,
|
||||||
|
parts=[asdict(p) for p in parts],
|
||||||
|
total_bounds={"size": [max_x, max_y, max_z]},
|
||||||
|
tags=["prop", "composite"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_zone_name(self, zone_id: str) -> str:
|
||||||
|
for z in self.plan.zones:
|
||||||
|
if z.zone_id == zone_id:
|
||||||
|
return z.zone_name
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _slugify(self, name: str) -> str:
|
||||||
|
"""将中文名称转为英文标识。"""
|
||||||
|
# 简单映射
|
||||||
|
_MAP = {
|
||||||
|
"旧": "Old", "新": "New", "大": "Big", "小": "Small",
|
||||||
|
"收音机": "Radio", "电视": "TV", "冰箱": "Fridge",
|
||||||
|
"空调": "AC", "电话": "Phone", "自行车": "Bicycle",
|
||||||
|
"摩托": "Motorcycle", "柜": "Cabinet", "桌": "Table",
|
||||||
|
"椅": "Chair", "灯": "Lamp", "招牌": "Sign",
|
||||||
|
}
|
||||||
|
result = ""
|
||||||
|
for key, val in sorted(_MAP.items(), key=lambda x: -len(x[0])):
|
||||||
|
if key in name:
|
||||||
|
result += val
|
||||||
|
return result if result else "Composite"
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
"""执行案 → 搭建指南 转换器(主入口)。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python -m execution_to_guide.converter -i <执行案.md> [-o <搭建指南.md>]
|
||||||
|
|
||||||
|
示例:
|
||||||
|
python converter.py -i docs/design/关卡-v01-城郊小街-执行案.md -o docs/technical/关卡-v01-搭建指南.md
|
||||||
|
python converter.py -i docs/design/关卡-v01-城郊小街-执行案.md # 自动推断输出路径
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from execution_parser import ExecutionPlanParser
|
||||||
|
from guide_writer import GuideWriter
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(name)s: %(message)s",
|
||||||
|
)
|
||||||
|
logger = logging.getLogger("execution_to_guide")
|
||||||
|
|
||||||
|
|
||||||
|
def convert(input_path: str, output_path: str = None) -> str:
|
||||||
|
"""将执行案转换为搭建指南。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_path: 执行案 Markdown 文件路径
|
||||||
|
output_path: 搭建指南输出路径(可选,默认自动推断)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
生成的搭建指南 Markdown 内容
|
||||||
|
"""
|
||||||
|
# 解析执行案
|
||||||
|
logger.info(f"解析执行案: {input_path}")
|
||||||
|
parser = ExecutionPlanParser()
|
||||||
|
plan_data = parser.parse_file(input_path)
|
||||||
|
|
||||||
|
logger.info(f"场景: {plan_data.scene_name} v{plan_data.version}")
|
||||||
|
logger.info(f"尺寸: {plan_data.scene_size[0]}×{plan_data.scene_size[1]}cm")
|
||||||
|
logger.info(f"区域: {len(plan_data.zones)} 个")
|
||||||
|
logger.info(f"结构体: {sum(len(v) for v in plan_data.zone_structures.values())} 条")
|
||||||
|
logger.info(f"布景物: {sum(len(v) for v in plan_data.zone_props.values())} 条")
|
||||||
|
logger.info(f"组合资产: {sum(len(v) for v in plan_data.zone_composites.values())} 条")
|
||||||
|
logger.info(f"全局物件: {len(plan_data.global_objects)} 条")
|
||||||
|
logger.info(f"边界: {len(plan_data.boundaries)} 段")
|
||||||
|
|
||||||
|
# 生成搭建指南
|
||||||
|
writer = GuideWriter(plan_data)
|
||||||
|
content = writer.generate()
|
||||||
|
|
||||||
|
# 输出文件
|
||||||
|
if output_path is None:
|
||||||
|
# 自动推断:docs/design/关卡-v01-xxx-执行案.md → docs/technical/关卡-v01-搭建指南.md
|
||||||
|
in_path = Path(input_path)
|
||||||
|
version = plan_data.version
|
||||||
|
out_dir = in_path.parent.parent / "technical"
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
output_path = str(out_dir / f"关卡-{version}-搭建指南.md")
|
||||||
|
|
||||||
|
writer.write_to_file(output_path)
|
||||||
|
logger.info(f"搭建指南已输出: {output_path}")
|
||||||
|
|
||||||
|
# 统计
|
||||||
|
total_actors = len(writer.all_actors)
|
||||||
|
unique_assets = len(writer.asset_refs)
|
||||||
|
logger.info(f"生成 Actor: {total_actors} 个,使用资产: {unique_assets} 种")
|
||||||
|
|
||||||
|
# 如果有组合资产,提示
|
||||||
|
total_composites = sum(len(v) for v in plan_data.zone_composites.values())
|
||||||
|
if total_composites > 0:
|
||||||
|
logger.warning(f"⚠️ 发现 {total_composites} 个组合资产需要手动展开或确认拼搭方案")
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="关卡执行案 → 搭建指南 转换器",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog="""
|
||||||
|
示例:
|
||||||
|
python -m execution_to_guide.converter -i docs/design/关卡-v01-城郊小街-执行案.md
|
||||||
|
python converter.py -i docs/design/关卡-v01-城郊小街-执行案.md -o output/指南.md
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-i", "--input", required=True,
|
||||||
|
help="关卡执行案 Markdown 文件路径"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-o", "--output", default=None,
|
||||||
|
help="搭建指南输出路径(默认自动推断到 docs/technical/)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-v", "--verbose", action="store_true",
|
||||||
|
help="显示详细日志"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.verbose:
|
||||||
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
try:
|
||||||
|
convert(args.input, args.output)
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
logger.error(f"文件未找到: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"转换失败: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,662 @@
|
|||||||
|
"""关卡执行案 Markdown 解析器。
|
||||||
|
|
||||||
|
解析 docs/design/关卡-vXX-<名称>-执行案.md,提取结构化数据供后续转换为搭建指南。
|
||||||
|
|
||||||
|
执行案的表格格式(简化版,无交互物/收集品):
|
||||||
|
- 区域总览表: 区域编号 / 区域名称 / 绝对坐标范围 (X,Y) / 尺寸 / 地面材质建议 / 标识色
|
||||||
|
- 交通流线表: 流线路径 / 宽度 / 说明
|
||||||
|
- 结构体表: 编号 / 构件名称 / 尺寸 / 相对原点偏移 / BSP 资产建议 / 标识色
|
||||||
|
- 布景表: 编号 / 物件名称 / 尺寸 / 相对原点偏移 / BSP 资产建议 / 说明
|
||||||
|
- 组合资产表: 编号 / 物件名称 / 期望尺寸 / 推荐拼搭方案 / 说明
|
||||||
|
- 全局物件表: 编号 / 物件名称 / 数量 / 放置位置 / BSP 资产建议 / 说明
|
||||||
|
- 边界表: 边界段 / 位置范围 / 遮挡方式 / BSP 资产建议 / 标识色
|
||||||
|
- 光照表: 光源类型 / 数量 / 位置/方向 / 参数建议 / 说明
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
# 允许从包内或直接运行
|
||||||
|
try:
|
||||||
|
from parsers.markdown_table import parse_table, clean_cell
|
||||||
|
except ImportError:
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "level-builder"))
|
||||||
|
from parsers.markdown_table import parse_table, clean_cell
|
||||||
|
|
||||||
|
logger = logging.getLogger("execution_to_guide.parser")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 数据结构
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ZoneInfo:
|
||||||
|
"""区域信息。"""
|
||||||
|
zone_id: str # Z1, Z2, ...
|
||||||
|
zone_name: str # 家、小卖部、...
|
||||||
|
origin: tuple # (X, Y, Z) 区域西南角绝对坐标
|
||||||
|
bounds_x: tuple # (X_min, X_max)
|
||||||
|
bounds_y: tuple # (Y_min, Y_max)
|
||||||
|
size_m: tuple # (宽m, 深m)
|
||||||
|
ground_material: str = "" # 地面材质建议
|
||||||
|
tag_color: str = "" # 标识色 MI_DevXxx
|
||||||
|
height: float = 0 # 建筑高度(cm)
|
||||||
|
wall_height: float = 250 # 围墙高度
|
||||||
|
door_width: float = 120 # 门洞宽度
|
||||||
|
door_height: float = 220 # 门洞高度
|
||||||
|
indoor_clear_height: float = 280 # 室内净高
|
||||||
|
eaves_height: float = 300 # 屋檐高度
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StructureItem:
|
||||||
|
"""结构体条目(墙/地/顶/门/窗等建筑构件)。"""
|
||||||
|
item_id: str # Z1-S01
|
||||||
|
name: str # 南围墙
|
||||||
|
size: tuple # (宽, 深, 高) cm
|
||||||
|
offset: tuple # (ΔX, ΔY, ΔZ) 相对区域原点
|
||||||
|
bsp_suggestion: str # Wall_400x300 x 2
|
||||||
|
tag_color: str = "" # MI_DevRed
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PropItem:
|
||||||
|
"""布景物条目(家具/道具/装饰)。"""
|
||||||
|
item_id: str # Z1-P01
|
||||||
|
name: str # 旧自行车
|
||||||
|
size: tuple # (宽, 深, 高) cm(约)
|
||||||
|
offset: tuple # (ΔX, ΔY, ΔZ) 相对区域原点
|
||||||
|
bsp_suggestion: str # Car01 缩放50%
|
||||||
|
note: str = "" # 靠墙停放
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CompositeItem:
|
||||||
|
"""组合资产条目(需 Primitives 拼搭)。"""
|
||||||
|
item_id: str # Z1-C01
|
||||||
|
name: str # 旧收音机
|
||||||
|
target_size: tuple # (宽, 深, 高) cm(约)
|
||||||
|
build_plan: str # Cube(0.2,0.15,0.12) + Cylinder天线
|
||||||
|
note: str = "" # 放在柜台上
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GlobalObject:
|
||||||
|
"""全局物件。"""
|
||||||
|
item_id: str # G-01 / G-E01
|
||||||
|
name: str
|
||||||
|
count: int
|
||||||
|
location: tuple # (X, Y, Z) 绝对坐标
|
||||||
|
bsp_suggestion: str
|
||||||
|
note: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BoundaryItem:
|
||||||
|
"""场景边界。"""
|
||||||
|
segment: str # 西边界/南边界/东边界/北边界
|
||||||
|
location_range: str
|
||||||
|
block_method: str # 围墙/自然遮挡
|
||||||
|
bsp_suggestion: str
|
||||||
|
tag_color: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LightItem:
|
||||||
|
"""光照建议。"""
|
||||||
|
light_type: str # 定向光/天空光/路灯
|
||||||
|
count: int
|
||||||
|
position: str # 位置/方向描述
|
||||||
|
params: str # 参数建议
|
||||||
|
note: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TrafficFlow:
|
||||||
|
"""交通流线。"""
|
||||||
|
path: str # Z1 → Z3 家门 → 街道
|
||||||
|
width: float # cm
|
||||||
|
note: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ExecutionPlanData:
|
||||||
|
"""执行案解析后的完整数据。"""
|
||||||
|
version: str = ""
|
||||||
|
scene_name: str = ""
|
||||||
|
scene_size: tuple = (0, 0) # (宽cm, 深cm)
|
||||||
|
ground_z: float = 0
|
||||||
|
character_height: float = 120
|
||||||
|
camera_height: float = 100
|
||||||
|
time_setting: str = ""
|
||||||
|
zones: list = field(default_factory=list) # list[ZoneInfo]
|
||||||
|
flows: list = field(default_factory=list) # list[TrafficFlow]
|
||||||
|
zone_structures: dict = field(default_factory=dict) # zone_id -> list[StructureItem]
|
||||||
|
zone_props: dict = field(default_factory=dict) # zone_id -> list[PropItem]
|
||||||
|
zone_composites: dict = field(default_factory=dict) # zone_id -> list[CompositeItem]
|
||||||
|
global_objects: list = field(default_factory=list) # list[GlobalObject]
|
||||||
|
boundaries: list = field(default_factory=list) # list[BoundaryItem]
|
||||||
|
lights: list = field(default_factory=list) # list[LightItem]
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 解析逻辑
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
class ExecutionPlanParser:
|
||||||
|
"""关卡执行案解析器。"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.data = ExecutionPlanData()
|
||||||
|
|
||||||
|
def parse_file(self, filepath: str) -> ExecutionPlanData:
|
||||||
|
"""解析执行案文件。"""
|
||||||
|
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) -> ExecutionPlanData:
|
||||||
|
"""解析执行案文本。"""
|
||||||
|
lines = text.split("\n")
|
||||||
|
|
||||||
|
# 解析标题 → 版本和场景名
|
||||||
|
self._parse_title(lines)
|
||||||
|
|
||||||
|
# 一、场景参数
|
||||||
|
self._parse_scene_params(lines)
|
||||||
|
|
||||||
|
# 二、区域坐标布局
|
||||||
|
self._parse_zone_layout(lines)
|
||||||
|
|
||||||
|
# 三、逐区域搭建指南
|
||||||
|
self._parse_zone_details(lines)
|
||||||
|
|
||||||
|
# 四、全局物件清单
|
||||||
|
self._parse_global_objects(lines)
|
||||||
|
|
||||||
|
# 五、场景边界与遮挡
|
||||||
|
self._parse_boundaries(lines)
|
||||||
|
|
||||||
|
# 六、光照建议
|
||||||
|
self._parse_lighting(lines)
|
||||||
|
|
||||||
|
return self.data
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# 标题
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
def _parse_title(self, lines: list[str]):
|
||||||
|
"""从标题提取版本号和场景名。如:# v0.1 关卡执行案 — 城郊小街"""
|
||||||
|
for line in lines:
|
||||||
|
m = re.match(r'#\s+v([\d.]+)\s+关卡执行案\s*[—\-–]\s*(.+)', line)
|
||||||
|
if m:
|
||||||
|
self.data.version = m.group(1)
|
||||||
|
self.data.scene_name = m.group(2).strip()
|
||||||
|
break
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# 一、场景参数
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
def _parse_scene_params(self, lines: list[str]):
|
||||||
|
"""解析场景参数表。"""
|
||||||
|
section_start = self._find_section(lines, "一、场景参数")
|
||||||
|
section_end = self._find_section(lines, "二、区域坐标布局")
|
||||||
|
if section_start < 0:
|
||||||
|
return
|
||||||
|
if section_end < 0:
|
||||||
|
section_end = len(lines)
|
||||||
|
|
||||||
|
section_lines = lines[section_start:section_end]
|
||||||
|
tables = self._extract_tables(section_lines)
|
||||||
|
if not tables:
|
||||||
|
return
|
||||||
|
|
||||||
|
for row in tables[0]:
|
||||||
|
param = clean_cell(row.get("参数", ""))
|
||||||
|
value = clean_cell(row.get("值", ""))
|
||||||
|
if not param:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if "场景总尺寸" in param:
|
||||||
|
size_match = re.search(r'(\d+)\s*cm\s*[x×]\s*(\d+)\s*cm', value)
|
||||||
|
if size_match:
|
||||||
|
self.data.scene_size = (int(size_match.group(1)), int(size_match.group(2)))
|
||||||
|
elif "地面高度基准" in param:
|
||||||
|
z_match = re.search(r'Z?\s*=\s*(-?\d+)', value)
|
||||||
|
if z_match:
|
||||||
|
self.data.ground_z = float(z_match.group(1))
|
||||||
|
elif "主角身高" in param:
|
||||||
|
h_match = re.search(r'~?(\d+)', value)
|
||||||
|
if h_match:
|
||||||
|
self.data.character_height = float(h_match.group(1))
|
||||||
|
elif "视角高度" in param:
|
||||||
|
h_match = re.search(r'~?(\d+)', value)
|
||||||
|
if h_match:
|
||||||
|
self.data.camera_height = float(h_match.group(1))
|
||||||
|
elif "时间设定" in param:
|
||||||
|
self.data.time_setting = value
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# 二、区域坐标布局
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
def _parse_zone_layout(self, lines: list[str]):
|
||||||
|
"""解析区域总览表和交通流线表。"""
|
||||||
|
section_start = self._find_section(lines, "二、区域坐标布局")
|
||||||
|
section_end = self._find_section(lines, "三、逐区域搭建指南")
|
||||||
|
if section_start < 0:
|
||||||
|
return
|
||||||
|
if section_end < 0:
|
||||||
|
section_end = len(lines)
|
||||||
|
|
||||||
|
section_lines = lines[section_start:section_end]
|
||||||
|
tables = self._extract_tables(section_lines)
|
||||||
|
|
||||||
|
# 第一个表格 = 区域总览表
|
||||||
|
if tables:
|
||||||
|
for row in tables[0]:
|
||||||
|
zone = self._parse_zone_row(row)
|
||||||
|
if zone:
|
||||||
|
self.data.zones.append(zone)
|
||||||
|
|
||||||
|
# 第二个表格 = 交通流线(如果有)
|
||||||
|
if len(tables) > 1:
|
||||||
|
for row in tables[1]:
|
||||||
|
path = clean_cell(row.get("流线路径", ""))
|
||||||
|
if not path:
|
||||||
|
continue
|
||||||
|
width_str = clean_cell(row.get("宽度", "200"))
|
||||||
|
w_match = re.search(r'(\d+)', width_str)
|
||||||
|
width = float(w_match.group(1)) if w_match else 200.0
|
||||||
|
note = clean_cell(row.get("说明", ""))
|
||||||
|
self.data.flows.append(TrafficFlow(
|
||||||
|
path=path, width=width, note=note
|
||||||
|
))
|
||||||
|
|
||||||
|
def _parse_zone_row(self, row: dict) -> Optional[ZoneInfo]:
|
||||||
|
"""解析区域总览表的一行。"""
|
||||||
|
zone_id = clean_cell(row.get("区域编号", ""))
|
||||||
|
zone_name = clean_cell(row.get("区域名称", ""))
|
||||||
|
coord_str = clean_cell(row.get("绝对坐标范围 (X,Y)", row.get("绝对坐标范围", "")))
|
||||||
|
|
||||||
|
if not zone_id or not zone_name:
|
||||||
|
return None
|
||||||
|
if not re.match(r'Z\d+', zone_id):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 解析坐标范围:(X1,Y1)→(X2,Y2)
|
||||||
|
coord_match = re.match(
|
||||||
|
r'\((\d+)\s*,\s*(\d+)\)\s*[→\-–]+\s*\((\d+)\s*,\s*(\d+)\)',
|
||||||
|
coord_str
|
||||||
|
)
|
||||||
|
if not coord_match:
|
||||||
|
return None
|
||||||
|
|
||||||
|
x_min, y_min = int(coord_match.group(1)), int(coord_match.group(2))
|
||||||
|
x_max, y_max = int(coord_match.group(3)), int(coord_match.group(4))
|
||||||
|
|
||||||
|
# 解析尺寸
|
||||||
|
size_str = clean_cell(row.get("尺寸 (宽x深)", row.get("尺寸", "")))
|
||||||
|
size_match = re.search(r'(\d+)\s*m?\s*[x×]\s*(\d+)\s*m?', size_str)
|
||||||
|
size_m = (float(size_match.group(1)), float(size_match.group(2))) if size_match else (0, 0)
|
||||||
|
|
||||||
|
ground_mat = clean_cell(row.get("地面材质建议", ""))
|
||||||
|
tag_color = clean_cell(row.get("标识色", ""))
|
||||||
|
|
||||||
|
return ZoneInfo(
|
||||||
|
zone_id=zone_id,
|
||||||
|
zone_name=zone_name,
|
||||||
|
origin=(x_min, y_min, 0),
|
||||||
|
bounds_x=(x_min, x_max),
|
||||||
|
bounds_y=(y_min, y_max),
|
||||||
|
size_m=size_m,
|
||||||
|
ground_material=ground_mat,
|
||||||
|
tag_color=tag_color,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# 三、逐区域搭建指南
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
def _parse_zone_details(self, lines: list[str]):
|
||||||
|
"""解析逐区域搭建指南(结构体/布景/组合资产)。"""
|
||||||
|
section_start = self._find_section(lines, "三、逐区域搭建指南")
|
||||||
|
section_end = self._find_section(lines, "四、全局物件清单")
|
||||||
|
if section_start < 0:
|
||||||
|
return
|
||||||
|
if section_end < 0:
|
||||||
|
section_end = len(lines)
|
||||||
|
|
||||||
|
section_lines = lines[section_start:section_end]
|
||||||
|
current_zone = ""
|
||||||
|
current_zone_info = None
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
while i < len(section_lines):
|
||||||
|
line = section_lines[i].strip()
|
||||||
|
|
||||||
|
# 检测区域标题:### Z1:家(Home)或 ### Z1: 家 或 ### Z1:家
|
||||||
|
zm = re.match(r'###\s+(Z\d+)\s*[::]\s*(.+?)(?:\s*[((].+[)])?\s*$', line)
|
||||||
|
if zm:
|
||||||
|
current_zone = zm.group(1)
|
||||||
|
current_zone_info = self._find_zone(current_zone)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 检测区域绝对原点行:**区域绝对原点**:(X, Y, Z)
|
||||||
|
if "区域绝对原点" in line and current_zone:
|
||||||
|
origin_match = re.search(r'\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)\)', line)
|
||||||
|
if origin_match and current_zone_info:
|
||||||
|
current_zone_info.origin = (
|
||||||
|
int(origin_match.group(1)),
|
||||||
|
int(origin_match.group(2)),
|
||||||
|
int(origin_match.group(3)),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 检测区域参数子标题
|
||||||
|
if "区域参数" in line:
|
||||||
|
i += 1
|
||||||
|
i = self._skip_blank(section_lines, i)
|
||||||
|
rows = self._read_table(section_lines, i)
|
||||||
|
if rows:
|
||||||
|
i = self._advance_past_table(section_lines, i)
|
||||||
|
if current_zone_info:
|
||||||
|
self._parse_zone_params(rows, current_zone_info)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 检测结构体子标题
|
||||||
|
if "结构体清单" in line:
|
||||||
|
i += 1
|
||||||
|
i = self._skip_blank(section_lines, i)
|
||||||
|
rows = self._read_table(section_lines, i)
|
||||||
|
if rows and current_zone:
|
||||||
|
items = [self._parse_structure_row(r) for r in rows]
|
||||||
|
items = [it for it in items if it]
|
||||||
|
self.data.zone_structures[current_zone] = items
|
||||||
|
i = self._advance_past_table(section_lines, i)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 检测布景物子标题
|
||||||
|
if "布景物清单" in line:
|
||||||
|
i += 1
|
||||||
|
i = self._skip_blank(section_lines, i)
|
||||||
|
rows = self._read_table(section_lines, i)
|
||||||
|
if rows and current_zone:
|
||||||
|
items = [self._parse_prop_row(r) for r in rows]
|
||||||
|
items = [it for it in items if it]
|
||||||
|
self.data.zone_props[current_zone] = items
|
||||||
|
i = self._advance_past_table(section_lines, i)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 检测组合资产子标题
|
||||||
|
if "组合资产标记" in line:
|
||||||
|
i += 1
|
||||||
|
i = self._skip_blank(section_lines, i)
|
||||||
|
rows = self._read_table(section_lines, i)
|
||||||
|
if rows and current_zone:
|
||||||
|
items = [self._parse_composite_row(r) for r in rows]
|
||||||
|
items = [it for it in items if it]
|
||||||
|
self.data.zone_composites[current_zone] = items
|
||||||
|
i = self._advance_past_table(section_lines, i)
|
||||||
|
continue
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
def _parse_zone_params(self, rows: list[dict], zone: ZoneInfo):
|
||||||
|
"""解析区域参数表。"""
|
||||||
|
for row in rows:
|
||||||
|
param = clean_cell(row.get("参数", ""))
|
||||||
|
value = clean_cell(row.get("值", ""))
|
||||||
|
if "围墙" in param or "外墙高度" in param:
|
||||||
|
m = re.search(r'(\d+)', value)
|
||||||
|
if m:
|
||||||
|
zone.wall_height = float(m.group(1))
|
||||||
|
elif "门洞宽度" in param:
|
||||||
|
m = re.search(r'(\d+)', value)
|
||||||
|
if m:
|
||||||
|
zone.door_width = float(m.group(1))
|
||||||
|
elif "门洞高度" in param:
|
||||||
|
m = re.search(r'(\d+)', value)
|
||||||
|
if m:
|
||||||
|
zone.door_height = float(m.group(1))
|
||||||
|
elif "室内净高" in param:
|
||||||
|
m = re.search(r'(\d+)', value)
|
||||||
|
if m:
|
||||||
|
zone.indoor_clear_height = float(m.group(1))
|
||||||
|
elif "屋檐高度" in param:
|
||||||
|
m = re.search(r'(\d+)', value)
|
||||||
|
if m:
|
||||||
|
zone.eaves_height = float(m.group(1))
|
||||||
|
|
||||||
|
def _parse_structure_row(self, row: dict) -> Optional[StructureItem]:
|
||||||
|
"""解析结构体表行。"""
|
||||||
|
item_id = clean_cell(row.get("编号", ""))
|
||||||
|
name = clean_cell(row.get("构件名称", ""))
|
||||||
|
if not item_id or not name:
|
||||||
|
return None
|
||||||
|
|
||||||
|
size_str = clean_cell(row.get("尺寸 (宽x深x高, cm)", row.get("尺寸", "")))
|
||||||
|
size = self._parse_size(size_str)
|
||||||
|
|
||||||
|
offset_str = clean_cell(row.get("相对原点偏移 (ΔX,ΔY,ΔZ)", row.get("相对原点偏移", "")))
|
||||||
|
offset = self._parse_xyz(offset_str)
|
||||||
|
|
||||||
|
bsp = clean_cell(row.get("BSP 资产建议", ""))
|
||||||
|
tag = clean_cell(row.get("标识色", ""))
|
||||||
|
|
||||||
|
return StructureItem(
|
||||||
|
item_id=item_id, name=name, size=size,
|
||||||
|
offset=offset, bsp_suggestion=bsp, tag_color=tag
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_prop_row(self, row: dict) -> Optional[PropItem]:
|
||||||
|
"""解析布景表行。"""
|
||||||
|
item_id = clean_cell(row.get("编号", ""))
|
||||||
|
name = clean_cell(row.get("物件名称", ""))
|
||||||
|
if not item_id or not name:
|
||||||
|
return None
|
||||||
|
|
||||||
|
size_str = clean_cell(row.get("尺寸 (约, cm)", row.get("尺寸", "")))
|
||||||
|
size = self._parse_size(size_str)
|
||||||
|
|
||||||
|
offset_str = clean_cell(row.get("相对原点偏移 (ΔX,ΔY,ΔZ)", row.get("相对原点偏移", "")))
|
||||||
|
offset = self._parse_xyz(offset_str)
|
||||||
|
|
||||||
|
bsp = clean_cell(row.get("BSP 资产建议", ""))
|
||||||
|
note = clean_cell(row.get("说明", ""))
|
||||||
|
|
||||||
|
return PropItem(
|
||||||
|
item_id=item_id, name=name, size=size,
|
||||||
|
offset=offset, bsp_suggestion=bsp, note=note
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_composite_row(self, row: dict) -> Optional[CompositeItem]:
|
||||||
|
"""解析组合资产表行。"""
|
||||||
|
item_id = clean_cell(row.get("编号", ""))
|
||||||
|
name = clean_cell(row.get("物件名称", ""))
|
||||||
|
if not item_id or not name:
|
||||||
|
return None
|
||||||
|
|
||||||
|
size_str = clean_cell(row.get("期望尺寸 (约, cm)", row.get("期望尺寸", "")))
|
||||||
|
size = self._parse_size(size_str)
|
||||||
|
|
||||||
|
plan = clean_cell(row.get("推荐拼搭方案", ""))
|
||||||
|
note = clean_cell(row.get("说明", ""))
|
||||||
|
|
||||||
|
return CompositeItem(
|
||||||
|
item_id=item_id, name=name, target_size=size,
|
||||||
|
build_plan=plan, note=note
|
||||||
|
)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# 四、全局物件清单
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
def _parse_global_objects(self, lines: list[str]):
|
||||||
|
"""解析全局物件清单。"""
|
||||||
|
section_start = self._find_section(lines, "四、全局物件清单")
|
||||||
|
section_end = self._find_section(lines, "五、场景边界")
|
||||||
|
if section_start < 0:
|
||||||
|
return
|
||||||
|
if section_end < 0:
|
||||||
|
section_end = len(lines)
|
||||||
|
|
||||||
|
section_lines = lines[section_start:section_end]
|
||||||
|
tables = self._extract_tables(section_lines)
|
||||||
|
|
||||||
|
for table in tables:
|
||||||
|
for row in table:
|
||||||
|
item_id = clean_cell(row.get("编号", ""))
|
||||||
|
name = clean_cell(row.get("物件名称", ""))
|
||||||
|
if not item_id or not name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
count_str = clean_cell(row.get("数量", "1"))
|
||||||
|
c_match = re.search(r'(\d+)', count_str)
|
||||||
|
count = int(c_match.group(1)) if c_match else 1
|
||||||
|
|
||||||
|
loc_str = clean_cell(row.get("放置位置 (绝对坐标 X,Y,Z)", row.get("放置位置", "")))
|
||||||
|
location = self._parse_xyz(loc_str)
|
||||||
|
|
||||||
|
bsp = clean_cell(row.get("BSP 资产建议", ""))
|
||||||
|
note = clean_cell(row.get("说明", ""))
|
||||||
|
|
||||||
|
self.data.global_objects.append(GlobalObject(
|
||||||
|
item_id=item_id, name=name, count=count,
|
||||||
|
location=location, bsp_suggestion=bsp, note=note
|
||||||
|
))
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# 五、场景边界与遮挡
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
def _parse_boundaries(self, lines: list[str]):
|
||||||
|
"""解析场景边界表。"""
|
||||||
|
section_start = self._find_section(lines, "五、场景边界")
|
||||||
|
section_end = self._find_section(lines, "六、光照建议")
|
||||||
|
if section_start < 0:
|
||||||
|
return
|
||||||
|
if section_end < 0:
|
||||||
|
section_end = len(lines)
|
||||||
|
|
||||||
|
section_lines = lines[section_start:section_end]
|
||||||
|
tables = self._extract_tables(section_lines)
|
||||||
|
|
||||||
|
if tables:
|
||||||
|
for row in tables[0]:
|
||||||
|
segment = clean_cell(row.get("边界段", ""))
|
||||||
|
if not segment:
|
||||||
|
continue
|
||||||
|
loc_range = clean_cell(row.get("位置范围", ""))
|
||||||
|
method = clean_cell(row.get("遮挡方式", ""))
|
||||||
|
bsp = clean_cell(row.get("BSP 资产建议", ""))
|
||||||
|
tag = clean_cell(row.get("标识色", ""))
|
||||||
|
self.data.boundaries.append(BoundaryItem(
|
||||||
|
segment=segment, location_range=loc_range,
|
||||||
|
block_method=method, bsp_suggestion=bsp, tag_color=tag
|
||||||
|
))
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# 六、光照建议
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
def _parse_lighting(self, lines: list[str]):
|
||||||
|
"""解析光照建议表。"""
|
||||||
|
section_start = self._find_section(lines, "六、光照建议")
|
||||||
|
if section_start < 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
section_lines = lines[section_start:]
|
||||||
|
tables = self._extract_tables(section_lines)
|
||||||
|
|
||||||
|
if tables:
|
||||||
|
for row in tables[0]:
|
||||||
|
light_type = clean_cell(row.get("光源类型", ""))
|
||||||
|
if not light_type:
|
||||||
|
continue
|
||||||
|
count_str = clean_cell(row.get("数量", "1"))
|
||||||
|
c_match = re.search(r'(\d+)', count_str)
|
||||||
|
count = int(c_match.group(1)) if c_match else 1
|
||||||
|
position = clean_cell(row.get("位置/方向", ""))
|
||||||
|
params = clean_cell(row.get("参数建议", ""))
|
||||||
|
note = clean_cell(row.get("说明", ""))
|
||||||
|
self.data.lights.append(LightItem(
|
||||||
|
light_type=light_type, count=count,
|
||||||
|
position=position, params=params, note=note
|
||||||
|
))
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# 辅助方法
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
def _find_section(self, lines: list[str], keyword: str) -> int:
|
||||||
|
"""找到包含关键词的章节标题行。"""
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if keyword in line:
|
||||||
|
return i
|
||||||
|
return -1
|
||||||
|
|
||||||
|
def _find_zone(self, zone_id: str) -> Optional[ZoneInfo]:
|
||||||
|
"""查找区域信息。"""
|
||||||
|
for z in self.data.zones:
|
||||||
|
if z.zone_id == zone_id:
|
||||||
|
return z
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _extract_tables(self, lines: list[str]) -> list[list[dict]]:
|
||||||
|
"""从行列表中提取所有表格。"""
|
||||||
|
tables = []
|
||||||
|
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])
|
||||||
|
i += 1
|
||||||
|
rows = parse_table(table_lines)
|
||||||
|
if rows:
|
||||||
|
tables.append(rows)
|
||||||
|
continue
|
||||||
|
i += 1
|
||||||
|
return tables
|
||||||
|
|
||||||
|
def _skip_blank(self, lines: list[str], i: int) -> int:
|
||||||
|
"""跳过空行,返回第一个非空行的索引。"""
|
||||||
|
while i < len(lines) and not lines[i].strip():
|
||||||
|
i += 1
|
||||||
|
return i
|
||||||
|
|
||||||
|
def _read_table(self, lines: list[str], i: int) -> list[dict]:
|
||||||
|
"""从第 i 行开始读取一个 Markdown 表格。"""
|
||||||
|
table_lines = []
|
||||||
|
while i < len(lines) and '|' in lines[i]:
|
||||||
|
table_lines.append(lines[i])
|
||||||
|
i += 1
|
||||||
|
if not table_lines:
|
||||||
|
return []
|
||||||
|
return parse_table(table_lines)
|
||||||
|
|
||||||
|
def _advance_past_table(self, lines: list[str], i: int) -> int:
|
||||||
|
"""将索引推进到表格之后。"""
|
||||||
|
while i < len(lines) and '|' in lines[i]:
|
||||||
|
i += 1
|
||||||
|
return i
|
||||||
|
|
||||||
|
def _parse_xyz(self, text: str) -> tuple:
|
||||||
|
"""解析 (X,Y,Z) 或 (ΔX,ΔY,ΔZ) 格式。"""
|
||||||
|
if not text:
|
||||||
|
return (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)
|
||||||
|
|
||||||
|
def _parse_size(self, text: str) -> tuple:
|
||||||
|
"""解析 尺寸格式:宽x深x高。支持 '800x20x250' 或 '800×20×250'。"""
|
||||||
|
if not text:
|
||||||
|
return (0, 0, 0)
|
||||||
|
m = re.search(r'(\d+(?:\.\d+)?)\s*[x×]\s*(\d+(?:\.\d+)?)\s*[x×]\s*(\d+(?:\.\d+)?)', text)
|
||||||
|
if m:
|
||||||
|
return (float(m.group(1)), float(m.group(2)), float(m.group(3)))
|
||||||
|
return (0, 0, 0)
|
||||||
@@ -0,0 +1,596 @@
|
|||||||
|
"""搭建指南 Markdown 生成器。
|
||||||
|
|
||||||
|
将 ActorSpec 列表转换为搭建指南 Markdown 文档。
|
||||||
|
输出格式精确匹配 build_guide_parser.py 的解析预期。
|
||||||
|
|
||||||
|
搭建指南结构(8章):
|
||||||
|
〇、全局信息(场景参数、路径前缀、命名规范、Outliner 结构)
|
||||||
|
一、BSP 资产映射表
|
||||||
|
二、Phase B 地面与边界
|
||||||
|
三、Phase C 逐区域搭建
|
||||||
|
四、Phase C 全局物件
|
||||||
|
五、Phase D 光照
|
||||||
|
六、Phase E 验证清单
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from dataclasses import asdict
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from execution_parser import (
|
||||||
|
ExecutionPlanData, ZoneInfo, StructureItem, PropItem,
|
||||||
|
CompositeItem, GlobalObject, BoundaryItem, LightItem,
|
||||||
|
)
|
||||||
|
from asset_expander import AssetExpander, ActorSpec
|
||||||
|
|
||||||
|
logger = logging.getLogger("execution_to_guide.writer")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 搭建指南生成器
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
class GuideWriter:
|
||||||
|
"""搭建指南 Markdown 生成器。"""
|
||||||
|
|
||||||
|
def __init__(self, plan_data: ExecutionPlanData):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
plan_data: 解析后的执行案数据
|
||||||
|
"""
|
||||||
|
self.plan = plan_data
|
||||||
|
self.expander = AssetExpander(plan_data.zones)
|
||||||
|
self.all_actors: list[ActorSpec] = []
|
||||||
|
self.asset_refs: dict[str, dict] = {} # 用到的资产 key → info
|
||||||
|
|
||||||
|
def generate(self) -> str:
|
||||||
|
"""生成完整的搭建指南 Markdown。"""
|
||||||
|
sections = []
|
||||||
|
|
||||||
|
# 〇、全局信息
|
||||||
|
sections.append(self._write_global_info())
|
||||||
|
|
||||||
|
# 一、BSP 资产映射表
|
||||||
|
sections.append(self._write_asset_mapping())
|
||||||
|
|
||||||
|
# 二、Phase B — 地面与边界
|
||||||
|
sections.append(self._write_ground_and_boundary())
|
||||||
|
|
||||||
|
# 三、Phase C — 逐区域搭建
|
||||||
|
sections.append(self._write_zone_builds())
|
||||||
|
|
||||||
|
# 四、Phase C — 全局物件
|
||||||
|
sections.append(self._write_global_objects())
|
||||||
|
|
||||||
|
# 五、Phase D — 光照
|
||||||
|
sections.append(self._write_lighting())
|
||||||
|
|
||||||
|
# 六、Phase E — 验证清单
|
||||||
|
sections.append(self._write_checklist())
|
||||||
|
|
||||||
|
return "\n\n".join(sections)
|
||||||
|
|
||||||
|
def write_to_file(self, filepath: str):
|
||||||
|
"""生成并写入文件。"""
|
||||||
|
content = self.generate()
|
||||||
|
path = Path(filepath)
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(content, encoding="utf-8")
|
||||||
|
logger.info(f"搭建指南已写入: {filepath}")
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# 〇、全局信息
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
def _write_global_info(self) -> str:
|
||||||
|
"""生成全局信息章节。"""
|
||||||
|
w, d = self.plan.scene_size
|
||||||
|
lines = [
|
||||||
|
f"# v{self.plan.version} 白模搭建操作指南 — {self.plan.scene_name}",
|
||||||
|
"",
|
||||||
|
f"> 本文档由 execution-to-guide 工具自动生成,供 level-builder 搭建白模关卡使用。",
|
||||||
|
f"> 基于: 关卡-v{self.plan.version}-{self.plan.scene_name}-执行案.md",
|
||||||
|
"",
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
"## 〇、全局信息",
|
||||||
|
"",
|
||||||
|
"### 场景参数",
|
||||||
|
"",
|
||||||
|
"| 参数 | 值 |",
|
||||||
|
"|------|-----|",
|
||||||
|
f"| 场景总尺寸 | {w}cm × {d}cm({w // 100}m × {d // 100}m) |",
|
||||||
|
f"| 地面高度基准 | Z = {int(self.plan.ground_z)} |",
|
||||||
|
f"| 主角身高 | ~{int(self.plan.character_height)}cm |",
|
||||||
|
f"| 摄像机高度 | ~{int(self.plan.camera_height)}cm |",
|
||||||
|
f"| 时间设定 | {self.plan.time_setting} |",
|
||||||
|
"| 坐标原点 | 场景西南角地平面 |",
|
||||||
|
"",
|
||||||
|
"### UE5 路径前缀",
|
||||||
|
"",
|
||||||
|
"- **Mesh**: `Content/BlockingStarterPack/Meshes/`",
|
||||||
|
"- **Material**: `Content/BlockingStarterPack/Materials/`",
|
||||||
|
"- **Material (Basics)**: `Content/BlockingStarterPack/Materials/Blocking/Basics/`",
|
||||||
|
"",
|
||||||
|
"### 命名规范",
|
||||||
|
"",
|
||||||
|
"```",
|
||||||
|
"LVL_<ZoneID>_<Category>_<Description>",
|
||||||
|
"Category: STR | PROP | COMP | BND | LGT",
|
||||||
|
"```",
|
||||||
|
"",
|
||||||
|
"### Outliner 文件夹结构",
|
||||||
|
"",
|
||||||
|
"```",
|
||||||
|
f"LVL_{self._level_slug()}/",
|
||||||
|
]
|
||||||
|
|
||||||
|
for zone in self.plan.zones:
|
||||||
|
zone_slug = f"{zone.zone_id}_{zone.zone_name}"
|
||||||
|
lines.append(f" {zone_slug}/")
|
||||||
|
lines.append(" Structure/ Props/")
|
||||||
|
|
||||||
|
lines.append(" Global/")
|
||||||
|
lines.append(" Boundaries/ Lighting/ PlayerStart/")
|
||||||
|
lines.append("```")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# 一、BSP 资产映射表
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
def _write_asset_mapping(self) -> str:
|
||||||
|
"""收集用到的所有资产,生成映射表。"""
|
||||||
|
# 先展开所有 Actor 以收集资产引用
|
||||||
|
self._collect_all_actors()
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
"## 一、BSP 资产映射表",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
|
||||||
|
# 按类别分组
|
||||||
|
categories = {}
|
||||||
|
for key, info in self.asset_refs.items():
|
||||||
|
cat = info.get("category", "other")
|
||||||
|
if cat not in categories:
|
||||||
|
categories[cat] = []
|
||||||
|
categories[cat].append((key, info))
|
||||||
|
|
||||||
|
# 类别标题映射
|
||||||
|
cat_titles = {
|
||||||
|
"wall": "建筑 — 墙体",
|
||||||
|
"wall_door": "建筑 — 门墙",
|
||||||
|
"wall_window": "建筑 — 窗墙",
|
||||||
|
"floor": "建筑 — 地板",
|
||||||
|
"door": "建筑 — 门",
|
||||||
|
"window": "建筑 — 窗户",
|
||||||
|
"barrier": "建筑 — 围挡",
|
||||||
|
"fence": "建筑 — 围栏",
|
||||||
|
"beam": "建筑 — 横梁",
|
||||||
|
"building": "建筑 — 大型建筑",
|
||||||
|
"furniture": "家具",
|
||||||
|
"industrial": "工业/装饰",
|
||||||
|
"vehicle": "载具",
|
||||||
|
"primitive": "基础体",
|
||||||
|
"vegetation": "植被",
|
||||||
|
"light_fixture": "灯具",
|
||||||
|
"decoration": "装饰",
|
||||||
|
"other": "其他",
|
||||||
|
}
|
||||||
|
|
||||||
|
for cat_key in ["wall", "wall_door", "wall_window", "floor", "door", "window",
|
||||||
|
"barrier", "fence", "beam", "building", "furniture",
|
||||||
|
"industrial", "vehicle", "vegetation", "light_fixture",
|
||||||
|
"decoration", "other"]:
|
||||||
|
if cat_key not in categories:
|
||||||
|
continue
|
||||||
|
items = categories[cat_key]
|
||||||
|
title = cat_titles.get(cat_key, cat_key)
|
||||||
|
|
||||||
|
lines.append(f"### {title}")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |")
|
||||||
|
lines.append("|-------------|-------------|--------------|")
|
||||||
|
|
||||||
|
for key, info in items:
|
||||||
|
path_short = info.get("path", "")
|
||||||
|
# 缩短路径显示
|
||||||
|
if path_short.startswith("/Game/BlockingStarterPack/Meshes/"):
|
||||||
|
path_short = path_short.replace("/Game/BlockingStarterPack/Meshes/", "")
|
||||||
|
path_short = path_short.split(".")[0] # 去掉 .SM_xxx 后缀
|
||||||
|
|
||||||
|
bounds = info.get("actual_bounds", ((0, 0, 0), (0, 0, 0)))
|
||||||
|
size_str = self._format_bounds_size(bounds)
|
||||||
|
|
||||||
|
lines.append(f"| {key} | `{path_short}` | {size_str} |")
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# 二、Phase B — 地面与边界
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
def _write_ground_and_boundary(self) -> str:
|
||||||
|
"""生成地面铺设和边界围挡。"""
|
||||||
|
lines = [
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
"## 二、Phase B — 地面与边界",
|
||||||
|
"",
|
||||||
|
"### B1. 铺设地面",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
|
||||||
|
# 全局地面
|
||||||
|
w, d = self.plan.scene_size
|
||||||
|
lines.append("#### 全局地面(MI_DevGray)")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 缩放 | 材质 |")
|
||||||
|
lines.append("|---|-----------|------|-------------|------|------|")
|
||||||
|
|
||||||
|
# 用 Floor_1000x1000 铺满
|
||||||
|
tiles_x = max(1, w // 1000)
|
||||||
|
tiles_y = max(1, d // 1000)
|
||||||
|
seq = 1
|
||||||
|
for tx in range(tiles_x):
|
||||||
|
for ty in range(tiles_y):
|
||||||
|
cx = tx * 1000 + 500
|
||||||
|
cy = ty * 1000 + 500
|
||||||
|
lines.append(
|
||||||
|
f"| {seq} | LVL_Global_STR_GroundFloor_{seq:02d} | Floor_1000x1000 | "
|
||||||
|
f"({cx},{cy},{int(self.plan.ground_z)}) | — | MI_DevGray |"
|
||||||
|
)
|
||||||
|
seq += 1
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# 边界围挡
|
||||||
|
lines.append("### B2. 放置边界围挡")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
for boundary in self.plan.boundaries:
|
||||||
|
seg = boundary.segment
|
||||||
|
lines.append(f"#### {seg}")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 旋转 (P,Y,R) | 缩放 | 材质 |")
|
||||||
|
lines.append("|---|-----------|------|-------------|-------------|------|------|")
|
||||||
|
|
||||||
|
# 根据边界方向生成围挡
|
||||||
|
bnd_actors = self._generate_boundary_actors(boundary)
|
||||||
|
for i, actor in enumerate(bnd_actors):
|
||||||
|
rot_str = self._fmt_rotation(actor.rotation)
|
||||||
|
scale_str = self._fmt_scale(actor.scale)
|
||||||
|
lines.append(
|
||||||
|
f"| {i + 1} | {actor.actor_name} | {actor.asset_key} | "
|
||||||
|
f"{self._fmt_location(actor.location)} | {rot_str} | {scale_str} | "
|
||||||
|
f"{actor.material} |"
|
||||||
|
)
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def _generate_boundary_actors(self, boundary: BoundaryItem) -> list[ActorSpec]:
|
||||||
|
"""根据边界描述生成围挡 Actor。"""
|
||||||
|
w, d = self.plan.scene_size
|
||||||
|
actors = []
|
||||||
|
|
||||||
|
# 确定边界的起止坐标和方向
|
||||||
|
if "西" in boundary.segment:
|
||||||
|
# X=0, 沿 Y 轴
|
||||||
|
x, y_start, y_end = 0, 0, d
|
||||||
|
yaw = 90.0
|
||||||
|
elif "东" in boundary.segment:
|
||||||
|
x, y_start, y_end = w, 0, d
|
||||||
|
yaw = 90.0
|
||||||
|
elif "南" in boundary.segment:
|
||||||
|
x_start, x_end, y = 0, w, 0
|
||||||
|
yaw = 0.0
|
||||||
|
elif "北" in boundary.segment:
|
||||||
|
x_start, x_end, y = 0, w, d
|
||||||
|
yaw = 0.0
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# 使用 BarrierWall_600 或 Wall_400x300
|
||||||
|
asset_key = "BarrierWall_600"
|
||||||
|
wall_len = 600 # cm
|
||||||
|
|
||||||
|
seq = 1
|
||||||
|
if "西" in boundary.segment or "东" in boundary.segment:
|
||||||
|
length = y_end - y_start
|
||||||
|
count = max(1, int(length / wall_len))
|
||||||
|
for i in range(count):
|
||||||
|
y_pos = y_start + i * wall_len + wall_len // 2
|
||||||
|
z_pos = 105 # 半高
|
||||||
|
actors.append(ActorSpec(
|
||||||
|
actor_name=f"LVL_Global_BND_{boundary.segment}_{seq:02d}",
|
||||||
|
asset_key=asset_key,
|
||||||
|
asset_path="",
|
||||||
|
location=(x, y_pos, z_pos),
|
||||||
|
rotation=(0.0, yaw, 0.0),
|
||||||
|
scale=(1.0, 1.0, 1.0),
|
||||||
|
material=boundary.tag_color or "MI_DevBrown",
|
||||||
|
material_path="",
|
||||||
|
category="BND",
|
||||||
|
zone_id="GLOBAL",
|
||||||
|
folder="LVL_Level/Global/Boundaries",
|
||||||
|
))
|
||||||
|
seq += 1
|
||||||
|
else:
|
||||||
|
length = x_end - x_start
|
||||||
|
count = max(1, int(length / wall_len))
|
||||||
|
for i in range(count):
|
||||||
|
x_pos = x_start + i * wall_len + wall_len // 2
|
||||||
|
y_pos = y
|
||||||
|
z_pos = 105
|
||||||
|
actors.append(ActorSpec(
|
||||||
|
actor_name=f"LVL_Global_BND_{boundary.segment}_{seq:02d}",
|
||||||
|
asset_key=asset_key,
|
||||||
|
asset_path="",
|
||||||
|
location=(x_pos, y_pos, z_pos),
|
||||||
|
rotation=(0.0, yaw, 0.0),
|
||||||
|
scale=(1.0, 1.0, 1.0),
|
||||||
|
material=boundary.tag_color or "MI_DevBrown",
|
||||||
|
material_path="",
|
||||||
|
category="BND",
|
||||||
|
zone_id="GLOBAL",
|
||||||
|
folder="LVL_Level/Global/Boundaries",
|
||||||
|
))
|
||||||
|
seq += 1
|
||||||
|
|
||||||
|
return actors
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# 三、Phase C — 逐区域搭建
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
def _write_zone_builds(self) -> str:
|
||||||
|
"""生成逐区域搭建章节。"""
|
||||||
|
lines = [
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
"## 三、Phase C — 逐区域搭建",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
|
||||||
|
for idx, zone in enumerate(self.plan.zones, 1):
|
||||||
|
lines.append(f"### C{idx}. {zone.zone_id} — {zone.zone_name}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# 结构体
|
||||||
|
structures = self.plan.zone_structures.get(zone.zone_id, [])
|
||||||
|
if structures:
|
||||||
|
lines.append(f"#### C{idx}.1 结构体(Structure)")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 旋转 (P,Y,R) | 缩放 | 材质 |")
|
||||||
|
lines.append("|---|-----------|------|-------------|-------------|------|------|")
|
||||||
|
|
||||||
|
for item in structures:
|
||||||
|
actors = self.expander.expand_structure(item, zone.zone_id)
|
||||||
|
for actor in actors:
|
||||||
|
rot_str = self._fmt_rotation(actor.rotation)
|
||||||
|
scale_str = self._fmt_scale(actor.scale)
|
||||||
|
lines.append(
|
||||||
|
f"| | {actor.actor_name} | {actor.asset_key} | "
|
||||||
|
f"{self._fmt_location(actor.location)} | {rot_str} | {scale_str} | "
|
||||||
|
f"{actor.material} |"
|
||||||
|
)
|
||||||
|
self._track_asset(actor)
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# 布景
|
||||||
|
props = self.plan.zone_props.get(zone.zone_id, [])
|
||||||
|
if props:
|
||||||
|
lines.append(f"#### C{idx}.2 布景物(Props)")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 缩放 | 材质 | 说明 |")
|
||||||
|
lines.append("|---|-----------|------|-------------|------|------|------|")
|
||||||
|
|
||||||
|
for item in props:
|
||||||
|
actors = self.expander.expand_prop(item, zone.zone_id)
|
||||||
|
for actor in actors:
|
||||||
|
scale_str = self._fmt_scale(actor.scale)
|
||||||
|
lines.append(
|
||||||
|
f"| | {actor.actor_name} | {actor.asset_key} | "
|
||||||
|
f"{self._fmt_location(actor.location)} | {scale_str} | "
|
||||||
|
f"{actor.material} | {actor.note} |"
|
||||||
|
)
|
||||||
|
self._track_asset(actor)
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# 组合资产(如有)
|
||||||
|
composites = self.plan.zone_composites.get(zone.zone_id, [])
|
||||||
|
if composites:
|
||||||
|
lines.append(f"#### C{idx}.3 组合资产(Composite)")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("| # | Actor 名称 | Primitives | 位置 (X,Y,Z) | 缩放 | 材质 | 说明 |")
|
||||||
|
lines.append("|---|-----------|-----------|-------------|------|------|------|")
|
||||||
|
for comp in composites:
|
||||||
|
lines.append(
|
||||||
|
f"| | ⚠️ {comp.name} | {comp.build_plan} | "
|
||||||
|
f"(待展开) | — | — | {comp.note} |"
|
||||||
|
)
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# 四、全局物件
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
def _write_global_objects(self) -> str:
|
||||||
|
"""生成全局物件章节。"""
|
||||||
|
lines = [
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
"## 四、Phase C — 全局物件",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
|
||||||
|
if not self.plan.global_objects:
|
||||||
|
lines.append("> 无全局物件。")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
lines.append("| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 缩放 | 材质 | 说明 |")
|
||||||
|
lines.append("|---|-----------|------|-------------|------|------|------|")
|
||||||
|
|
||||||
|
for item in self.plan.global_objects:
|
||||||
|
actors = self.expander.expand_global(item)
|
||||||
|
for actor in actors:
|
||||||
|
scale_str = self._fmt_scale(actor.scale)
|
||||||
|
lines.append(
|
||||||
|
f"| | {actor.actor_name} | {actor.asset_key} | "
|
||||||
|
f"{self._fmt_location(actor.location)} | {scale_str} | "
|
||||||
|
f"{actor.material} | {actor.note} |"
|
||||||
|
)
|
||||||
|
self._track_asset(actor)
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# 五、光照
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
def _write_lighting(self) -> str:
|
||||||
|
"""生成光照章节。"""
|
||||||
|
lines = [
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
"## 五、Phase D — 光照",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
|
||||||
|
for light in self.plan.lights:
|
||||||
|
if "定向" in light.light_type or "Directional" in light.light_type:
|
||||||
|
lines.append("### D1. 定向光(Directional Light)")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("| 类型 | 位置/方向 | 参数 |")
|
||||||
|
lines.append("|------|---------|------|")
|
||||||
|
lines.append(f"| Directional Light | {light.position} | {light.params} |")
|
||||||
|
lines.append(f"> {light.note}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
elif "天空" in light.light_type or "Sky" in light.light_type:
|
||||||
|
lines.append("### D2. 天空光(Sky Light)")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("| 类型 | 位置/方向 | 参数 |")
|
||||||
|
lines.append("|------|---------|------|")
|
||||||
|
lines.append(f"| Sky Light | 全局 | {light.params} |")
|
||||||
|
lines.append(f"> {light.note}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
elif "路灯" in light.light_type or "点光" in light.light_type:
|
||||||
|
lines.append("### D3. 路灯点光源")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(f"> {light.note},数量: {light.count}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# 六、验证清单
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
def _write_checklist(self) -> str:
|
||||||
|
"""生成验证清单章节。"""
|
||||||
|
total = len(self.all_actors)
|
||||||
|
zones = len(self.plan.zones)
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
"## 六、Phase E — 验证清单",
|
||||||
|
"",
|
||||||
|
"| # | 检查项 | 状态 |",
|
||||||
|
"|---|--------|------|",
|
||||||
|
f"| 1 | 场景尺寸 {self.plan.scene_size[0]}×{self.plan.scene_size[1]}cm 正确 | ⬜ |",
|
||||||
|
f"| 2 | {zones} 个区域均已搭建 | ⬜ |",
|
||||||
|
"| 3 | 所有边界围挡无缺口 | ⬜ |",
|
||||||
|
"| 4 | 玩家起点可到达 | ⬜ |",
|
||||||
|
"| 5 | 所有区域可通行 | ⬜ |",
|
||||||
|
"| 6 | 建筑入口与流线对齐 | ⬜ |",
|
||||||
|
"| 7 | 光照方向与场景一致 | ⬜ |",
|
||||||
|
f"| 8 | Actor 总数: {total} | ⬜ |",
|
||||||
|
"",
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
f"> 搭建指南生成完成。共 {zones} 个区域,{total} 个 Actor。",
|
||||||
|
]
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# 辅助方法
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
def _collect_all_actors(self):
|
||||||
|
"""收集所有区域的 Actor(用于资产映射表)。"""
|
||||||
|
if self.all_actors:
|
||||||
|
return # 已收集
|
||||||
|
|
||||||
|
# 结构体
|
||||||
|
for zone_id, items in self.plan.zone_structures.items():
|
||||||
|
for item in items:
|
||||||
|
actors = self.expander.expand_structure(item, zone_id)
|
||||||
|
self.all_actors.extend(actors)
|
||||||
|
for a in actors:
|
||||||
|
self._track_asset(a)
|
||||||
|
|
||||||
|
# 布景
|
||||||
|
for zone_id, items in self.plan.zone_props.items():
|
||||||
|
for item in items:
|
||||||
|
actors = self.expander.expand_prop(item, zone_id)
|
||||||
|
self.all_actors.extend(actors)
|
||||||
|
for a in actors:
|
||||||
|
self._track_asset(a)
|
||||||
|
|
||||||
|
# 全局物件
|
||||||
|
for item in self.plan.global_objects:
|
||||||
|
actors = self.expander.expand_global(item)
|
||||||
|
self.all_actors.extend(actors)
|
||||||
|
for a in actors:
|
||||||
|
self._track_asset(a)
|
||||||
|
|
||||||
|
def _track_asset(self, actor: ActorSpec):
|
||||||
|
"""跟踪用到的资产。"""
|
||||||
|
if actor.asset_key and actor.asset_key not in self.asset_refs:
|
||||||
|
try:
|
||||||
|
from config.asset_mapping import MESH_ASSETS
|
||||||
|
info = MESH_ASSETS.get(actor.asset_key, {})
|
||||||
|
except ImportError:
|
||||||
|
info = {}
|
||||||
|
self.asset_refs[actor.asset_key] = info
|
||||||
|
|
||||||
|
def _level_slug(self) -> str:
|
||||||
|
"""生成关卡标识符。"""
|
||||||
|
name = self.plan.scene_name.replace(" ", "")
|
||||||
|
return name
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _fmt_location(loc: tuple) -> str:
|
||||||
|
"""格式化位置。"""
|
||||||
|
return f"({int(loc[0])},{int(loc[1])},{int(loc[2])})"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _fmt_rotation(rot: tuple) -> str:
|
||||||
|
"""格式化旋转。"""
|
||||||
|
if rot == (0, 0, 0):
|
||||||
|
return "—"
|
||||||
|
return f"({int(rot[0])},{int(rot[1])},{int(rot[2])})"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _fmt_scale(scale: tuple) -> str:
|
||||||
|
"""格式化缩放。"""
|
||||||
|
if scale == (1, 1, 1):
|
||||||
|
return "—"
|
||||||
|
return f"({scale[0]:.2f},{scale[1]:.2f},{scale[2]:.2f})"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _format_bounds_size(bounds) -> str:
|
||||||
|
"""格式化包围盒尺寸。"""
|
||||||
|
if not bounds or len(bounds) < 2:
|
||||||
|
return "—"
|
||||||
|
mn, mx = bounds[0], bounds[1]
|
||||||
|
w = mx[0] - mn[0] if len(mx) > 0 else 0
|
||||||
|
d = mx[1] - mn[1] if len(mx) > 1 else 0
|
||||||
|
h = mx[2] - mn[2] if len(mx) > 2 else 0
|
||||||
|
return f"{int(w)}×{int(d)}×{int(h)}"
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
REM === ChildHood Input Setup - Create v0.1 InputAction assets + update IMC ===
|
||||||
|
REM
|
||||||
|
REM Usage:
|
||||||
|
REM setup_input.bat (default, editor quits after done)
|
||||||
|
REM setup_input.bat --no-quit (keep editor open)
|
||||||
|
|
||||||
|
set "SCRIPT_DIR=%~dp0"
|
||||||
|
set "PROJECT_DIR=%SCRIPT_DIR%..\.."
|
||||||
|
for %%i in ("%PROJECT_DIR%\.") do set "PROJECT_ROOT=%%~fi"
|
||||||
|
set "UPROJECT=%PROJECT_ROOT%\ChildHood.uproject"
|
||||||
|
|
||||||
|
REM ---- Defaults ----
|
||||||
|
set "NO_QUIT=0"
|
||||||
|
set "DRY_RUN=0"
|
||||||
|
|
||||||
|
REM ---- Parse Arguments ----
|
||||||
|
:parse_args
|
||||||
|
if "%~1"=="" goto done_args
|
||||||
|
if /i "%~1"=="--no-quit" (
|
||||||
|
set "NO_QUIT=1"
|
||||||
|
shift
|
||||||
|
goto parse_args
|
||||||
|
)
|
||||||
|
if /i "%~1"=="--dry-run" (
|
||||||
|
set "DRY_RUN=1"
|
||||||
|
shift
|
||||||
|
goto parse_args
|
||||||
|
)
|
||||||
|
if /i "%~1"=="--help" goto show_help
|
||||||
|
if /i "%~1"=="-h" goto show_help
|
||||||
|
echo [WARN] Unknown argument: %~1
|
||||||
|
shift
|
||||||
|
goto parse_args
|
||||||
|
:done_args
|
||||||
|
|
||||||
|
REM ---- Find UE5 Editor ----
|
||||||
|
set "UE_EDITOR="
|
||||||
|
if defined UE5_ROOT (
|
||||||
|
for %%b in (UnrealEditor.exe UE5Editor.exe) do (
|
||||||
|
if exist "%UE5_ROOT%\Engine\Binaries\Win64\%%b" (
|
||||||
|
set "UE_EDITOR=%UE5_ROOT%\Engine\Binaries\Win64\%%b"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%UE_EDITOR%"=="" (
|
||||||
|
for %%p in (
|
||||||
|
"C:\Program Files\Epic Games\UE_5.7"
|
||||||
|
"C:\Program Files (x86)\Epic Games\UE_5.7"
|
||||||
|
"D:\Program Files\Epic Games\UE_5.7"
|
||||||
|
"E:\Program Files\Epic Games\UE_5.7"
|
||||||
|
) do (
|
||||||
|
if not defined UE_EDITOR (
|
||||||
|
for %%b in (UnrealEditor.exe UE5Editor.exe) do (
|
||||||
|
if exist "%%~p\Engine\Binaries\Win64\%%b" (
|
||||||
|
set "UE_EDITOR=%%~p\Engine\Binaries\Win64\%%b"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%UE_EDITOR%"=="" (
|
||||||
|
echo.
|
||||||
|
echo [ERROR] UE5 Editor not found!
|
||||||
|
echo Set UE5_ROOT, e.g.: set UE5_ROOT=C:\Program Files\Epic Games\UE_5.7
|
||||||
|
echo.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM ---- Path conversion ----
|
||||||
|
set "SCRIPT_PATH=%SCRIPT_DIR%setup_input_assets.py"
|
||||||
|
set "SCRIPT_PATH=!SCRIPT_PATH:\=/!"
|
||||||
|
set "UPROJECT_FWD=!UPROJECT:\=/!"
|
||||||
|
|
||||||
|
REM ---- Build command line ----
|
||||||
|
set "CMD_ARGS=-unattended -NoSound -NullRHI -ExecutePythonScript=!SCRIPT_PATH!"
|
||||||
|
|
||||||
|
REM ---- Pass config via env vars ----
|
||||||
|
set "INPUT_PROJECT_ROOT=!PROJECT_ROOT!"
|
||||||
|
if "!NO_QUIT!"=="1" set "INPUT_NO_QUIT=1"
|
||||||
|
|
||||||
|
REM ---- Print info ----
|
||||||
|
echo.
|
||||||
|
echo ============================================
|
||||||
|
echo ChildHood Input Setup - v0.1
|
||||||
|
echo ============================================
|
||||||
|
echo UE5: !UE_EDITOR!
|
||||||
|
echo Project: !UPROJECT_FWD!
|
||||||
|
echo Script: !SCRIPT_PATH!
|
||||||
|
echo ============================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM ---- Dry Run ----
|
||||||
|
if "%DRY_RUN%"=="1" (
|
||||||
|
echo [DRY RUN] Command:
|
||||||
|
echo.
|
||||||
|
echo "!UE_EDITOR!" "!UPROJECT_FWD!" !CMD_ARGS!
|
||||||
|
echo.
|
||||||
|
exit /b 0
|
||||||
|
)
|
||||||
|
|
||||||
|
REM ---- Execute ----
|
||||||
|
echo [InputSetup] Launching UE5 Editor ...
|
||||||
|
echo.
|
||||||
|
|
||||||
|
"!UE_EDITOR!" "!UPROJECT_FWD!" !CMD_ARGS!
|
||||||
|
|
||||||
|
set "EXIT_CODE=!ERRORLEVEL!"
|
||||||
|
echo.
|
||||||
|
if "!EXIT_CODE!"=="0" (
|
||||||
|
echo [InputSetup] Done. Editor exited.
|
||||||
|
) else (
|
||||||
|
echo [InputSetup] Editor exit code: !EXIT_CODE!
|
||||||
|
)
|
||||||
|
exit /b !EXIT_CODE!
|
||||||
|
|
||||||
|
:show_help
|
||||||
|
echo.
|
||||||
|
echo ChildHood Input Setup - v0.1
|
||||||
|
echo.
|
||||||
|
echo Usage: setup_input.bat [options]
|
||||||
|
echo.
|
||||||
|
echo Options:
|
||||||
|
echo --no-quit Keep editor open after done
|
||||||
|
echo --dry-run Print command without executing
|
||||||
|
echo --help, -h Show help
|
||||||
|
echo.
|
||||||
|
exit /b 0
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
"""UE5 编辑器 Python 脚本:创建 v0.1 角色控制所需的 InputAction 资产并更新 IMC。
|
||||||
|
|
||||||
|
使用方式:
|
||||||
|
方式1 — 在编辑器 Python 控制台运行:
|
||||||
|
py "E:/MiceCola/work/ChildHood/tools/input-setup/setup_input_assets.py"
|
||||||
|
方式2 — 通过 setup_input.bat 启动编辑器执行
|
||||||
|
|
||||||
|
功能:
|
||||||
|
1. 创建 3 个 InputAction 资产:IA_Interact, IA_Cancel, IA_SwitchInteract
|
||||||
|
2. 尝试更新 IMC_Default:添加 E / ESC / Tab 按键映射
|
||||||
|
3. 保存所有修改的资产
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
# ---- 检查 unreal 模块 ----
|
||||||
|
try:
|
||||||
|
import unreal
|
||||||
|
except ImportError:
|
||||||
|
print("[InputSetup] FATAL: unreal 模块不可用,必须在 UE5 编辑器环境中运行")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# ---- 资产路径配置 ----
|
||||||
|
IA_PACKAGE = "/Game/Input/Actions"
|
||||||
|
IMC_PATH = "/Game/Input/IMC_Default"
|
||||||
|
TEMPLATE_IA = f"{IA_PACKAGE}/IA_Look" # 用已存在的 IA 作为复制模板
|
||||||
|
|
||||||
|
# 新增 InputAction 定义(key_name 为字符串,运行时解析)
|
||||||
|
NEW_INPUT_ACTIONS = [
|
||||||
|
{"name": "IA_Interact", "desc": "E 键交互输入动作", "key_name": "E"},
|
||||||
|
{"name": "IA_Cancel", "desc": "ESC 取消输入动作", "key_name": "Escape"},
|
||||||
|
{"name": "IA_SwitchInteract", "desc": "Tab 切换交互目标", "key_name": "Tab"},
|
||||||
|
]
|
||||||
|
|
||||||
|
# 按键名 → UE5 Key 对象的多种尝试路径
|
||||||
|
KEY_RESOLVE_ORDER = ["E", "Escape", "Tab"]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_key(key_name: str):
|
||||||
|
"""尝试多种方式从 UE5 Python API 获取 Key 对象。
|
||||||
|
|
||||||
|
UE5 各版本的 Python 绑定对 Key 的暴露方式不同,
|
||||||
|
这里依次尝试多种路径,找到可用的那一个。
|
||||||
|
"""
|
||||||
|
attempts = [
|
||||||
|
# 方式1: unreal.Key 枚举属性 (如 unreal.Key.E, unreal.Key.Escape)
|
||||||
|
lambda: getattr(unreal.Key, key_name),
|
||||||
|
# 方式2: unreal.Keys 类 (部分版本用这个)
|
||||||
|
lambda: getattr(unreal.Keys, key_name),
|
||||||
|
# 方式3: 全大写 (如 ESCAPE, TAB)
|
||||||
|
lambda: getattr(unreal.Key, key_name.upper()),
|
||||||
|
lambda: getattr(unreal.Keys, key_name.upper()),
|
||||||
|
# 方式4: KEY_ 前缀 (如 KEY_E, KEY_ESCAPE)
|
||||||
|
lambda: getattr(unreal.Key, f"KEY_{key_name.upper()}"),
|
||||||
|
lambda: getattr(unreal.Keys, f"KEY_{key_name.upper()}"),
|
||||||
|
# 方式5: 构造函数 (部分版本支持从字符串构造)
|
||||||
|
lambda: unreal.Key(key_name),
|
||||||
|
]
|
||||||
|
|
||||||
|
for i, attempt in enumerate(attempts, 1):
|
||||||
|
try:
|
||||||
|
key = attempt()
|
||||||
|
if key is not None:
|
||||||
|
print(f"[InputSetup] Key('{key_name}') 解析成功 (方式{i})")
|
||||||
|
return key
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"[InputSetup] Key('{key_name}') 所有解析方式均失败")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def asset_exists(asset_path: str) -> bool:
|
||||||
|
"""检查资产是否已存在"""
|
||||||
|
return unreal.EditorAssetLibrary.does_asset_exist(asset_path)
|
||||||
|
|
||||||
|
|
||||||
|
def create_input_action_by_duplicate(name: str, desc: str) -> object:
|
||||||
|
"""通过复制已有 IA 模板创建新 InputAction 资产。
|
||||||
|
|
||||||
|
这是最可靠的方式——不依赖 Factory 类的 Python 绑定。
|
||||||
|
"""
|
||||||
|
asset_path = f"{IA_PACKAGE}/{name}"
|
||||||
|
|
||||||
|
if asset_exists(asset_path):
|
||||||
|
print(f"[InputSetup] 已存在,跳过: {asset_path}")
|
||||||
|
return unreal.load_asset(asset_path)
|
||||||
|
|
||||||
|
# 确保目标目录存在
|
||||||
|
if not unreal.EditorAssetLibrary.does_directory_exist(IA_PACKAGE):
|
||||||
|
unreal.EditorAssetLibrary.make_directory(IA_PACKAGE)
|
||||||
|
|
||||||
|
# 复制模板资产
|
||||||
|
new_asset = unreal.EditorAssetLibrary.duplicate_asset(TEMPLATE_IA, asset_path)
|
||||||
|
if new_asset:
|
||||||
|
print(f"[InputSetup] 复制创建成功: {asset_path} — {desc}")
|
||||||
|
return new_asset
|
||||||
|
|
||||||
|
print(f"[InputSetup] 复制创建失败: {name}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def create_input_action_by_factory(name: str, desc: str) -> object:
|
||||||
|
"""通过 AssetTools + Factory 创建 InputAction(备用方案)。"""
|
||||||
|
asset_path = f"{IA_PACKAGE}/{name}"
|
||||||
|
|
||||||
|
if asset_exists(asset_path):
|
||||||
|
return unreal.load_asset(asset_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
factory = unreal.InputActionFactory()
|
||||||
|
except Exception:
|
||||||
|
print("[InputSetup] InputActionFactory 不可用")
|
||||||
|
return None
|
||||||
|
|
||||||
|
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
|
||||||
|
if not asset_tools:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# create_asset 的 package_path 需要去掉 /Game/ 前缀
|
||||||
|
new_asset = asset_tools.create_asset(
|
||||||
|
name,
|
||||||
|
IA_PACKAGE[5:], # "Input/Actions"
|
||||||
|
unreal.InputAction,
|
||||||
|
factory
|
||||||
|
)
|
||||||
|
if new_asset:
|
||||||
|
print(f"[InputSetup] Factory 创建成功: {asset_path} — {desc}")
|
||||||
|
return new_asset
|
||||||
|
|
||||||
|
|
||||||
|
def create_input_action(name: str, desc: str) -> object:
|
||||||
|
"""创建 InputAction 资产,依次尝试多种方法。"""
|
||||||
|
asset_path = f"{IA_PACKAGE}/{name}"
|
||||||
|
|
||||||
|
# 已存在则直接加载
|
||||||
|
if asset_exists(asset_path):
|
||||||
|
print(f"[InputSetup] 已存在: {asset_path}")
|
||||||
|
return unreal.load_asset(asset_path)
|
||||||
|
|
||||||
|
print(f"[InputSetup] 尝试创建: {name} — {desc}")
|
||||||
|
|
||||||
|
# 方法1: 复制已有模板(最可靠)
|
||||||
|
result = create_input_action_by_duplicate(name, desc)
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
|
||||||
|
# 方法2: Factory 创建
|
||||||
|
result = create_input_action_by_factory(name, desc)
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
|
||||||
|
print(f"[InputSetup] ERROR: 所有创建方法均失败: {name}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def try_update_imc(created_ias: dict) -> bool:
|
||||||
|
"""尝试更新 IMC_Default 添加按键映射。
|
||||||
|
|
||||||
|
返回 True 表示成功,False 表示需要手动操作。
|
||||||
|
由于 UE5 Python API 对 Enhanced Input 的暴露有限,
|
||||||
|
此功能可能失败——失败时提供手动操作指引。
|
||||||
|
"""
|
||||||
|
print(f"[InputSetup] 加载 IMC: {IMC_PATH}")
|
||||||
|
|
||||||
|
if not asset_exists(IMC_PATH):
|
||||||
|
print(f"[InputSetup] ERROR: IMC 不存在: {IMC_PATH}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
imc = unreal.load_asset(IMC_PATH)
|
||||||
|
if not imc:
|
||||||
|
print(f"[InputSetup] ERROR: 无法加载 IMC")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 读取当前映射数量
|
||||||
|
try:
|
||||||
|
current_mappings = imc.get_editor_property("mappings")
|
||||||
|
print(f"[InputSetup] 当前映射数: {len(current_mappings)}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[InputSetup] 无法读取 mappings 属性: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 解析所有需要的 Key
|
||||||
|
key_map = {}
|
||||||
|
for ia_info in NEW_INPUT_ACTIONS:
|
||||||
|
if ia_info["name"] not in created_ias:
|
||||||
|
continue
|
||||||
|
key = resolve_key(ia_info["key_name"])
|
||||||
|
if key is None:
|
||||||
|
print(f"[InputSetup] 无法解析按键: {ia_info['key_name']},跳过 IMC 自动更新")
|
||||||
|
return False
|
||||||
|
key_map[ia_info["name"]] = key
|
||||||
|
|
||||||
|
# 尝试添加映射
|
||||||
|
success_count = 0
|
||||||
|
for ia_info in NEW_INPUT_ACTIONS:
|
||||||
|
name = ia_info["name"]
|
||||||
|
if name not in created_ias or name not in key_map:
|
||||||
|
continue
|
||||||
|
|
||||||
|
ia = created_ias[name]
|
||||||
|
key = key_map[name]
|
||||||
|
|
||||||
|
try:
|
||||||
|
new_mapping = unreal.InputMapping()
|
||||||
|
new_mapping.set_editor_property("action", ia)
|
||||||
|
|
||||||
|
km = unreal.KeyMapping()
|
||||||
|
km.set_editor_property("key", key)
|
||||||
|
|
||||||
|
# 尝试设置触发器(可能不支持)
|
||||||
|
try:
|
||||||
|
km.set_editor_property("triggers", [unreal.InputTriggerDown()])
|
||||||
|
except Exception:
|
||||||
|
pass # 触发器使用默认值
|
||||||
|
|
||||||
|
new_mapping.set_editor_property("key_mappings", [km])
|
||||||
|
|
||||||
|
current_mappings.append(new_mapping)
|
||||||
|
imc.set_editor_property("mappings", current_mappings)
|
||||||
|
success_count += 1
|
||||||
|
print(f"[InputSetup] 映射已添加: {name} → {ia_info['key_name']}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[InputSetup] 映射添加失败: {name} — {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if success_count == len(created_ias):
|
||||||
|
# 保存 IMC
|
||||||
|
unreal.EditorAssetLibrary.save_asset(IMC_PATH)
|
||||||
|
print(f"[InputSetup] IMC 已保存")
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 60)
|
||||||
|
print("[InputSetup] v0.1 角色控制 Input 资产创建脚本")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# ---- Step 1: 创建 InputAction 资产 ----
|
||||||
|
print("\n[InputSetup] Step 1/4: 创建 InputAction 资产")
|
||||||
|
print(f"[InputSetup] 模板: {TEMPLATE_IA}")
|
||||||
|
|
||||||
|
if not asset_exists(TEMPLATE_IA):
|
||||||
|
print(f"[InputSetup] ERROR: 模板 IA 不存在: {TEMPLATE_IA}")
|
||||||
|
print("[InputSetup] 可用 IA 资产:")
|
||||||
|
# 尝试列出 Actions 目录下的资产作为替代
|
||||||
|
try:
|
||||||
|
assets = unreal.EditorAssetLibrary.list_assets(IA_PACKAGE)
|
||||||
|
for a in assets:
|
||||||
|
print(f"[InputSetup] {a}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
created_ias = {}
|
||||||
|
for ia_info in NEW_INPUT_ACTIONS:
|
||||||
|
ia = create_input_action(ia_info["name"], ia_info["desc"])
|
||||||
|
if ia:
|
||||||
|
created_ias[ia_info["name"]] = ia
|
||||||
|
else:
|
||||||
|
print(f"[InputSetup] WARNING: {ia_info['name']} 创建失败")
|
||||||
|
|
||||||
|
if not created_ias:
|
||||||
|
print("[InputSetup] ERROR: 没有成功创建任何 IA")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ---- Step 2: 保存新创建的 IA 资产 ----
|
||||||
|
print(f"\n[InputSetup] Step 2/4: 保存资产 ({len(created_ias)} 个)")
|
||||||
|
for name in created_ias:
|
||||||
|
asset_path = f"{IA_PACKAGE}/{name}"
|
||||||
|
unreal.EditorAssetLibrary.save_asset(asset_path)
|
||||||
|
print(f"[InputSetup] 已保存: {asset_path}")
|
||||||
|
|
||||||
|
# ---- Step 3: 尝试更新 IMC ----
|
||||||
|
print(f"\n[InputSetup] Step 3/4: 更新 IMC_Default")
|
||||||
|
imc_ok = False
|
||||||
|
try:
|
||||||
|
imc_ok = try_update_imc(created_ias)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[InputSetup] IMC 更新异常: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
if not imc_ok:
|
||||||
|
print("[InputSetup] ⚠️ IMC 自动更新未成功,需要手动操作(见下方指引)")
|
||||||
|
|
||||||
|
# ---- Step 4: 验证 ----
|
||||||
|
print(f"\n[InputSetup] Step 4/4: 验证")
|
||||||
|
all_ok = True
|
||||||
|
for ia_info in NEW_INPUT_ACTIONS:
|
||||||
|
asset_path = f"{IA_PACKAGE}/{ia_info['name']}"
|
||||||
|
exists = asset_exists(asset_path)
|
||||||
|
status = "✅" if exists else "❌"
|
||||||
|
print(f"[InputSetup] {status} {ia_info['name']}")
|
||||||
|
if not exists:
|
||||||
|
all_ok = False
|
||||||
|
|
||||||
|
# ---- 总结 ----
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
if imc_ok:
|
||||||
|
print("[InputSetup] ✅ 全部完成(IA 创建 + IMC 更新)")
|
||||||
|
print("[InputSetup] ⚠️ 仍需手动:BP_ChildHoodCharacter → Details → Input → 绑定 IA 属性")
|
||||||
|
else:
|
||||||
|
print("[InputSetup] ⚠️ IA 资产已创建,IMC 需手动配置")
|
||||||
|
print()
|
||||||
|
print("[InputSetup] === 手动操作指引 ===")
|
||||||
|
print()
|
||||||
|
print("1. 打开 IMC_Default (Content/Input/IMC_Default)")
|
||||||
|
print(" 添加 3 个映射:")
|
||||||
|
for ia_info in NEW_INPUT_ACTIONS:
|
||||||
|
print(f" - {ia_info['key_name']} → {ia_info['name']}")
|
||||||
|
print()
|
||||||
|
print("2. 打开 BP_ChildHoodCharacter (Content/BP/Character/)")
|
||||||
|
print(" Details → Input → 设置以下属性:")
|
||||||
|
print(" - Interact Action → IA_Interact")
|
||||||
|
print(" - Cancel Action → IA_Cancel")
|
||||||
|
print(" - Switch Interact Action → IA_SwitchInteract")
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
success = main()
|
||||||
|
if not success:
|
||||||
|
sys.exit(1)
|
||||||
+130
-56
@@ -3,17 +3,66 @@
|
|||||||
> 在场景概念与关卡执行案之间,提供可视化布局沟通工具。
|
> 在场景概念与关卡执行案之间,提供可视化布局沟通工具。
|
||||||
> 用颜色画笔绘制世界布局 → AI 解读 → 双方确认 → 产出执行案。
|
> 用颜色画笔绘制世界布局 → AI 解读 → 双方确认 → 产出执行案。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
1. 双击 `sketch.html` 在浏览器中打开(推荐 Chrome / Edge)
|
### 方式一:本地服务器(推荐)
|
||||||
2. 左侧设置场景名称和世界尺寸(米)
|
|
||||||
3. 选择画笔,在画布上绘制
|
保存/加载直接读写 `output/` 目录,无需手动下载。
|
||||||
4. **双击** 已绘制区域 → 弹出命名对话框:
|
|
||||||
|
1. 双击 `start.bat`
|
||||||
|
2. 浏览器自动打开 `http://localhost:8765/sketch.html`
|
||||||
|
3. 开始绘制,点"保存"直接存入 `output/`
|
||||||
|
|
||||||
|
> 需要 Python 3。关闭终端窗口即可停止服务器。
|
||||||
|
|
||||||
|
### 方式二:直接打开 HTML
|
||||||
|
|
||||||
|
双击 `sketch.html` 在浏览器中打开(推荐 Chrome / Edge)。
|
||||||
|
|
||||||
|
- 保存时通过浏览器下载 ZIP 文件
|
||||||
|
- 加载时通过浏览器文件选择器选择 ZIP
|
||||||
|
|
||||||
|
> **注意**:直接打开 HTML 时加载独立的 `.json` 文件**不会恢复画布位图**——位图数据存储在 ZIP 包内的 PNG 文件中,必须加载 `.zip` 文件才能完整恢复。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
world-sketcher/
|
||||||
|
├── start.bat ← 启动脚本(双击运行)
|
||||||
|
├── server.py ← 本地保存/加载服务器
|
||||||
|
├── sketch.html ← 主工具(单文件,浏览器运行)
|
||||||
|
├── README.md
|
||||||
|
└── output/ ← 保存/加载目录
|
||||||
|
├── 迈皋桥-scene.zip ← 完整场景包(JSON + 所有 PNG)
|
||||||
|
├── 迈皋桥-scene.json ← 结构化数据(不含位图)
|
||||||
|
├── 迈皋桥-L1-drawing.png # L1 画布像素数据
|
||||||
|
├── 迈皋桥-L1-overview-preview.png # L1 合并预览
|
||||||
|
└── 迈皋桥-L2-*-drawing.png # L2 各区域画布数据
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 基本操作
|
||||||
|
|
||||||
|
1. 左侧设置**场景名称**和**世界尺寸**(米)
|
||||||
|
2. 选择画笔,在画布上绘制
|
||||||
|
3. **双击** 已绘制区域 → 弹出命名对话框:
|
||||||
- 输入名称(如"家"、"小卖部")
|
- 输入名称(如"家"、"小卖部")
|
||||||
- 设置**高度**(建筑/区域/不可通行类型时显示,默认 3m)
|
- **L1 区域**:设置**地形标高**(米,可为负值表示洼地)
|
||||||
- 勾选**连通到**其他已命名区域(建立区域间连接关系)
|
- **L2 建筑**:设置**建筑高度**(米)
|
||||||
|
- 勾选**连通到**其他已命名区域(建立连接关系)
|
||||||
- 勾选「可进入」→ 创建下级详图(L1 → L2)
|
- 勾选「可进入」→ 创建下级详图(L1 → L2)
|
||||||
5. 保存 ZIP(含 PNG + JSON)
|
4. 保存 → 存入 `output/`(服务器模式)或浏览器下载(离线模式)
|
||||||
|
|
||||||
|
### 画布标签
|
||||||
|
|
||||||
|
- **L1 层**:显示区域名称 + 地形标高(如 `棚户区 (标高2m)`)
|
||||||
|
- **L2 层**:显示建筑名称 + 建筑高度(如 `棚户1 (3m)`)
|
||||||
|
- 标高为 0 时不显示高度
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -30,36 +79,30 @@
|
|||||||
- L1 画布上,双击已命名的区域并勾选「可进入」
|
- L1 画布上,双击已命名的区域并勾选「可进入」
|
||||||
- 左侧元素列表中点击 **"进入 →"**
|
- 左侧元素列表中点击 **"进入 →"**
|
||||||
- 面包屑导航点击 **"场景总览"** 返回上级
|
- 面包屑导航点击 **"场景总览"** 返回上级
|
||||||
|
- 返回 L1 时,L2 的绘制内容会自动合成回 L1 对应区域
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 画笔图例
|
## 画笔图例
|
||||||
|
|
||||||
| 画笔 | 颜色 | 用途 | 对应 UE 材质 | 默认高度 |
|
| 画笔 | 颜色 | 用途 | 对应 UE 材质 | 可用层级 | 高度含义 |
|
||||||
|------|------|------|-------------|---------|
|
|------|------|------|-------------|---------|---------|
|
||||||
| 道路 Road | 灰 `#909090` | 道路、马路、小路 | MI_DevGray | — |
|
| 道路 Road | 灰 `#909090` | 道路、马路、小路 | MI_DevGray | L1/L2 | — |
|
||||||
| 建筑 Building | 橙 `#E07020` | 建筑物 | MI_DevOrange | 3m |
|
| 建筑 Building | 橙 `#E07020` | 建筑物 | MI_DevOrange | L2 | 建筑高度 |
|
||||||
| 水域 Water | 天蓝 `#40A0D0` | 河流、水塘、水体 | MI_DevSkyBlue | — |
|
| 水域 Water | 天蓝 `#40A0D0` | 河流、水塘、水体 | MI_DevSkyBlue | L1/L2 | — |
|
||||||
| 植被 Greenery | 绿 `#40B040` | 树木、草地、绿化带 | MI_DevGreen | 2m |
|
| 植被 Greenery | 绿 `#40B040` | 树木、草地、绿化带 | MI_DevGreen | L2 | 2m |
|
||||||
| 不可通行 Blocked | 棕 `#A07040` | 山体、围墙、不可达区域 | MI_DevBrown | 3m |
|
| 不可通行 Blocked | 棕 `#A07040` | 山体、围墙、不可达区域 | MI_DevBrown | L1/L2 | 3m |
|
||||||
| 区域 Zone | 红 `#D04040` | 区域边界、地块划分 | MI_DevRed | 3m |
|
| 区域 Zone | 红 `#D04040` | 区域边界、地块划分 | MI_DevRed | L1 | 地形标高 |
|
||||||
| 橡皮擦 Eraser | — | 擦除 | — | — |
|
| 橡皮擦 Eraser | — | 擦除 | — | L1/L2 | — |
|
||||||
|
|
||||||
**L1 层**主要用:区域、道路、水域
|
|
||||||
**L2 层**主要用:建筑、道路、植被、水域、不可通行
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 绘图工具
|
## 快捷键
|
||||||
|
|
||||||
| 工具 | 快捷键 | 说明 |
|
|
||||||
|------|--------|------|
|
|
||||||
| 画笔 | P | 自由绘制,按住鼠标拖动 |
|
|
||||||
| 矩形 | R | 绘制矩形区域,拖动定义两角 |
|
|
||||||
| 橡皮擦 | E | 擦除画布内容 |
|
|
||||||
|
|
||||||
| 操作 | 快捷键 |
|
| 操作 | 快捷键 |
|
||||||
|------|--------|
|
|------|--------|
|
||||||
|
| 画笔工具 | P |
|
||||||
|
| 橡皮擦工具 | E |
|
||||||
| 撤销 | Ctrl+Z |
|
| 撤销 | Ctrl+Z |
|
||||||
| 重做 | Ctrl+Y |
|
| 重做 | Ctrl+Y |
|
||||||
| 网格开关 | G |
|
| 网格开关 | G |
|
||||||
@@ -74,57 +117,87 @@
|
|||||||
- **单位**:厘米(与 UE5 一致,1 uu = 1 cm)
|
- **单位**:厘米(与 UE5 一致,1 uu = 1 cm)
|
||||||
- 画布顶部 = 世界最大 Y,画布底部 = 世界 Y=0
|
- 画布顶部 = 世界最大 Y,画布底部 = 世界 Y=0
|
||||||
|
|
||||||
状态栏实时显示鼠标位置的世界坐标。
|
状态栏实时显示鼠标位置的像素坐标和世界坐标。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 输出文件
|
## 输出格式
|
||||||
|
|
||||||
保存全部时生成:
|
|
||||||
|
|
||||||
```
|
|
||||||
<场景名>-L1-overview.png # L1 画布截图
|
|
||||||
<场景名>-scene.json # 完整场景数据(所有层级)
|
|
||||||
```
|
|
||||||
|
|
||||||
### scene.json 结构
|
### scene.json 结构
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"scene_name": "城郊小街",
|
"scene_name": "迈皋桥",
|
||||||
"version": "v02",
|
"version": "v02",
|
||||||
|
"created": "2026-06-09T16:46:32",
|
||||||
"origin": "SW corner, X+ East, Y+ North",
|
"origin": "SW corner, X+ East, Y+ North",
|
||||||
"brush_legend": { "色值": { "type": "类型", "mi_material": "MI_Dev*" } },
|
"brush_legend": {
|
||||||
|
"#D04040": { "type": "zone", "label": "区域 Zone", "mi_material": "MI_DevRed" }
|
||||||
|
},
|
||||||
"levels": {
|
"levels": {
|
||||||
"L1": {
|
"L1": {
|
||||||
"world_size_m": [500, 400],
|
"world_size_m": [500, 400],
|
||||||
"canvas_size_px": [1000, 800],
|
"canvas_size_px": [1000, 800],
|
||||||
"scale_m_per_px": 0.5,
|
"scale_m_per_px": 0.5,
|
||||||
"elements": [
|
"elements": [
|
||||||
{ "id": "el_001", "type": "zone", "name": "家", "height_m": 3,
|
{
|
||||||
"connections": ["el_003"], "has_detail": false },
|
"id": "el_001", "type": "zone", "name": "棚户区",
|
||||||
{ "id": "el_002", "type": "zone", "name": "小卖部", "height_m": 3,
|
"height_m": 2, "connections": ["el_002"],
|
||||||
"connections": ["el_003"], "has_detail": false },
|
"has_detail": true, "detail_ref": "L2-el_001"
|
||||||
{ "id": "el_003", "type": "zone", "name": "主街道", "height_m": 3,
|
}
|
||||||
"connections": ["el_001", "el_002", "el_004"], "has_detail": true, "detail_ref": "L2-el_003" }
|
|
||||||
],
|
],
|
||||||
"regions": [
|
"regions": [
|
||||||
{ "id": "r_001", "type": "zone", "name": "家",
|
{
|
||||||
"bounds_world_m": [5.0, 5.0, 12.5, 15.0], "area_px": 15000 }
|
"id": "r_001", "type": "zone", "name": "棚户区",
|
||||||
]
|
"bounds_world_m": [5.0, 5.0, 120.0, 95.0],
|
||||||
|
"area_px": 15000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"image_file": "迈皋桥-L1-drawing.png",
|
||||||
|
"l1_base_file": null
|
||||||
},
|
},
|
||||||
"L2-el_003": {
|
"L2-el_001": {
|
||||||
"world_size_m": [80, 60],
|
"world_size_m": [80, 60],
|
||||||
"elements": [
|
"elements": [
|
||||||
{ "id": "el_yyy", "type": "building", "name": "家", "height_m": 3, "connections": [], "has_detail": false }
|
{ "id": "el_010", "type": "building", "name": "棚户1", "height_m": 3 }
|
||||||
],
|
],
|
||||||
|
"image_file": "迈皋桥-L2-el_001-drawing.png",
|
||||||
|
"l1_base_file": "迈皋桥-L2-el_001-l1base.png",
|
||||||
"parent_level": "L1",
|
"parent_level": "L1",
|
||||||
"parent_element": "el_003"
|
"parent_element": "el_001"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 高度字段语义
|
||||||
|
|
||||||
|
| 层级 | 元素类型 | `height_m` 含义 | 用途 |
|
||||||
|
|------|---------|----------------|------|
|
||||||
|
| L1 | zone(区域) | **地形标高** | 地面海拔相对高度,0=基准面,负值=洼地 |
|
||||||
|
| L2 | building(建筑) | **建筑高度** | 建筑物垂直高度 |
|
||||||
|
| L1/L2 | blocked(不可通行) | **障碍高度** | 围墙/山体等垂直尺度 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 本地服务器
|
||||||
|
|
||||||
|
`server.py` 提供三个 HTTP 端点,供 `sketch.html` 直接读写 `output/` 目录:
|
||||||
|
|
||||||
|
| 端点 | 方法 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/list` | GET | 列出 `output/` 下所有 `.json` / `.zip` 文件 |
|
||||||
|
| `/load/<filename>` | GET | 从 `output/` 读取指定文件 |
|
||||||
|
| `/save` | POST (multipart) | 接收文件并保存到 `output/` |
|
||||||
|
|
||||||
|
服务器运行时,sketch.html 自动检测并使用服务器模式;未运行时自动回退到浏览器下载/文件选择。
|
||||||
|
|
||||||
|
手动启动:
|
||||||
|
```bash
|
||||||
|
python server.py # 默认端口 8765
|
||||||
|
python server.py 9000 # 自定义端口
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 工作流集成
|
## 工作流集成
|
||||||
@@ -145,20 +218,21 @@ AI 布局解读文档 (场景名-layout-vXX.md)
|
|||||||
|
|
||||||
### AI 解读流程
|
### AI 解读流程
|
||||||
|
|
||||||
1. 用户在工具中画好布局,命名区域,设置高度和连通关系,保存 ZIP
|
1. 用户在工具中画好布局,命名区域,设置标高/高度和连通关系,保存
|
||||||
2. AI 读取 JSON 布局数据(elements + regions + connections + height_m)
|
2. AI 读取 JSON 中的 elements + regions + connections + height_m
|
||||||
3. AI 交叉参考场景设计案的氛围描述,将 JSON 数据与场景概念建立映射
|
3. AI 交叉参考场景设计案的氛围描述,将 JSON 数据与场景概念建立映射
|
||||||
4. AI 产出结构化布局解读 Markdown(区域表、连通关系、高度、待确认问题)
|
4. AI 产出结构化布局解读 Markdown(区域表、连通关系、标高、待确认问题)
|
||||||
5. 用户确认后,策划基于解读文档产出关卡执行案
|
5. 用户确认后,策划基于解读文档产出关卡执行案
|
||||||
|
|
||||||
> **说明**:PNG 文件保存在 ZIP 中用于画布的保存/加载,AI 分析以 JSON 结构化数据为准,不依赖 PNG 像素分析。
|
> PNG 文件用于画布的保存/加载和人工预览。AI 分析以 JSON 结构化数据为准,不依赖 PNG 像素分析。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 设计原则
|
## 设计原则
|
||||||
|
|
||||||
- **强制使用预定义画笔**:无自由拾色器,保证每个像素都映射到已知地形类型
|
- **预定义画笔**:无自由拾色器,保证每个像素都映射到已知地形类型
|
||||||
- **结构化数据优先**:高度和连通关系由用户直接输入,AI 无需从图像推断
|
- **结构化数据优先**:标高、高度和连通关系由用户直接输入,AI 无需从图像推断
|
||||||
- **层次化设计**:L1 定大局,L2 定细节,L3 预留
|
- **层次化设计**:L1 定大局,L2 定细节,L3 预留
|
||||||
- **人机共读**:PNG 供人视觉确认,JSON 供 AI 和程序化解析
|
- **人机共读**:PNG 供人视觉确认,JSON 供 AI 和程序化解析
|
||||||
- **与 Level Builder 一致**:画笔颜色直接映射 MI_Dev* 标识色材质
|
- **材质一致**:画笔颜色直接映射 UE5 的 MI_Dev* 标识色材质
|
||||||
|
- **离线优先**:单 HTML 文件即可使用,服务器仅为便利增强
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
"""
|
||||||
|
World Sketcher 本地保存服务器
|
||||||
|
用法: python server.py [port]
|
||||||
|
然后浏览器打开 http://localhost:<port>/sketch.html
|
||||||
|
"""
|
||||||
|
|
||||||
|
import http.server, json, os, sys, urllib.parse
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8765
|
||||||
|
SERVE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
OUTPUT_DIR = os.path.join(SERVE_DIR, "output")
|
||||||
|
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Handler(http.server.SimpleHTTPRequestHandler):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, directory=SERVE_DIR, **kwargs)
|
||||||
|
|
||||||
|
# ── POST /save ── 保存文件到 output/
|
||||||
|
def do_POST(self):
|
||||||
|
if self.path != "/save":
|
||||||
|
self.send_error(404); return
|
||||||
|
|
||||||
|
ctype = self.headers.get("Content-Type", "")
|
||||||
|
if ctype.startswith("multipart/form-data"):
|
||||||
|
boundary = ctype.split("boundary=")[-1].encode()
|
||||||
|
data = self.rfile.read(int(self.headers["Content-Length"]))
|
||||||
|
|
||||||
|
# 简易 multipart 解析
|
||||||
|
parts = data.split(b"--" + boundary)
|
||||||
|
saved = []
|
||||||
|
for part in parts:
|
||||||
|
if not part or part.strip() in (b"", b"--\r\n", b"--"):
|
||||||
|
continue
|
||||||
|
header_end = part.find(b"\r\n\r\n")
|
||||||
|
if header_end < 0:
|
||||||
|
continue
|
||||||
|
header = part[:header_end].decode("utf-8", errors="replace")
|
||||||
|
body = part[header_end + 4:]
|
||||||
|
if body.endswith(b"\r\n"):
|
||||||
|
body = body[:-2]
|
||||||
|
|
||||||
|
# 提取文件名
|
||||||
|
fname = None
|
||||||
|
for seg in header.split(";"):
|
||||||
|
seg = seg.strip()
|
||||||
|
if seg.startswith("filename="):
|
||||||
|
fname = seg.split("=", 1)[1].strip('"')
|
||||||
|
break
|
||||||
|
if not fname:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 安全:只取文件名,禁止路径遍历
|
||||||
|
fname = os.path.basename(fname)
|
||||||
|
fpath = os.path.join(OUTPUT_DIR, fname)
|
||||||
|
with open(fpath, "wb") as f:
|
||||||
|
f.write(body)
|
||||||
|
saved.append(fname)
|
||||||
|
|
||||||
|
resp = json.dumps({"ok": True, "saved": saved}, ensure_ascii=False)
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||||
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(resp.encode("utf-8"))
|
||||||
|
print(f" [{datetime.now():%H:%M:%S}] Saved: {', '.join(saved)}")
|
||||||
|
else:
|
||||||
|
self.send_error(400, "Expected multipart/form-data")
|
||||||
|
|
||||||
|
# ── GET 路由 ──
|
||||||
|
def do_GET(self):
|
||||||
|
# /list → 列出 output/ 下可用文件
|
||||||
|
if self.path == "/list":
|
||||||
|
files = sorted(
|
||||||
|
f for f in os.listdir(OUTPUT_DIR)
|
||||||
|
if f.endswith((".json", ".zip"))
|
||||||
|
)
|
||||||
|
resp = json.dumps({"files": files}, ensure_ascii=False)
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(resp.encode("utf-8"))
|
||||||
|
return
|
||||||
|
|
||||||
|
# /load/<file> → 从 output/ 读取文件
|
||||||
|
if self.path.startswith("/load/"):
|
||||||
|
fname = os.path.basename(urllib.parse.unquote(self.path[6:], encoding="utf-8"))
|
||||||
|
fpath = os.path.join(OUTPUT_DIR, fname)
|
||||||
|
if not os.path.isfile(fpath):
|
||||||
|
self.send_error(404); return
|
||||||
|
self.send_response(200)
|
||||||
|
if fname.endswith(".zip"):
|
||||||
|
self.send_header("Content-Type", "application/zip")
|
||||||
|
elif fname.endswith(".json"):
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.end_headers()
|
||||||
|
with open(fpath, "rb") as f:
|
||||||
|
self.wfile.write(f.read())
|
||||||
|
return
|
||||||
|
|
||||||
|
# 其他 → 静态文件(sketch.html 等)
|
||||||
|
super().do_GET()
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
# 只显示非静态资源的请求日志
|
||||||
|
if "/save" in str(args) or "/list" in str(args) or "/load/" in str(args):
|
||||||
|
super().log_message(format, *args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
with http.server.HTTPServer(("", PORT), Handler) as httpd:
|
||||||
|
url = f"http://localhost:{PORT}/sketch.html"
|
||||||
|
print(f"World Sketcher server running")
|
||||||
|
print(f" URL: {url}")
|
||||||
|
print(f" Output: {OUTPUT_DIR}")
|
||||||
|
print(f" Press Ctrl+C to stop.\n")
|
||||||
|
try:
|
||||||
|
httpd.serve_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nServer stopped.")
|
||||||
@@ -147,7 +147,7 @@ html, body { height: 100%; overflow: hidden; font-family: "Microsoft YaHei", "Se
|
|||||||
<button class="tbtn" id="btn-clear">清空</button>
|
<button class="tbtn" id="btn-clear">清空</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="tool-group">
|
<div class="tool-group">
|
||||||
<button class="tbtn" id="btn-save-all">保存 ZIP</button>
|
<button class="tbtn" id="btn-save-all">保存</button>
|
||||||
<button class="tbtn" id="btn-load">加载</button>
|
<button class="tbtn" id="btn-load">加载</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -174,8 +174,8 @@ html, body { height: 100%; overflow: hidden; font-family: "Microsoft YaHei", "Se
|
|||||||
<input id="name-input" type="text" placeholder="输入名称(如"家"、"小卖部")">
|
<input id="name-input" type="text" placeholder="输入名称(如"家"、"小卖部")">
|
||||||
<label><input type="checkbox" id="name-enterable"> 可进入(有下级详图)</label>
|
<label><input type="checkbox" id="name-enterable"> 可进入(有下级详图)</label>
|
||||||
<div id="name-height-row" style="display:none;">
|
<div id="name-height-row" style="display:none;">
|
||||||
<label style="color:var(--text-dim);">高度</label>
|
<label id="name-height-label" style="color:var(--text-dim);">高度</label>
|
||||||
<input id="name-height" type="number" min="0" step="0.5" value="3">
|
<input id="name-height" type="number" step="0.5" value="3">
|
||||||
<span style="color:var(--text-dim);">m</span>
|
<span style="color:var(--text-dim);">m</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="name-connections-row" style="display:none;">
|
<div id="name-connections-row" style="display:none;">
|
||||||
@@ -191,6 +191,17 @@ html, body { height: 100%; overflow: hidden; font-family: "Microsoft YaHei", "Se
|
|||||||
|
|
||||||
<input type="file" id="file-input" accept=".json,.zip" style="display:none">
|
<input type="file" id="file-input" accept=".json,.zip" style="display:none">
|
||||||
|
|
||||||
|
<!-- 从 output/ 加载的文件选择弹窗 -->
|
||||||
|
<div id="output-modal" style="display:none;position:fixed;z-index:100;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);">
|
||||||
|
<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:#2d2d2d;border:1px solid #666;border-radius:8px;padding:16px;min-width:320px;max-height:60vh;display:flex;flex-direction:column;">
|
||||||
|
<h4 style="margin-bottom:10px;font-size:13px;color:#ddd;">从 output/ 加载</h4>
|
||||||
|
<div id="output-file-list" style="flex:1;overflow-y:auto;max-height:300px;"></div>
|
||||||
|
<div style="display:flex;justify-content:flex-end;margin-top:10px;">
|
||||||
|
<button class="tbtn" id="output-modal-close">关闭</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
@@ -256,6 +267,8 @@ const App = {
|
|||||||
this.initKeyboard();
|
this.initKeyboard();
|
||||||
this.initLevel("L1", 500, 400, this.defaultL1.canvasW, this.defaultL1.canvasH);
|
this.initLevel("L1", 500, 400, this.defaultL1.canvasW, this.defaultL1.canvasH);
|
||||||
this.switchToLevel("L1");
|
this.switchToLevel("L1");
|
||||||
|
// 缓存服务器可用性(首次加载时检测)
|
||||||
|
this._serverOk = this.serverAvailable();
|
||||||
},
|
},
|
||||||
|
|
||||||
initLevel(id, wW, wH, cW, cH) {
|
initLevel(id, wW, wH, cW, cH) {
|
||||||
@@ -303,6 +316,7 @@ const App = {
|
|||||||
this.renderBrushPalette();
|
this.renderBrushPalette();
|
||||||
this.updateBreadcrumb();
|
this.updateBreadcrumb();
|
||||||
this.updateElementsList();
|
this.updateElementsList();
|
||||||
|
this.drawElementLabels();
|
||||||
this.updateScaleDisplay();
|
this.updateScaleDisplay();
|
||||||
this.updateStatus();
|
this.updateStatus();
|
||||||
},
|
},
|
||||||
@@ -356,7 +370,10 @@ const App = {
|
|||||||
document.getElementById("btn-redo").addEventListener("click", () => this.redo());
|
document.getElementById("btn-redo").addEventListener("click", () => this.redo());
|
||||||
document.getElementById("btn-clear").addEventListener("click", () => this.clearCanvas());
|
document.getElementById("btn-clear").addEventListener("click", () => this.clearCanvas());
|
||||||
document.getElementById("btn-save-all").addEventListener("click", () => this.saveAll());
|
document.getElementById("btn-save-all").addEventListener("click", () => this.saveAll());
|
||||||
document.getElementById("btn-load").addEventListener("click", () => document.getElementById("file-input").click());
|
document.getElementById("btn-load").addEventListener("click", () => this.smartLoad());
|
||||||
|
document.getElementById("output-modal-close").addEventListener("click", () => {
|
||||||
|
document.getElementById("output-modal").style.display = "none";
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/* ---- Canvas 事件 ---- */
|
/* ---- Canvas 事件 ---- */
|
||||||
@@ -619,7 +636,7 @@ const App = {
|
|||||||
|
|
||||||
this.undoStack = []; this.redoStack = []; this.updateUndoButtons();
|
this.undoStack = []; this.redoStack = []; this.updateUndoButtons();
|
||||||
this.wW.value = l2d.worldW; this.wH.value = l2d.worldH;
|
this.wW.value = l2d.worldW; this.wH.value = l2d.worldH;
|
||||||
this.drawGrid(); this.renderBrushPalette(); this.updateBreadcrumb(); this.updateElementsList(); this.updateScaleDisplay(); this.updateStatus();
|
this.drawGrid(); this.renderBrushPalette(); this.updateBreadcrumb(); this.updateElementsList(); this.drawElementLabels(); this.updateScaleDisplay(); this.updateStatus();
|
||||||
},
|
},
|
||||||
|
|
||||||
compositeL2BackToL1() {
|
compositeL2BackToL1() {
|
||||||
@@ -685,9 +702,19 @@ const App = {
|
|||||||
|
|
||||||
// 高度输入:building / blocked / zone 类型显示
|
// 高度输入:building / blocked / zone 类型显示
|
||||||
const showHeight = ["building", "blocked", "zone"].includes(brushType.id);
|
const showHeight = ["building", "blocked", "zone"].includes(brushType.id);
|
||||||
|
const heightLabel = document.getElementById("name-height-label");
|
||||||
heightRow.style.display = showHeight ? "flex" : "none";
|
heightRow.style.display = showHeight ? "flex" : "none";
|
||||||
if (showHeight) {
|
if (showHeight) {
|
||||||
heightInp.value = existing && existing.height_m != null ? existing.height_m : this.defaultHeightForType(brushType.id);
|
// L1 zone = 地形标高(允许负值);L2 building = 建筑高度
|
||||||
|
if (this.currentLevel === "L1" && brushType.id === "zone") {
|
||||||
|
heightLabel.textContent = "地形标高";
|
||||||
|
heightInp.min = "-10";
|
||||||
|
heightInp.value = existing && existing.height_m != null ? existing.height_m : 0;
|
||||||
|
} else {
|
||||||
|
heightLabel.textContent = "建筑高度";
|
||||||
|
heightInp.min = "0";
|
||||||
|
heightInp.value = existing && existing.height_m != null ? existing.height_m : this.defaultHeightForType(brushType.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 连通多选:列出同层级其他已命名元素
|
// 连通多选:列出同层级其他已命名元素
|
||||||
@@ -791,14 +818,26 @@ const App = {
|
|||||||
drawElementLabels() {
|
drawElementLabels() {
|
||||||
const ctx = this.uc.getContext("2d"), d = this.levelData[this.currentLevel];
|
const ctx = this.uc.getContext("2d"), d = this.levelData[this.currentLevel];
|
||||||
ctx.clearRect(0, 0, d.canvasW, d.canvasH);
|
ctx.clearRect(0, 0, d.canvasW, d.canvasH);
|
||||||
|
const isL1 = this.currentLevel === "L1";
|
||||||
d.elements.forEach(el => {
|
d.elements.forEach(el => {
|
||||||
|
// L1 只显示区域(zone)名,L2 只显示建筑(building)名
|
||||||
|
if (isL1 && el.type !== "zone") return;
|
||||||
|
if (!isL1 && el.type !== "building") return;
|
||||||
|
|
||||||
ctx.font = "bold 12px sans-serif";
|
ctx.font = "bold 12px sans-serif";
|
||||||
const tw = ctx.measureText(el.name).width + 8, th = 18;
|
// L1: height_m = 地形标高;L2: height_m = 建筑高度
|
||||||
|
let label = el.name;
|
||||||
|
if (el.height_m != null && el.height_m !== 0) {
|
||||||
|
label = isL1
|
||||||
|
? `${el.name} (标高${el.height_m}m)`
|
||||||
|
: `${el.name} (${el.height_m}m)`;
|
||||||
|
}
|
||||||
|
const tw = ctx.measureText(label).width + 8, th = 18;
|
||||||
const tx = el.px - tw/2, ty = el.py - 28;
|
const tx = el.px - tw/2, ty = el.py - 28;
|
||||||
ctx.fillStyle = "rgba(0,0,0,0.7)";
|
ctx.fillStyle = "rgba(0,0,0,0.7)";
|
||||||
ctx.fillRect(tx, ty, tw, th);
|
ctx.fillRect(tx, ty, tw, th);
|
||||||
ctx.fillStyle = "#fff"; ctx.textAlign = "center"; ctx.textBaseline = "middle";
|
ctx.fillStyle = "#fff"; ctx.textAlign = "center"; ctx.textBaseline = "middle";
|
||||||
ctx.fillText(el.name, el.px, ty + th/2);
|
ctx.fillText(label, el.px, ty + th/2);
|
||||||
if (el.hasDetail) {
|
if (el.hasDetail) {
|
||||||
ctx.fillStyle = "rgba(74,158,255,0.8)";
|
ctx.fillStyle = "rgba(74,158,255,0.8)";
|
||||||
ctx.beginPath(); ctx.arc(el.px + tw/2 - 2, ty + 2, 5, 0, Math.PI*2); ctx.fill();
|
ctx.beginPath(); ctx.arc(el.px + tw/2 - 2, ty + 2, 5, 0, Math.PI*2); ctx.fill();
|
||||||
@@ -929,8 +968,31 @@ const App = {
|
|||||||
zip.file(`${name}-scene.json`, JSON.stringify(sceneData, null, 2));
|
zip.file(`${name}-scene.json`, JSON.stringify(sceneData, null, 2));
|
||||||
|
|
||||||
const blob = await zip.generateAsync({ type: "blob" });
|
const blob = await zip.generateAsync({ type: "blob" });
|
||||||
this.downloadBlob(blob, `${name}-scene.zip`);
|
|
||||||
this.showSaveFeedback("ZIP 已保存 ✓");
|
// 优先 POST 到本地服务器,失败则回退浏览器下载
|
||||||
|
const savedToServer = await this.saveToServer(`${name}-scene.zip`, blob);
|
||||||
|
if (!savedToServer) {
|
||||||
|
this.downloadBlob(blob, `${name}-scene.zip`);
|
||||||
|
}
|
||||||
|
this.showSaveFeedback(savedToServer ? `已保存到 output/ ✓` : "ZIP 已下载 ✓");
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 尝试通过本地服务器保存到 output/ 目录 */
|
||||||
|
async saveToServer(filename, blob) {
|
||||||
|
try {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append("file", blob, filename);
|
||||||
|
const resp = await fetch("/save", { method: "POST", body: fd });
|
||||||
|
return resp.ok;
|
||||||
|
} catch { return false; }
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 服务器是否可用 */
|
||||||
|
async serverAvailable() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch("/list", { method: "GET" });
|
||||||
|
return resp.ok;
|
||||||
|
} catch { return false; }
|
||||||
},
|
},
|
||||||
|
|
||||||
showSaveFeedback(msg) {
|
showSaveFeedback(msg) {
|
||||||
@@ -961,6 +1023,69 @@ const App = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** 弹出 output/ 文件选择列表 */
|
||||||
|
async showOutputModal() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch("/list");
|
||||||
|
const { files } = await resp.json();
|
||||||
|
const modal = document.getElementById("output-modal");
|
||||||
|
const list = document.getElementById("output-file-list");
|
||||||
|
list.innerHTML = "";
|
||||||
|
if (files.length === 0) {
|
||||||
|
list.innerHTML = '<div style="color:#999;font-size:12px;padding:8px;">output/ 目录为空</div>';
|
||||||
|
} else {
|
||||||
|
files.forEach(f => {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.style.cssText = "padding:6px 8px;cursor:pointer;border-radius:3px;font-size:12px;display:flex;align-items:center;gap:8px;";
|
||||||
|
row.addEventListener("mouseenter", () => row.style.background = "#3a3a3a");
|
||||||
|
row.addEventListener("mouseleave", () => row.style.background = "");
|
||||||
|
const icon = f.endsWith(".zip") ? "📦" : "📄";
|
||||||
|
row.innerHTML = `<span>${icon}</span><span style="flex:1;color:#ddd;">${f}</span>`;
|
||||||
|
row.addEventListener("click", async () => {
|
||||||
|
modal.style.display = "none";
|
||||||
|
await this.loadFromServer(f);
|
||||||
|
});
|
||||||
|
list.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
modal.style.display = "block";
|
||||||
|
} catch {
|
||||||
|
alert("无法连接本地服务器,请确认 server.py 正在运行");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 从服务器 output/ 加载文件 */
|
||||||
|
async loadFromServer(filename) {
|
||||||
|
try {
|
||||||
|
const resp = await fetch("/load/" + encodeURIComponent(filename));
|
||||||
|
if (!resp.ok) throw new Error("文件不存在");
|
||||||
|
if (filename.endsWith(".zip")) {
|
||||||
|
const ab = await resp.arrayBuffer();
|
||||||
|
const zip = await JSZip.loadAsync(ab);
|
||||||
|
const jsonFile = Object.keys(zip.files).find(n => n.endsWith("-scene.json"));
|
||||||
|
if (!jsonFile) { alert("ZIP 中未找到 scene.json"); return; }
|
||||||
|
const txt = await zip.file(jsonFile).async("string");
|
||||||
|
await this.restoreScene(JSON.parse(txt), zip);
|
||||||
|
} else {
|
||||||
|
const txt = await resp.text();
|
||||||
|
await this.restoreScene(JSON.parse(txt), null);
|
||||||
|
}
|
||||||
|
this.showSaveFeedback(`已加载 ${filename} ✓`);
|
||||||
|
} catch (err) {
|
||||||
|
alert("加载失败:" + err.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 智能加载:服务器在则从 output/ 读,否则回退浏览器文件选择 */
|
||||||
|
async smartLoad() {
|
||||||
|
const ok = await this._serverOk;
|
||||||
|
if (ok) {
|
||||||
|
this.showOutputModal();
|
||||||
|
} else {
|
||||||
|
document.getElementById("file-input").click();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/** 从 PNG Blob 恢复 ImageData */
|
/** 从 PNG Blob 恢复 ImageData */
|
||||||
blobToImageData(blob) {
|
blobToImageData(blob) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
@echo off
|
||||||
|
title World Sketcher
|
||||||
|
|
||||||
|
where python >nul 2>&1
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo [ERROR] Python not found. Install Python 3 first.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
set PORT=8765
|
||||||
|
if not "%~1"=="" set PORT=%~1
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo World Sketcher
|
||||||
|
echo Port: %PORT%
|
||||||
|
echo Save dir: output\
|
||||||
|
echo Close this window to stop.
|
||||||
|
echo.
|
||||||
|
|
||||||
|
start "" cmd /c "timeout /t 2 /nobreak >nul & start http://localhost:%PORT%/sketch.html"
|
||||||
|
|
||||||
|
python "%~dp0server.py" %PORT%
|
||||||
Reference in New Issue
Block a user