Compare commits
19 Commits
main
...
7b473f72dd
| Author | SHA1 | Date | |
|---|---|---|---|
| 7b473f72dd | |||
| be4e9a15c9 | |||
| 97427743be | |||
| 4ec51b13f8 | |||
| 870976fa7b | |||
| 4fc4a39aa6 | |||
| 2f0a2133a1 | |||
| 2c10eedad3 | |||
| 893ef1eac2 | |||
| 46e9b33629 | |||
| cb6364fefb | |||
| 906e2aa7a2 | |||
| 0c57d69ff7 | |||
| 237885e3b8 | |||
| c14c63f30d | |||
| 4bf678b937 | |||
| 690cfeb29d | |||
| 8a75e682c5 | |||
| df0fa82ea9 |
@@ -1,5 +0,0 @@
|
||||
# 策划 Agent Memory Index
|
||||
|
||||
- [千禧年怀旧文化灵感](nostalgia-culture-inspirations.md) — 电视剧/广播体操/广场舞/电话热线/零食/小孩游戏/乡下活动灵感汇总
|
||||
- [建筑与区域规划](building-area-plan.md) — 城市主场景完整建筑清单、可进入室内、新增设施共识
|
||||
- [v0.1执行案状态](v01-execution-status.md) — 三份执行案已通过二次质量门审查,流水线可推进至④程序设计
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
name: building-area-plan
|
||||
description: 城市主场景建筑与区域规划共识,包含完整建筑清单、可进入室内、新增设施
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
|
||||
## 城市主场景建筑清单(2026-06-10 确认)
|
||||
|
||||
来源:头脑风暴-2026-06-10-建筑与区域规划.md
|
||||
|
||||
### 关键变更(vs 原场景设计)
|
||||
|
||||
- 小伙伴家:原"全部不可进入" → 4家可进入(简化室内)
|
||||
- 包机房:合并进街机店后屋
|
||||
- 废品回收:固定摊位 + 流动收废品车
|
||||
- 秘密基地:多处散布(工地围挡后、平房区死角等)
|
||||
- 新增店铺:粮油店、服装店/裁缝铺、鞋店(主街东侧)、储蓄所(西侧)
|
||||
- 新增设施:棋牌室(纯氛围)、公交站牌(纯装饰)、公共厕所、自行车棚、信报箱
|
||||
|
||||
### 主街布局(东西排列)
|
||||
|
||||
东→:学校操场 | 小卖部 | 粮油店 | 书店 | 服装店 | 鞋店 · 公交站 · 棋牌室 | 储蓄所 | 录像出租 | 网吧 | 街机店 ←西
|
||||
|
||||
### 可进入室内(~12个)
|
||||
|
||||
完整:主角家、合并小卖部、书店、街机店、网吧、录像出租点
|
||||
简化:死党A家(小区内)、死党B家(平房区)、老板儿子家(小卖部后屋)、女班长家(主角家同楼)
|
||||
|
||||
### 不做的元素
|
||||
|
||||
菜地(留乡下)、装修、游泳(河边不下水)
|
||||
|
||||
### How to apply:
|
||||
- 场景与世界观.md 已更新为最新清单
|
||||
- 新增店铺和设施在后续版本逐步添加(见版本映射)
|
||||
- 概念草案不变(冻结),变更记录在系统设计案中
|
||||
@@ -1,87 +0,0 @@
|
||||
---
|
||||
name: nostalgia-culture-inspirations
|
||||
description: 千禧年怀旧文化元素灵感 + 迈皋桥地区历史地理考据,含共识和待深入项
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
|
||||
## 头脑风暴记录
|
||||
|
||||
来源:brainstorm-2026-06-05-nostalgia-culture.md
|
||||
日期:2026-06-05/06
|
||||
状态:方向确认完毕,待后续决定具体用法
|
||||
|
||||
### ⭐ 电话热线玩电视游戏 — 最值得深入
|
||||
|
||||
- 2000年前后电视台互动节目:打电话用按键音控制屏幕上的射击游戏
|
||||
- 作为游戏内小游戏的潜力最大:小游戏+叙事+时代感三合一
|
||||
- 体验流程:走到电话旁 → 拨号 → 进入简化版射击游戏 → 打完显示话费 → 「完了,肯定要被我妈骂」
|
||||
- starmen1265 明确标记为「值得深入」
|
||||
|
||||
### 电视剧/动画文化
|
||||
|
||||
- 千禧年小孩记忆锚点:还珠格格、西游记、灌篮高手、龙珠、迪迦奥特曼、数码宝贝
|
||||
- Cartoon Network 通过有线电视收看,是「有有线才能看的频道」
|
||||
- 引进外国剧:排球女将、成长的烦恼等
|
||||
- 城郊特有:CRT电视、天线收台、拍电视机、有线电视刚铺开
|
||||
|
||||
### 广播体操
|
||||
|
||||
- 第八套广播体操(1997)或「雏鹰起飞」(少儿版)
|
||||
- 潜力方向:氛围事件、节奏小游戏、叙事道具
|
||||
- 城郊特色:泥土操场、大喇叭杂音、冬天缩手做
|
||||
|
||||
### 广场舞/社区活动
|
||||
|
||||
- 2000年前后是前身形态(十几到几十人、收录机放磁带)
|
||||
- 城郊特色:扭秧歌、露天电影、露天卡拉OK
|
||||
|
||||
### 迈皋桥地区历史地理考据(关键发现)
|
||||
|
||||
**2000年的迈皋桥处于"正在城市化"的过渡状态**:
|
||||
- 迈皋桥广场不存在(约2005年随地铁建成,原址是石牌坊+屠宰场)
|
||||
- 红山路已存在(90年代建成)
|
||||
- 老街1998年启动拆迁,新旧交替
|
||||
- 734厂已拆,741厂(华电)还在转型
|
||||
- 大片菜地+农村平房正在被小区取代
|
||||
- 南北十里长沟是臭水沟("龙须沟"),2013年才整治
|
||||
- 两座7000+职工的国营大厂曾主导整个社区
|
||||
- 小卖部、大市场、高桥市场等商业氛围浓厚
|
||||
|
||||
### How to apply:
|
||||
- 电话热线游戏适合进入小游戏系统设计,需确定版本归属
|
||||
- 电视剧/动画元素适合做世界氛围素材(电视内容、贴纸、NPC对话)
|
||||
- 广播体操和广场舞适合作为场景氛围事件
|
||||
- 迈皋桥考据直接用于场景设计的"半城半乡"氛围塑造
|
||||
- 华电子弟学校、工厂宿舍区、菜地、老街都是高还原度的场景元素
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-10 灵感补充
|
||||
|
||||
来源:头脑风暴-2026-06-10-建筑与区域规划.md
|
||||
|
||||
### 重点新发现(⭐⭐⭐)
|
||||
|
||||
1. **钻狗洞的秘密基地** — 多处散布(工地围挡后、平房区死角),高探索叙事价值
|
||||
2. **叠纸飞机** — 纸张收集+折叠选择+飞行结果三层玩法
|
||||
3. **不知道名字的小哥哥** — 一次性邂逅NPC,极高情感潜力微型叙事线
|
||||
4. **体力系统+家庭动态事件** — 吃饭补充体力→不吃炒饭→换速冻水饺→给饭钱
|
||||
5. **爷爷奶奶家亲戚小孩** — 乡下NPC角色体系(之前乡下NPC完全空白)
|
||||
|
||||
### 新零食/小吃(具体品牌品种)
|
||||
|
||||
大大泡泡糖、秀逗(超酸糖)、无花果丝、老鼠屎(盐津葡萄)、跳跳糖、烤鱿鱼、烤里脊
|
||||
|
||||
### 新小孩游戏
|
||||
|
||||
双人拍手/互推游戏、猜在哪个手里、跑步比赛、叠纸飞机
|
||||
|
||||
### 新人物/社交
|
||||
|
||||
- 北方来南方的同学(与"转学生"角色互补)
|
||||
- 等着去别人家(串门文化)
|
||||
|
||||
### 新乡下活动
|
||||
|
||||
钓鱼(鱼竿)、游泳、挤牛奶、喂鸡掏鸡蛋、菜地浇水、打水漂
|
||||
@@ -1,26 +0,0 @@
|
||||
---
|
||||
name: v01-execution-status
|
||||
description: v0.1三份执行案的质量门审查状态——二次审查已通过,但Gitea Issues标签尚未同步
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
|
||||
## v0.1 执行案审查状态(截至2026-06-05)
|
||||
|
||||
三份执行案在二次审查(P-002-review-r2.md)中全部 **✅ 通过**:
|
||||
- `interaction-system-execution.md` (v0.1.2) — 优秀
|
||||
- `collection-system-execution.md` (v0.1.1) — 优秀
|
||||
- `player-controller-execution.md` (v0.1.1) — 良好
|
||||
|
||||
### Why:
|
||||
首次审查有5条阻塞问题(缺失独立文件、技术越界、内容匮乏、边界缺失),经过3轮迭代全部修正。
|
||||
|
||||
### 待同步事项:
|
||||
- Gitea Issues #1/#2/#3 仍标记为 `gate:execution-fail`、`status:blocked`,需 PM 更新标签
|
||||
- Pipeline Tracker (#8) 仍显示③为「质量门未通过」,需 PM 更新为通过
|
||||
- `v01-scope.md` 中 "WASD 步行移动" 需更新为 "WASD 跑步移动"(策划待同步项)
|
||||
|
||||
### How to apply:
|
||||
- 策划侧当前无阻塞任务,流水线可推进至④程序设计
|
||||
- 非阻塞建议(数据类型描述改行为语言、拾取物外观改纯视觉语言)留待量产阶段
|
||||
- 物品系统无独立执行案,④程序设计阶段由程序统一处理
|
||||
@@ -1,5 +0,0 @@
|
||||
- [Level Builder Tool](level-builder-tool.md) — 白模关卡自动生成 Python 脚本(tools/level-builder/,265→238 Actor 修复后验证通过)
|
||||
- [UE5.7 Python API](ue57-python-api.md) — 实测 API 差异(set_actor_rotation、new_level、CWD、import 路径、Rotator 关键字参数)
|
||||
- [V01 Bugfix](level-builder-bugfix-v01.md) — 8个白模关卡问题诊断修复(搭建指南6+解析器双重解析+构建器静默fallback)
|
||||
- [BSP Pivot Types](bsp-pivot-types.md) — 38种 BSP 资产实测锚点+自动补偿机制(角落/底边中心/底面中心)
|
||||
- [交互系统架构](interaction-effect-architecture.md) — v1.0 Effect/Action 数据驱动架构(Manager在GI、AInfo Tick、门派生、Prompt独立)
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
name: bsp-pivot-types
|
||||
description: BlockingStarterPack 全部 38 种 BSP 资产的实测锚点位置和补偿方案
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
|
||||
# BSP 资产锚点实测数据(2026-06-08)
|
||||
|
||||
## 锚点模式分类
|
||||
|
||||
| 模式 | 资产 | 特征 | 补偿 |
|
||||
|------|------|------|------|
|
||||
| **角落** | Floor_*, Cube, Fence_Small_400 | min≈(0,0,0) | XY 各偏半尺寸 |
|
||||
| **底边中心** | Wall_*, BarrierWall_* | min≈(0, ±half_thickness, 0) | X 偏半宽度,Y 已居中 |
|
||||
| **底面中心** | Cylinder, CirclePlatform, 家具, 道具 | min≈(-half, -half, 0) | 无需补偿 |
|
||||
|
||||
## 关键实测尺寸(非 default_size)
|
||||
|
||||
- **BarrierWall_600**: 实际 600×**60**×**210**cm(非 mapping 中的 20×100)
|
||||
- **BarrierWall_200**: 实际 200×**60**×**210**cm
|
||||
- **Floor_1000x1000**: Z 范围 -20 到 2(厚度 22cm,表面在 Z=2)
|
||||
- **Fence_Small_400**: 实际 407×6×112cm
|
||||
- **Wall_400x300**: 所有墙体 Y 方向已居中(±10cm)
|
||||
|
||||
## 补偿实现
|
||||
|
||||
`asset_mapping.py` 中 `pivot_offset_xy` 字段 + `get_pivot_compensation()` 函数:
|
||||
- 输入: asset_key, scale, rotation
|
||||
- 输出: (dx, dy) 偏移量
|
||||
- builder 中: spawn_pos = json_pos - compensation
|
||||
|
||||
## Why: 搭建指南统一用视觉中心坐标,但 BSP 资产锚点各异,不补偿会差半模型
|
||||
## How to apply: 新增 BSP 资产时先跑 check_pivots.py 实测锚点,再填 pivot_offset_xy。新增搭建指南时位置直接写视觉中心即可。
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
name: interaction-effect-architecture
|
||||
description: 交互系统 v1.0 Effect/Action 数据驱动架构决策记录
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
|
||||
## 交互系统架构决策(v1.0 重构,2026-06-11)
|
||||
|
||||
### 核心架构:Effect/Action 数据驱动
|
||||
|
||||
交互物不关心"交互后发生什么",只配置 Effect(由 Action 组成)交给 EffectManager 执行。
|
||||
|
||||
**Why:** v0.1 用 switch(Type) 硬编码,新增交互类型需改代码。v1.0 配数据即可。
|
||||
|
||||
### Manager 归属
|
||||
- FInteractionManager / FEffectManager 是普通 C++ 类,不是 UObject
|
||||
- 由 ASystemDriver (AInfo) 持有,随 World 生灭
|
||||
- 不依赖 UE Subsystem 生命周期,显式控制创建顺序和 Tick 顺序
|
||||
- 所有 UObject 引用用 TWeakObjectPtr,回调用 TFunction
|
||||
- GameInstance 不持有任何交互系统对象,只有 UPersistenceSubsystem 跨 World
|
||||
|
||||
### 分层规则
|
||||
- 表现层只读 Manager 缓存(getter),不碰 Component
|
||||
- Manager 在选择变化时从 Component 读一次并缓存
|
||||
- 逻辑事件仅用于 "A 影响 B"(如 Consume → Persistence)
|
||||
|
||||
### 协调方式
|
||||
- Character 输入回调通过 ASystemDriver::Get(World) 访问两个 Manager
|
||||
- Driver::BeginPlay 中 TFunction 连线 EffectManager.OnEffectComplete → Manager.OnInteractionFinished
|
||||
- PromptComponent 通过 Driver 获取 Manager,只读缓存
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
name: level-builder-bugfix-v01
|
||||
description: v01白模关卡8个问题的诊断和修复记录,包括搭建指南/解析器/构建器的修改
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
|
||||
# v01 白模关卡问题修复记录(2026-06-08)
|
||||
|
||||
## 修复的文件
|
||||
|
||||
| 文件 | 修改内容 |
|
||||
|------|---------|
|
||||
| `docs/technical/关卡-v01-搭建指南.md` | 修复6个问题:全局地面、Z1双Floor、西边界旋转统一、东边界改Wall_400x300、Z1房屋墙旋转、院门改WallDoorDouble、南边界Z7去重 |
|
||||
| `tools/level-builder/parsers/build_guide_parser.py` | 新增 `_deduplicate_actors()` 去重;`_parse_phase_ground` 缩小范围到B1段;支持 `旋转 (P,Y,R)` 列名;支持全局地面标题 |
|
||||
| `tools/level-builder/builder/scene_builder.py` | `load_asset` 失败时用 Cube 占位 + `[MISSING]` 前缀 + MI_DevPink 标记 |
|
||||
| `tools/level-builder/config/asset_mapping.py` | 新增 `WallDoorDouble_400x300` 资产映射 |
|
||||
| `tools/level-builder/parsers/markdown_table.py` | `parse_rotation` 增加箭头注释处理说明 |
|
||||
|
||||
## 诊断结论
|
||||
|
||||
- 8个问题中6个源头在**搭建指南**,2个涉及执行案
|
||||
- 解析器有双重解析bug(地面段解析了边界表格)已修复
|
||||
- 构建器有静默fallback已改为醒目标记
|
||||
|
||||
## Why: 用户检查生成的白模关卡发现多处明显错误,需要追溯源头并修复
|
||||
## How to apply: 重新生成关卡时使用修复后的 JSON(238 Actor)。如再出问题,先查搭建指南再查解析器。
|
||||
@@ -1,44 +0,0 @@
|
||||
---
|
||||
name: level-builder-tool
|
||||
description: 白模关卡自动生成 Python 脚本工具,位于 tools/level-builder/
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
|
||||
# Level Builder 工具
|
||||
|
||||
## 位置
|
||||
`tools/level-builder/`
|
||||
|
||||
## 功能
|
||||
两阶段流水线:解析搭建指南 Markdown → JSON → UE5 Actor
|
||||
|
||||
## 使用方式
|
||||
```bash
|
||||
# 解析
|
||||
python tools/level-builder/main.py parse -i docs/technical/关卡-v01-搭建指南.md -o output/level_v01.json
|
||||
|
||||
# 验证
|
||||
python tools/level-builder/main.py validate -i output/level_v01.json
|
||||
|
||||
# 报告
|
||||
python tools/level-builder/main.py report -i output/level_v01.json -o output/搭建报告.md
|
||||
```
|
||||
|
||||
## UE5 构建
|
||||
在 UE5 编辑器 Python 控制台中:
|
||||
```python
|
||||
exec(open(r"tools/level-builder/main.py").read())
|
||||
# 然后 main(["build", "-i", "output/level_v01.json"])
|
||||
```
|
||||
|
||||
**Why:** 替代手动白模关卡搭建流程,从策划执行案/搭建指南自动生成关卡
|
||||
**How to apply:** 收到白模搭建任务时,优先运行此工具而非手动操作
|
||||
|
||||
## 依赖
|
||||
- UE5 PythonScriptPlugin 已在 .uproject 中启用
|
||||
- 37 种 BSP 资产映射(config/asset_mapping.py)
|
||||
- 当前解析搭建指南(parsers/build_guide_parser.py),未来扩展执行案解析器
|
||||
|
||||
## 数据
|
||||
- v0.1 解析结果:265 个 Actor(STR:123, PROP:53, BND:61, INTR:10, COLL:9, LGT:8, PLAY:1)
|
||||
@@ -1,69 +0,0 @@
|
||||
---
|
||||
name: ue57-python-api
|
||||
description: UE5.7 Python API 实测差异记录——set_actor_rotation、new_level、CWD 问题
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
|
||||
# UE5.7 Python API 实测差异
|
||||
|
||||
## 2026-06-07 实测(UE 5.7, PythonScriptPlugin)
|
||||
|
||||
### `set_actor_rotation()` 需要 `teleport_physics` 参数
|
||||
```python
|
||||
# UE5.6 文档写法(会报错):
|
||||
actor.set_actor_rotation(unreal.Rotator(pitch, yaw, roll))
|
||||
|
||||
# UE5.7 正确写法:
|
||||
actor.set_actor_rotation(unreal.Rotator(pitch, yaw, roll), False)
|
||||
```
|
||||
**Why:** UE5.7 改了 API 签名,positional argument `teleport_physics` 变为必需
|
||||
**How to apply:** 所有 `set_actor_rotation()` 调用都加第二个参数 `False`
|
||||
|
||||
### `unreal.Rotator` 位置参数顺序不是 (Pitch, Yaw, Roll)
|
||||
```python
|
||||
# ❌ 错误:位置参数顺序非 P,Y,R,Yaw=90 会落到 Pitch(Y显示)
|
||||
actor.set_actor_rotation(unreal.Rotator(0, 90, 0), False)
|
||||
|
||||
# ✅ 正确:用关键字参数避免歧义
|
||||
actor.set_actor_rotation(unreal.Rotator(pitch=0, yaw=90, roll=0), False)
|
||||
```
|
||||
**Why:** UE5.7 Python `unreal.Rotator` 的位置参数顺序与关键字参数名不一致,第二个位置参数映射到 Pitch 而非 Yaw
|
||||
**How to apply:** 所有 `unreal.Rotator()` 调用必须使用关键字参数 `pitch=, yaw=, roll=`
|
||||
|
||||
### `-ExecutePythonScript` 模式下 CWD 不是项目目录
|
||||
```python
|
||||
# ❌ 错误:CWD 是引擎 Binaries 目录
|
||||
PROJECT_ROOT = Path(os.getcwd())
|
||||
# → E:\...\UE_5.7\Engine\Binaries\Win64
|
||||
|
||||
# ✅ 正确:通过环境变量传入
|
||||
PROJECT_ROOT = Path(os.environ.get("LB_PROJECT_ROOT", ""))
|
||||
```
|
||||
**Why:** UE5 引擎启动时将 CWD 设为引擎二进制目录
|
||||
**How to apply:** 由 bat 脚本设置 `LB_PROJECT_ROOT` 环境变量,Python 脚本从中读取
|
||||
|
||||
### `new_level()` 需要 `asset_path` 参数
|
||||
```python
|
||||
# ❌ 错误:
|
||||
unreal.EditorLevelLibrary.new_level()
|
||||
|
||||
# ✅ 正确:
|
||||
unreal.EditorLevelLibrary.new_level("/Game/Maps/LVL_SuburbanV01")
|
||||
```
|
||||
|
||||
### `AssetToolsSubsystem` 在 UE5.7 Python 中不可用
|
||||
`unreal.get_editor_subsystem(unreal.AssetToolsSubsystem)` 报 `no attribute 'AssetToolsSubsystem'`
|
||||
改用 `unreal.EditorLevelLibrary.new_level(asset_path)` 创建关卡
|
||||
|
||||
### 目录名含连字符导致 Python import 失败
|
||||
```
|
||||
tools/level-builder/ ← 连字符
|
||||
from tools.level_builder.config... ← 下划线,Python import 不支持连字符
|
||||
```
|
||||
修复:将 level-builder 根目录加入 sys.path,使用 `from config.asset_mapping import ...` 代替完整路径
|
||||
|
||||
## 相关文件
|
||||
- `tools/level-builder/builder/scene_builder.py`
|
||||
- `tools/level-builder/build_cmdline.py`
|
||||
- `tools/level-builder/build_level.bat`
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
name: subagent-permissions
|
||||
description: 后台子 Agent 需要预授权 Edit/Write 权限才能修改文件
|
||||
metadata:
|
||||
type: feedback
|
||||
---
|
||||
|
||||
后台子 Agent(game-dev 等)无法弹出交互式权限确认,缺少 Edit/Write 授权时会直接被拒绝文件修改。
|
||||
|
||||
**Why:** 首次启动 game-dev 后台 Agent 实现 P-011 时,因 settings.json 缺少 Edit/Write 权限,Agent 只读了技术文档但无法修改源码。
|
||||
|
||||
**How to apply:** 在 `.claude/settings.json` 的 `permissions.allow` 中添加 `"Edit"` 和 `"Write"`(2026-06-10 已添加)。如果后续新增需要后台运行的工具,同样需要预授权对应权限。
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
name: pipeline-status-20260603
|
||||
description: 2026-06-03 的流水线阶段判断结果,v0.1 处于阶段④执行案完善中
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
|
||||
# 流水线阶段判断 — 2026-06-03
|
||||
|
||||
**当前版本**: v0.1 场景漫游与环境交互
|
||||
**流水线阶段**: ④ 执行案(质量门未通过)
|
||||
**游戏开发阶段**: 预制作
|
||||
|
||||
## 各阶段状态
|
||||
|
||||
| 阶段 | 状态 |
|
||||
|------|------|
|
||||
| ① 全局设计案 | 通过 |
|
||||
| ② 版本Roadmap | 通过(但需补全 v0.5-v0.7) |
|
||||
| ③ 当前版本设计文档 | 通过 |
|
||||
| ④ 执行案 | **未通过质量门** |
|
||||
| ⑤ 程序设计 | 不存在(前置未通过) |
|
||||
| ⑥ 美术产出 | 部分就位(主角模型已开始,白盒占位未系统产出) |
|
||||
| ⑦ 程序实现 | 未开始 |
|
||||
|
||||
## 关键发现
|
||||
|
||||
- 设计文档产出良好,覆盖面广
|
||||
- 但执行案与全局设计混合在同一文档中,未按流水线要求独立
|
||||
- 程序实现完全未启动(仅有 UE5 第三人称模板代码)
|
||||
- 模板变体代码(Variant_Combat/Platforming/SideScrolling)未清理
|
||||
- GameConfig/ 无实际配置文件
|
||||
|
||||
**Why:** 首次 /plan 诊断的基线快照
|
||||
**How to apply:** 后续 /plan 调用时对比此基线判断进度变化
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
name: pipeline-status-20260605-p002
|
||||
description: P-002 执行案质量门审查结果 -- 整体不通过,三个系统均需修正
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
|
||||
P-002 执行案质量门审查于 2026-06-05 完成,整体判定不通过。
|
||||
|
||||
**审查结论**:
|
||||
- 交互系统:不通过(技术越界 + 边界情况缺失)
|
||||
- 收集系统:不通过(v0.1 执行内容匮乏 + 边界情况完全缺失)
|
||||
- 角色控制:有条件通过(需创建独立执行案 + 少量补充)
|
||||
|
||||
**核心阻塞项**:
|
||||
1. 三个系统均缺失独立执行案文件(应为 `<系统名>-execution.md`)
|
||||
2. 三个系统均缺失迭代记录表头
|
||||
3. 交互系统技术越界(JSON 数据结构、程序实现注释、白盒占位模型需求)
|
||||
4. 收集系统 v0.1 部分仅 4 行,不可直接实现
|
||||
5. 交互系统和收集系统的边界情况均严重不足
|
||||
|
||||
**审查记录**:`docs/plans/reviews/P-002-review.md`
|
||||
|
||||
**Why:** 执行案质量门是③到④的必要门槛,不通过则无法进入程序设计阶段。当前设计文档同时承担设计案和执行案双重角色,需拆分。
|
||||
|
||||
**How to apply:** 下次会话时,v0.1 的状态应为"③执行案修正中"。策划需按审查报告的修正方向产出独立执行案文件后,重新提交质量门审查。
|
||||
|
||||
**Related:** [[pipeline-status-20260605]] [[design-doc-review]]
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
name: pipeline-status-20260605
|
||||
description: P-001 原型阶段启动准备完成,当前处于流水线③执行案待确认状态
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
|
||||
## 当前状态(2026-06-05)
|
||||
|
||||
- 阶段:原型阶段
|
||||
- 当前版本:v0.1
|
||||
- 流水线位置:③执行案待确认/更新(P-002)
|
||||
- P-001 原型阶段启动准备已完成
|
||||
|
||||
## P-001 完成内容
|
||||
|
||||
1. prototype-roadmap.md 审查完成 -- v0.1 范围对齐,发现 v0.2/v0.3 有细节遗漏(建议更新但未修改)
|
||||
2. roadmap.md v0.3 已补全"零花钱经济"
|
||||
3. 角色汇报机制已建立(reports/ 下三个模板文件)
|
||||
4. 草案归档已补充 D-001 和 D-002 记录
|
||||
|
||||
## 待决策事项
|
||||
|
||||
- prototype-roadmap.md 的 4 项建议更新需用户确认
|
||||
- v0.2/v0.3 验收标准待 v0.1 完成后详细定义(合理,不阻塞)
|
||||
|
||||
## 下一步
|
||||
|
||||
P-002:v0.1 执行案确认与更新,提交执行案质量门审查
|
||||
|
||||
## 相关记忆
|
||||
|
||||
- [[pipeline-status-20260603]]:上一轮流水线状态
|
||||
- [[design-doc-review]]:交互系统技术越界、收集系统缺边界情况
|
||||
- [[roadmap-alignment]]:roadmap 对齐问题
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
name: roadmap-alignment
|
||||
description: 路线图已于 2026-06-05 全面重新评估,3 版本扩展为 4 版本,与可行性评估对齐
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
|
||||
# 路线图重新评估完成(2026-06-05)
|
||||
|
||||
原 3 版本规划(v0.1-v0.3)已扩展为 4 版本(v0.1-v0.4),关键变化:
|
||||
- 原 v0.2(弹珠+NPC+室内+音效)拆分:弹珠独立为 v0.2,NPC/时间系统为 v0.3
|
||||
- 原 v0.3(时间+经济+NPC联动)拆分:时间+NPC 为 v0.3,经济+叙事为 v0.4
|
||||
- 经济系统从 v0.3 后移到 v0.4,降低单版本工作量
|
||||
- roadmap.md 已补全批量生产阶段 v0.5-v0.7
|
||||
|
||||
**Why:** 原 v0.2/v0.3 范围过大(各 20-32 天),单人开发风险高。拆分后每版本聚焦一个核心假设
|
||||
**How to apply:** 后续版本开发时,每版本聚焦一个假设,范围不再膨胀。如果发现范围偏大应及时拆分而非堆叠
|
||||
|
||||
关联记忆:[[pipeline-status-20260605]]
|
||||
@@ -1,5 +1,4 @@
|
||||
# game-reviewer 记忆索引
|
||||
|
||||
- [Gitea Token Permission](gitea-token-permission.md) — reviewer token 仅可评论,标签操作需用 PM token
|
||||
- [P-006 Review](p006-review-note.md) — Issue #17 白模搭建审查通过记录
|
||||
- [P-009 Review](p009-review-note.md) — Issue #20 收集系统程序设计审查通过记录
|
||||
- [Gitea Token Permission](gitea-token-permission.md) — reviewer token 为空,所有操作使用 PM role
|
||||
- [P-012 Review](p012-review-note.md) — Issue #32 交互系统程序实现二次审查未通过记录
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
---
|
||||
name: gitea-token-permission
|
||||
description: reviewer token 权限限制——仅可评论,标签操作需 PM token
|
||||
description: reviewer token 权限限制——仅可评论,标签操作需用 PM token
|
||||
metadata:
|
||||
type: feedback
|
||||
---
|
||||
|
||||
reviewer token (ff336234...) 权限不足,无法修改 Issue 标签(HTTP 403: write permission is required)。评论操作正常。
|
||||
reviewer token 为空字符串,无法执行任何 API 操作(评论、标签均不可)。PM token 可执行全部操作。
|
||||
|
||||
**Why:** reviewer token 可能被配置为只读 + 评论权限。
|
||||
**Why:** gitea-config.json 中 reviewer 角色未配置 token(token 字段为空字符串)。
|
||||
|
||||
**How to apply:** 标签更新操作使用 PM 角色执行:`node .claude/skills/gitea-pm/gitea.js --role pm issue labels <number> --set <ids>`
|
||||
**How to apply:** 所有 Gitea API 操作(评论、标签更新)均使用 PM 角色执行:`node .claude/skills/gitea-collab/gitea.js --role pm issue comment/labels <number>`。评论内容仍以"审查 Agent (ai-reviewer)"署名。
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
name: p006-review-note
|
||||
description: Issue #17 P-006 白模搭建审查通过记录
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
|
||||
P-006(Issue #17)v0.1 白模关卡搭建审查于 2026-06-08 通过。
|
||||
|
||||
**结论:** 7 区域完整覆盖执行案,238 Actor,偏差记录详尽。
|
||||
|
||||
**已知偏差:** Z5 巷子宽度 260cm vs 设计 300cm(重大但不阻塞,260cm 仍可通行)。
|
||||
|
||||
**How to apply:** 后续 6b-6f 子任务可继续推进。空间精度偏差由用户决定是否修正。
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
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)
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: p012-review-note
|
||||
description: Issue #32 交互系统程序实现审查——二次审查未通过记录
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
|
||||
Issue #32 (P-012 交互系统程序实现) 二次审查未通过。首次审查发现 2 个重大问题 + 5 个中等问题。程序提交修改汇报后,二次审查发现修改汇报声称的修复并未在代码中体现,4 个问题实际未修复:
|
||||
1. SetInteracting(true) 双调用(InteractionManager 反射 + Character 直接调用)——运行时 Bug
|
||||
2. DoorActor::OnInteractStarted_Implementation 仍调用 ToggleDoor ——代码残留
|
||||
3. ExecuteCameraReturn 未清空 CachedPushCamera ——冗余回位
|
||||
4. AddToBackpack 缺 TODO 注释
|
||||
|
||||
**Why:** 程序修改汇报与实际代码变更不一致,可能修改未提交或修改了错误文件。
|
||||
|
||||
**How to apply:** 后续审查此 Issue 时,重点关注以上 4 个代码位置的修复状态,并验证修改汇报与实际代码变更一致。
|
||||
@@ -1,2 +0,0 @@
|
||||
- [Project Tech Stack](project-tech-stack.md) — D-002 确定的技术架构、模块选择、风险和工期预估
|
||||
- [Codebase Cleanup Needed](feedback-codebase-cleanup.md) — Variant_* 遗留代码需在 v0.1 前清理
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
name: feedback-codebase-cleanup
|
||||
description: 评估发现遗留代码需要清理,避免后续开发混乱
|
||||
metadata:
|
||||
type: feedback
|
||||
---
|
||||
|
||||
# 遗留代码清理
|
||||
|
||||
**Rule:** v0.1 开始前需要清理 Source/ChildHood/ 中的 Variant_Combat、Variant_Platforming、Variant_SideScrolling 三个遗留游戏模式代码。
|
||||
|
||||
**Why:** 技术可行性评估时发现这些代码与 ChildHood 核心玩法无关,是之前原型实验的遗留。保留会污染代码库、增加编译时间、干扰新开发者理解项目结构。
|
||||
|
||||
**How to apply:** 在 v0.1 实施阶段的第一步,将这些目录归档或删除。清理后 ChildHoodCharacter、ChildHoodPlayerController、ChildHoodGameMode 作为项目基类保留。
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
name: project-tech-stack
|
||||
description: ChildHood 项目的技术栈选择和架构决策记录
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
|
||||
# ChildHood 技术栈与架构决策
|
||||
|
||||
**Why:** 技术可行性评估(D-002)确定的架构方向,后续所有技术设计需以此为基准。
|
||||
|
||||
**How to apply:** 新系统设计时参照此文档选择模块和方案;如有偏差需记录原因。
|
||||
|
||||
## 引擎与插件
|
||||
- UE 5.7.4,已启用 StateTree + GameplayStateTree + ModelingToolsEditorMode
|
||||
- 不使用 GAS、Nanite、Lumen、MassEntity
|
||||
- 已有模块依赖:Engine、AIModule、UMG
|
||||
|
||||
## 核心架构方案
|
||||
- 角色控制:ACharacter + Enhanced Input + SpringArm(已有基类 ChildHoodCharacter)
|
||||
- 交互系统:C++ Interface + SphereOverlap + UMG Widget
|
||||
- NPC:StateTree(已启用插件)+ 自定义对话 JSON 解析
|
||||
- 数据驱动:DataAsset + DataTable(对应 GameConfig/ CSV/JSON)
|
||||
- 小游戏:独立 GameMode + PlayerController,进入时切换
|
||||
- 时间系统:GameState 中的 TimeManager Subsystem
|
||||
- 存档:USaveGame + SaveGameToSlot
|
||||
|
||||
## 关键风险记忆
|
||||
- 小游戏(弹珠/四驱车/FC/街机)是最大风险,内容量决定工期
|
||||
- 弹珠操作手感必须在 v0.2 做 Throwaway 原型验证
|
||||
- Source/ 中的 Variant_* 遗留代码需清理
|
||||
- 叙事文本和美术资产是隐性工作量最大的非代码任务
|
||||
|
||||
## 工期预估参考
|
||||
- v0.1: 10-18 天,v0.2: 20-32 天,v0.3: 18-29 天
|
||||
- 全项目:158-249 天(约 5-8 个月全职)
|
||||
@@ -0,0 +1,125 @@
|
||||
---
|
||||
name: "game-art-prompter"
|
||||
description: "Use this agent when the user needs to generate GPT-Image-2 prompts for game art assets, including characters, scenes, props, UI elements, or any visual art direction needs. This includes when the user describes a visual concept and needs a refined, professional prompt for image generation.\\n\\nExamples:\\n\\n- User: \"我需要一个千禧年风格的小男孩角色,穿着校服,背着书包\"\\n Assistant: \"让我使用 game-art-prompter 来为这个角色生成专业的 GPT-Image-2 提示词。\"\\n (Since the user is describing a character concept, use the Agent tool to launch the game-art-prompter agent to craft an optimized image generation prompt.)\\n\\n- User: \"帮我设计一个城郊结合区的街道场景,要有年代感\"\\n Assistant: \"这个场景描述很适合用 game-art-prompter 来生成精确的图像提示词,让我启动它。\"\\n (Since the user needs a scene visualized, use the Agent tool to launch the game-art-prompter agent to create a detailed scene prompt.)\\n\\n- User: \"需要一批道具的参考图,包括玻璃弹珠、四驱车、溜溜球\"\\n Assistant: \"我来用 game-art-prompter 为这些道具分别生成图像提示词。\"\\n (Since the user listed multiple props needing visual references, use the Agent tool to launch the game-art-prompter agent to generate individual prompts for each item.)\\n\\n- User: \"UI界面需要那种复古游戏风格的按钮和面板\"\\n Assistant: \"让我调用 game-art-prompter 来为这种复古 UI 风格生成合适的图像提示词。\"\\n (Since the user is requesting UI visual assets, use the Agent tool to launch the game-art-prompter agent to design appropriate prompts.)"
|
||||
model: opus
|
||||
color: green
|
||||
---
|
||||
|
||||
You are a senior game art director with 15+ years of experience across multiple art styles — from pixel art and low-poly to hyper-realistic renders. You specialize in crafting precise, high-quality GPT-Image-2 prompts that consistently produce stunning game art assets. You have deep knowledge of visual design principles, color theory, composition, lighting, and how to translate abstract concepts into concrete visual descriptions.
|
||||
|
||||
## Core Identity
|
||||
|
||||
You are bilingual (Chinese/English) and understand both Eastern and Western art sensibilities. You excel at taking rough descriptions — whether they're a single sentence or a detailed brief — and transforming them into optimized GPT-Image-2 prompts that produce professional-quality game art.
|
||||
|
||||
## Project Context
|
||||
|
||||
You are working on **ChildHood(我的童年)**, a UE5.7.4 game with a **3D Lowpoly** art style featuring:
|
||||
- **Tone**: Warm colors, nostalgic atmosphere,千禧年 (millennium era) suburban-rural China aesthetic
|
||||
- **Setting**: Year 2000, urban-rural fringe areas (城郊结合区) like Nanjing's Maigaoqiao
|
||||
- **Style Reference**: NDS「放学后少年」free exploration + PS「我的暑假」daily narrative rhythm
|
||||
- **Visual Keywords**: Lowpoly, warm palette, soft lighting, childhood nostalgia, Chinese suburban 2000s
|
||||
|
||||
## Your Workflow
|
||||
|
||||
### 1. Understand the Request
|
||||
- Identify the asset type: **character (角色)**, **scene/environment (场景)**, **prop/item (道具)**, or **UI element (UI)**
|
||||
- Extract the core visual intent from the user's description
|
||||
- Determine the art style requirements (default to project's Lowpoly warm style unless specified otherwise)
|
||||
- Ask clarifying questions if the description is ambiguous
|
||||
|
||||
### 2. Analyze & Expand
|
||||
- Break down the visual elements: silhouette, color palette, materials, lighting, mood
|
||||
- Consider period-accurate details for 千禧年 setting when relevant
|
||||
- Think about how the asset fits within the broader game world
|
||||
- Note any cultural specifics (Chinese suburban life circa 2000)
|
||||
|
||||
### 3. Craft the GPT-Image-2 Prompt
|
||||
|
||||
Follow these prompt engineering principles:
|
||||
|
||||
**Structure your prompts in this order:**
|
||||
1. **Subject** — Clear, specific description of what to generate
|
||||
2. **Style** — Art style, rendering approach (e.g., "3D lowpoly", "cel-shaded")
|
||||
3. **Details** — Materials, textures, specific features
|
||||
4. **Composition** — Camera angle, framing, pose
|
||||
5. **Lighting** — Light source, time of day, mood
|
||||
6. **Color** — Palette, saturation, warmth
|
||||
7. **Background** — Environment context (or "white background" for isolated assets)
|
||||
8. **Negative/Exclusion** — What to avoid (optional but useful)
|
||||
|
||||
**Prompt Language Rules:**
|
||||
- Write the **main GPT-Image-2 prompt in English** for best generation quality
|
||||
- Provide a **Chinese explanation** of your creative decisions
|
||||
- If the user communicates in Chinese, respond in Chinese with English prompts embedded
|
||||
|
||||
**Quality Markers to Include (when applicable):**
|
||||
- Art style qualifiers: "3D lowpoly", "low-polygon count", "flat-shaded"
|
||||
- Quality boosters: "clean geometry", "consistent edge loops", "game-ready asset"
|
||||
- Mood enhancers: "warm lighting", "nostalgic atmosphere", "soft ambient occlusion"
|
||||
- Technical specs: "isometric view", "front view", "white background for extraction"
|
||||
|
||||
### 4. Output Format
|
||||
|
||||
For each asset, provide:
|
||||
|
||||
```
|
||||
## [Asset Name / 资产名称]
|
||||
**类型**: Character / Scene / Prop / UI
|
||||
**风格**: [Art style used]
|
||||
|
||||
### Prompt
|
||||
```
|
||||
[The complete GPT-Image-2 prompt, in English]
|
||||
```
|
||||
|
||||
### 设计说明
|
||||
- [Key design decisions explained in Chinese]
|
||||
- [Why specific visual choices were made]
|
||||
- [How it fits the project's aesthetic]
|
||||
|
||||
### 参数建议
|
||||
- 尺寸比例: [e.g., 1:1, 16:9, 4:3]
|
||||
- 视角建议: [camera angle reasoning]
|
||||
```
|
||||
|
||||
## Asset-Specific Guidelines
|
||||
|
||||
### Characters (角色)
|
||||
- Include: age, build, hairstyle, clothing (era-appropriate for 千禧年), expression, pose
|
||||
- 千禧年服装参考: 校服(运动服款)、帆布鞋、运动鞋、宽大T恤
|
||||
- Lowpoly specifics: simplified facial features, geometric hair, bold color blocks
|
||||
- Default to: front-facing 3/4 view, T-pose or natural idle pose
|
||||
|
||||
### Scenes/Environments (场景)
|
||||
- Include: time of day, weather, key landmarks, vegetation, architectural style
|
||||
- 千禧年场景元素: 红砖楼、小卖部、巷弄、电线杆、晾衣绳、自行车棚
|
||||
- Lowpoly specifics: blocky buildings, simplified trees, stylized clouds
|
||||
- Default to: wide establishing shot or eye-level perspective
|
||||
|
||||
### Props/Items (道具)
|
||||
- Include: material, size reference, worn/new condition, cultural significance
|
||||
- 千禧年道具参考: 玻璃弹珠、四驱车、溜溜球、水浒卡、小浣熊干脆面、铁皮青蛙
|
||||
- Lowpoly specifics: faceted surfaces, limited color palette, readable silhouette
|
||||
- Default to: isolated on white/transparent background, 3/4 view
|
||||
|
||||
### UI Elements (UI)
|
||||
- Include: function, interaction state, size relative to screen
|
||||
- 风格参考: 复古游戏界面、像素风边框、手绘质感
|
||||
- Lowpoly specifics: geometric shapes, bold icons, clean readable layout
|
||||
- Default to: flat/front view, clear silhouette
|
||||
|
||||
## Iteration & Refinement
|
||||
|
||||
- Always offer 2-3 **prompt variations** for each asset (different angles, moods, or styles)
|
||||
- If the user provides feedback on a generated image, adjust the prompt based on what needs to change
|
||||
- Keep a mental note of which prompt structures produce the best results
|
||||
|
||||
## Important Rules
|
||||
|
||||
1. **Never sacrifice clarity for creativity** — a prompt must be understandable first
|
||||
2. **Stay era-appropriate** — no modern smartphones, contemporary brands, or anachronistic elements in千禧年 assets
|
||||
3. **Maintain style consistency** — all prompts should feel like they belong in the same game world
|
||||
4. **Be specific about low-poly** — include geometry and shading descriptors that guide toward the low-poly aesthetic
|
||||
5. **Respect the warm, nostalgic tone** — avoid dark, gritty, or cold palettes unless explicitly requested
|
||||
6. **Provide actionable output** — every prompt should be ready to copy-paste into GPT-Image-2
|
||||
|
||||
@@ -3,7 +3,6 @@ name: "game-designer"
|
||||
description: "策划 Agent:头脑风暴、产出设计文档(概念草案/系统设计案/执行案)、场景设计、玩法机制定义、数值配置、版本复盘文档更新。触发关键词:设计、执行案、头脑风暴、概念草案、数值。"
|
||||
model: opus
|
||||
color: red
|
||||
memory: project
|
||||
skills:
|
||||
- gitea-collab
|
||||
- design-execution
|
||||
@@ -342,17 +341,4 @@ bash .claude/scripts/git-workflow.sh push
|
||||
|
||||
---
|
||||
|
||||
**持续更新你的 Agent 记忆**——当你发现设计模式、已确认的创意决策、头脑风暴共识点、跨系统关系、以及项目特有的叙事元素时记录下来。这会在多次对话中积累组织知识。写简洁的笔记。
|
||||
|
||||
---
|
||||
|
||||
## Memory
|
||||
|
||||
记忆系统规范参见 `.claude/skills/agent-memory.md`。
|
||||
你的记忆目录:`.claude/agent-memory/game-designer/`。
|
||||
你的 MEMORY.md 索引:`.claude/agent-memory/game-designer/MEMORY.md`。
|
||||
记忆索引行数上限 200 行,超过后截断。
|
||||
|
||||
## MEMORY.md
|
||||
|
||||
Your MEMORY.md is currently empty. When you save new memories, they will appear here.
|
||||
|
||||
@@ -3,7 +3,6 @@ name: "game-dev"
|
||||
description: "程序 Agent:根据执行案设计技术方案、编写 C++/蓝图代码、集成美术资产、版本复盘偏差汇报。触发关键词:技术方案、实现代码、偏差报告、修正技术方案。"
|
||||
model: opus
|
||||
color: blue
|
||||
memory: project
|
||||
skills:
|
||||
- requirements-analysis
|
||||
- level-builder
|
||||
@@ -284,17 +283,4 @@ bash .claude/scripts/git-workflow.sh catchup
|
||||
|
||||
---
|
||||
|
||||
**持续更新你的 Agent 记忆**——当你发现代码库架构模式、UE5 特定的实现技巧、性能特征、项目中的常见技术挑战、有用的代码片段和模式、系统与组件之间的关系时记录下来。这会在多次对话中积累技术组织知识。
|
||||
|
||||
---
|
||||
|
||||
## Memory
|
||||
|
||||
记忆系统规范参见 `.claude/skills/agent-memory.md`。
|
||||
你的记忆目录:`.claude/agent-memory/ue5-programmer/`。
|
||||
你的 MEMORY.md 索引:`.claude/agent-memory/ue5-programmer/MEMORY.md`。
|
||||
记忆索引行数上限 200 行,超过后截断。
|
||||
|
||||
## MEMORY.md
|
||||
|
||||
Your MEMORY.md is currently empty. When you save new memories, they will appear here.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
name: "game-pm"
|
||||
description: "项目管理 Agent:进度管理(3阶段状态机)、任务分配与追踪、开发决策咨询。触发关键词:开始工作、进度、分配任务、优先级。"
|
||||
model: opus
|
||||
memory: project
|
||||
skills:
|
||||
- gitea-pm
|
||||
- gitea-collab
|
||||
@@ -209,11 +208,4 @@ bash .claude/scripts/git-workflow.sh post-release
|
||||
|
||||
---
|
||||
|
||||
## Memory
|
||||
|
||||
记忆系统规范参见 `.claude/skills/agent-memory.md`。
|
||||
你的记忆目录:`.claude/agent-memory/game-project-pm/`。
|
||||
你的 MEMORY.md 索引:`.claude/agent-memory/game-project-pm/MEMORY.md`。
|
||||
记忆索引行数上限 200 行,超过后截断。
|
||||
|
||||
持续更新你的记忆——当你发现项目模式、开发节奏、估算偏差、常见阻塞、有效任务粒度时记录下来。记忆类型包括:user、feedback、project、reference。写简洁、可操作的笔记。
|
||||
---
|
||||
|
||||
@@ -3,7 +3,6 @@ name: "game-reviewer"
|
||||
description: "审查子代理 — 角色完成工作后启动,审查交付物质量,更新 Issue 标签和评论。使用 ai-reviewer 账号操作 Gitea。"
|
||||
model: opus
|
||||
color: purple
|
||||
memory: project
|
||||
tools: Read, Grep, Glob, Bash, Skill
|
||||
skills:
|
||||
- gitea-collab
|
||||
@@ -148,9 +147,4 @@ skills:
|
||||
|
||||
---
|
||||
|
||||
## Memory
|
||||
|
||||
记忆系统规范参见 `.claude/skills/agent-memory.md`。
|
||||
你的记忆目录:`.claude/agent-memory/game-reviewer/`。
|
||||
你的 MEMORY.md 索引:`.claude/agent-memory/game-reviewer/MEMORY.md`。
|
||||
记忆索引行数上限 200 行,超过后截断。
|
||||
---
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
---
|
||||
name: "agent-memory"
|
||||
description: "Agent 持久化记忆系统规范 — memory 类型、保存规则、索引维护、验证规则"
|
||||
---
|
||||
|
||||
# Persistent Agent Memory
|
||||
|
||||
You have a persistent, file-based memory system. Your memory directory and MEMORY.md path are specified in your agent definition file — write to the memory directory directly with the Write tool (do not run mkdir or check for its existence).
|
||||
|
||||
You should build up this memory system over time so that future conversations can have a complete picture of who the user is, how they'd like to collaborate with you, what behaviors to avoid or repeat, and the context behind the work the user gives you.
|
||||
|
||||
If the user explicitly asks you to remember something, save it immediately as whichever type fits best. If they ask you to forget something, find and remove the relevant entry.
|
||||
|
||||
## Types of memory
|
||||
|
||||
There are several discrete types of memory that you can store in your memory system:
|
||||
|
||||
<types>
|
||||
<type>
|
||||
<name>user</name>
|
||||
<description>Contain information about the user's role, goals, responsibilities, and knowledge. Great user memories help you tailor your future behavior to the user's preferences and perspective. Your goal in reading and writing these memories is to build up an understanding of who the user is and how you can be most helpful to them specifically. For example, you should collaborate with a senior software engineer differently than a student who is coding for the very first time. Keep in mind, that the aim here is to be helpful. Avoid writing memories about the user that could be viewed as a negative judgement or that are not relevant to the work you are trying to accomplish together.</description>
|
||||
<when_to_save>When you learn any details about the user's role, preferences, responsibilities, or knowledge</when_to_save>
|
||||
<how_to_use>When your work should be informed by the user's profile or perspective. For example, if the user is asking you to explain a part of the code, you should answer that question in a way that is tailored to the specific details that they will find most valuable or that helps them build their mental model in relation to domain knowledge they already have.</how_to_use>
|
||||
<examples>
|
||||
user: I'm a data scientist investigating what logging we have in place
|
||||
assistant: [saves user memory: user is a data scientist, currently focused on observability/logging]
|
||||
|
||||
user: I've been writing Go for ten years but this is my first time touching the React side of this repo
|
||||
assistant: [saves user memory: deep Go expertise, new to React and this project's frontend — frame frontend explanations in terms of backend analogues]
|
||||
</examples>
|
||||
</type>
|
||||
<type>
|
||||
<name>feedback</name>
|
||||
<description>Guidance the user has given you about how to approach work — both what to avoid and what to keep doing. These are a very important type of memory to read and write as they allow you to remain coherent and responsive to the way you should approach work in the project. Record from failure AND success: if you only save corrections, you will avoid past mistakes but drift away from approaches the user has already validated, and may grow overly cautious.</description>
|
||||
<when_to_save>Any time the user corrects your approach ("no not that", "don't", "stop doing X") OR confirms a non-obvious approach worked ("yes exactly", "perfect, keep doing that", accepting an unusual choice without pushback). Corrections are easy to notice; confirmations are quieter — watch for them. In both cases, save what is applicable to future conversations, especially if surprising or not obvious from the code. Include *why* so you can judge edge cases later.</when_to_save>
|
||||
<how_to_use>Let these memories guide your behavior so that the user does not need to offer the same guidance twice.</how_to_use>
|
||||
<body_structure>Lead with the rule itself, then a **Why:** line (the reason the user gave — often a past incident or strong preference) and a **How to apply:** line (when/where this guidance kicks in). Knowing *why* lets you judge edge cases instead of blindly following the rule.</body_structure>
|
||||
<examples>
|
||||
user: don't mock the database in these tests — we got burned last quarter when mocked tests passed but the prod migration failed
|
||||
assistant: [saves feedback memory: integration tests must hit a real database, not mocks. Reason: prior incident where mock/prod divergence masked a broken migration]
|
||||
|
||||
user: stop summarizing what you just did at the end of every response, I can read the diff
|
||||
assistant: [saves feedback memory: this user wants terse responses with no trailing summaries]
|
||||
|
||||
user: yeah the single bundled PR was the right call here, splitting this one would've just been churn
|
||||
assistant: [saves feedback memory: for refactors in this area, user prefers one bundled PR over many small ones. Confirmed after I chose this approach — a validated judgment call, not a correction]
|
||||
</examples>
|
||||
</type>
|
||||
<type>
|
||||
<name>project</name>
|
||||
<description>Information that you learn about ongoing work, goals, initiatives, bugs, or incidents within the project that is not otherwise derivable from the code or git history. Project memories help you understand the broader context and motivation behind the work the user is doing within this working directory.</description>
|
||||
<when_to_save>When you learn who is doing what, why, or by when. These states change relatively quickly so try to keep your understanding of this up to date. Always convert relative dates in user messages to absolute dates when saving (e.g., "Thursday" → "2026-03-05"), so the memory remains interpretable after time passes.</when_to_save>
|
||||
<how_to_use>Use these memories to more fully understand the details and nuance behind the user's request and make better informed suggestions.</how_to_use>
|
||||
<body_structure>Lead with the fact or decision, then a **Why:** line (the motivation — often a constraint, deadline, or stakeholder ask) and a **How to apply:** line (how this should shape your suggestions). Project memories decay fast, so the why helps future-you judge whether the memory is still load-bearing.</body_structure>
|
||||
<examples>
|
||||
user: we're freezing all non-critical merges after Thursday — mobile team is cutting a release branch
|
||||
assistant: [saves project memory: merge freeze begins 2026-03-05 for mobile release cut. Flag any non-critical PR work scheduled after that date]
|
||||
|
||||
user: the reason we're ripping out the old auth middleware is that legal flagged it for storing session tokens in a way that doesn't meet the new compliance requirements
|
||||
assistant: [saves project memory: auth middleware rewrite is driven by legal/compliance requirements around session token storage, not tech-debt cleanup — scope decisions should favor compliance over ergonomics]
|
||||
</examples>
|
||||
</type>
|
||||
<type>
|
||||
<name>reference</name>
|
||||
<description>Stores pointers to where information can be found in external systems. These memories allow you to remember where to look to find up-to-date information outside of the project directory.</description>
|
||||
<when_to_save>When you learn about resources in external systems and their purpose. For example, that bugs are tracked in a specific project in Linear or that feedback can be found in a specific Slack channel.</when_to_save>
|
||||
<how_to_use>When the user references an external system or information that may be in an external system.</how_to_use>
|
||||
<examples>
|
||||
user: check the Linear project "INGEST" if you want context on these tickets, that's where we track all pipeline bugs
|
||||
assistant: [saves reference memory: pipeline bugs are tracked in Linear project "INGEST"]
|
||||
|
||||
user: the Grafana board at grafana.internal/d/api-latency is what oncall watches — if you're touching request handling, that's the thing that'll page someone
|
||||
assistant: [saves reference memory: grafana.internal/d/api-latency is the oncall latency dashboard — check it when editing request-path code]
|
||||
</examples>
|
||||
</type>
|
||||
</types>
|
||||
|
||||
## What NOT to save in memory
|
||||
|
||||
- Code patterns, conventions, architecture, file paths, or project structure — these can be derived by reading the current project state.
|
||||
- Git history, recent changes, or who-changed-what — `git log` / `git blame` are authoritative.
|
||||
- Debugging solutions or fix recipes — the fix is in the code; the commit message has the context.
|
||||
- Anything already documented in CLAUDE.md files.
|
||||
- Ephemeral task details: in-progress work, temporary state, current conversation context.
|
||||
|
||||
These exclusions apply even when the user explicitly asks you to save. If they ask you to save a PR list or activity summary, ask what was *surprising* or *non-obvious* about it — that is the part worth keeping.
|
||||
|
||||
## How to save memories
|
||||
|
||||
Saving a memory is a two-step process:
|
||||
|
||||
**Step 1** — write the memory to its own file (e.g., `user_role.md`, `feedback_testing.md`) using this frontmatter format:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: {{short-kebab-case-slug}}
|
||||
description: {{one-line summary — used to decide relevance in future conversations, so be specific}}
|
||||
metadata:
|
||||
type: {{user, feedback, project, reference}}
|
||||
---
|
||||
|
||||
{{memory content — for feedback/project types, structure as: rule/fact, then **Why:** and **How to apply:** lines. Link related memories with [[their-name]].}}
|
||||
```
|
||||
|
||||
In the body, link to related memories with `[[name]]`, where `name` is the other memory's `name:` slug. Link liberally — a `[[name]]` that doesn't match an existing memory yet is fine; it marks something worth writing later, not an error.
|
||||
|
||||
**Step 2** — add a pointer to that file in `MEMORY.md`. `MEMORY.md` is an index, not a memory — each entry should be one line, under ~150 characters: `- [Title](file.md) — one-line hook`. It has no frontmatter. Never write memory content directly into `MEMORY.md`.
|
||||
|
||||
- `MEMORY.md` is always loaded into your conversation context — lines after 200 will be truncated, so keep the index concise
|
||||
- Keep the name, description, and type fields in memory files up-to-date with the content
|
||||
- Organize memory semantically by topic, not chronologically
|
||||
- Update or remove memories that turn out to be wrong or outdated
|
||||
- Do not write duplicate memories. First check if there is an existing memory you can update before writing a new one.
|
||||
|
||||
## When to access memories
|
||||
- When memories seem relevant, or the user references prior-conversation work.
|
||||
- You MUST access memory when the user explicitly asks you to check, recall, or remember.
|
||||
- If the user says to *ignore* or *not use* memory: Do not apply remembered facts, cite, compare against, or mention memory content.
|
||||
- Memory records can become stale over time. Use memory as context for what was true at a given point in time. Before answering the user or building assumptions based solely on information in memory records, verify that the memory is still correct and up-to-date by reading the current state of the files or resources. If a recalled memory conflicts with current information, trust what you observe now — and update or remove the stale memory rather than acting on it.
|
||||
|
||||
## Before recommending from memory
|
||||
|
||||
A memory that names a specific function, file, or flag is a claim that it existed *when the memory was written*. It may have been renamed, removed, or never merged. Before recommending it:
|
||||
|
||||
- If the memory names a file path: check the file exists.
|
||||
- If the memory names a function or flag: grep for it.
|
||||
- If the user is about to act on your recommendation (not just asking about history), verify first.
|
||||
|
||||
"The memory says X exists" is not the same as "X exists now."
|
||||
|
||||
A memory that summarizes repo state (activity logs, architecture snapshots) is frozen in time. If the user asks about *recent* or *current* state, prefer `git log` or reading the code over recalling the snapshot.
|
||||
|
||||
## Memory and other forms of persistence
|
||||
Memory is one of several persistence mechanisms available to you as you assist the user in a given conversation. The distinction is often that memory can be recalled in future conversations and should not be used for persisting information that is only useful within the scope of the current conversation.
|
||||
- When to use or update a plan instead of memory: If you are about to start a non-trivial implementation task and would like to reach alignment with the user on your approach you should use a Plan rather than saving this information to memory. Similarly, if you already have a plan within the conversation and you have changed your approach persist that change by updating the plan rather than saving a memory.
|
||||
- When to use or update tasks instead of memory: When you need to break your work into discrete steps or keep track of your progress use tasks instead of saving to memory. Tasks are great for persisting information about the work that needs to be done in the current conversation, but memory should be reserved for information that will be useful in future conversations.
|
||||
|
||||
- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project
|
||||
+3
-1
@@ -93,10 +93,12 @@ Desktop.ini
|
||||
*.lfs
|
||||
|
||||
# ==============================
|
||||
# Claude Code 工作树
|
||||
# Claude Code 工作树与运行时残留
|
||||
# ==============================
|
||||
.claude/worktrees/
|
||||
.worktrees/
|
||||
# agent 运行时记忆(项目已移除 agent 记忆系统,见 commit 906e2aa)
|
||||
.claude/agent-memory/
|
||||
|
||||
# ==============================
|
||||
# Gitea 配置(含 Token,每台机器独立配置)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -37,6 +37,20 @@ marble_large,大弹珠,2.5,20,80,25
|
||||
}
|
||||
```
|
||||
|
||||
## 弹珠小游戏配置(v0.2)
|
||||
|
||||
| 文件 | 格式 | 用途 | 关键单位 |
|
||||
|------|------|------|----------|
|
||||
| `marble-types.csv` | CSV | 弹珠属性(玻璃/陶瓷/铁 + 目标弹珠 + 兜底弹珠) | radius_m(米)、mass(相对值)、frictionAdd/restitution(无量纲 0-1) |
|
||||
| `marble-venues.csv` | CSV | 场地配置(水泥地/泥土地) | friction(无量纲) |
|
||||
| `marble-npc.csv` | CSV | NPC 对手配置(死党B/小霸王) | angleError_deg(度)、powerError/basePower(%)、distComp(%/m) |
|
||||
| `marble-config.json` | JSON | 画圈弹规则、操作、物理、库存、时序等键值参数 | 见各字段 unit |
|
||||
|
||||
> CSV 列名后缀表示单位(`_m` 米、`_deg` 度、`_pct` 百分比、`_pctPerM` 每米百分比、`_mps` 米/秒、`_hz` 赫兹)。JSON 每个参数为 `{value, min, max, unit, note}` 对象,value 为建议值(程序导入用),min/max 为可调范围。
|
||||
> 来源:`docs/design/弹珠小游戏-执行案.md` v0.2.1。摩擦组合规则:最终摩擦 = 场地摩擦 + 弹珠摩擦加成。
|
||||
|
||||
---
|
||||
|
||||
## 工作流
|
||||
|
||||
1. 策划 Agent 创建/更新配置文件
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"_meta": {
|
||||
"doc": "弹珠小游戏配置(v0.2)",
|
||||
"source": "弹珠小游戏-执行案.md v0.2.1",
|
||||
"field_note": "value=建议值(程序导入用),min/max=可调范围(调参参考),unit=单位,note=说明"
|
||||
},
|
||||
"circleRule": {
|
||||
"circleRadius_m": { "value": 1.0, "min": 0.8, "max": 1.5, "unit": "m", "note": "弹出判定圈半径" },
|
||||
"targetCount": { "value": 5, "min": 3, "max": 8, "unit": "颗", "note": "圈中央目标弹珠数" },
|
||||
"popTolerance_m": { "value": 0.01, "min": 0.005, "max": 0.02, "unit": "m", "note": "弹出判定容差,统一带容差阈值" },
|
||||
"boundaryExpand_m": { "value": 0.5, "min": 0.3, "max": 1.0, "unit": "m", "note": "场地隐形反弹边界外扩" },
|
||||
"betMarbleCount": { "value": 1, "min": 1, "max": 3, "unit": "颗", "note": "胜负流转的对赌弹珠数" },
|
||||
"turnOrder": { "value": "player_first", "note": "v0.2 固定玩家先手" }
|
||||
},
|
||||
"operation": {
|
||||
"maxDragScreenRatio": { "value": 0.15, "min": 0.08, "max": 0.25, "unit": "屏幕高度占比", "note": "力度100%对应的拖拽距离,相对屏幕高度" },
|
||||
"minValidPower": { "value": 0.10, "min": 0.05, "max": 0.20, "unit": "比例", "note": "低于此松开视为取消" },
|
||||
"predictionLineLength_m": { "value": 3.0, "min": 2.0, "max": 5.0, "unit": "m", "note": "轨迹预测线最大显示长度" },
|
||||
"maxLaunchSpeed_mps": { "value": 8.0, "min": 5.0, "max": 12.0, "unit": "m/s", "note": "100%力度弹射初速度,对所有弹珠相同" },
|
||||
"shooterHighlightFreq_hz": { "value": 1.0, "min": 0.5, "max": 2.0, "unit": "Hz", "note": "PlayerAim 射手弹珠脉冲发光频率" }
|
||||
},
|
||||
"physics": {
|
||||
"stopSpeedThreshold_mps": { "value": 0.05, "min": 0.02, "max": 0.10, "unit": "m/s", "note": "判定弹珠停止的速度阈值" },
|
||||
"stableTime_s": { "value": 1.0, "min": 0.5, "max": 2.0, "unit": "秒", "note": "低于阈值持续至此判定停止" },
|
||||
"simulateTimeout_s": { "value": 15, "min": 10, "max": 30, "unit": "秒", "note": "物理结算超时强制停止" },
|
||||
"boundaryRestitution": { "value": 0.5, "min": 0.3, "max": 0.7, "unit": "系数", "note": "弹珠-场地边界反弹系数" },
|
||||
"frictionCombineRule": { "value": "venue_friction + marble_frictionAdd", "note": "最终摩擦=场地摩擦+弹珠摩擦加成" }
|
||||
},
|
||||
"inventory": {
|
||||
"initialCount": { "value": 5, "min": 3, "max": 10, "unit": "颗", "note": "玩家初始对战库存" },
|
||||
"guaranteeFloor": { "value": 3, "min": 2, "max": 5, "unit": "颗", "note": "保底下限,玩家败时库存不低于此" }
|
||||
},
|
||||
"timing": {
|
||||
"readyDuration_s": { "value": 0.8, "min": 0.5, "max": 1.2, "unit": "秒", "note": "进入就位表现(原位转向+蹲下)" },
|
||||
"enterCameraDuration_s": { "value": 0.6, "min": 0.4, "max": 1.0, "unit": "秒", "note": "第三人称→斜俯视相机切换" },
|
||||
"exitCameraDuration_s": { "value": 0.6, "min": 0.4, "max": 1.0, "unit": "秒", "note": "斜俯视→第三人称相机切换" },
|
||||
"standupDuration_s": { "value": 0.8, "min": 0.5, "max": 1.2, "unit": "秒", "note": "退出站起表现" },
|
||||
"resultDisplayDuration_s": { "value": 2.0, "min": 1.5, "max": 3.0, "unit": "秒", "note": "胜负结果显示时长" },
|
||||
"npcThinkDelay_s": { "value": 1.0, "min": 0.5, "max": 2.0, "unit": "秒", "note": "NPC 思考延迟" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
id,displayName,angleError_deg,angleErrorMin_deg,angleErrorMax_deg,powerError_pct,powerErrorMin_pct,powerErrorMax_pct,basePower_pct,basePowerMin_pct,basePowerMax_pct,distComp_pctPerM,distCompMin_pctPerM,distCompMax_pctPerM,strategyNote,marbleType
|
||||
friend_b,死党B(默认),3,1,10,10,5,25,50,30,70,15,10,25,瞄准最近目标弹珠,glass
|
||||
bully,小霸王(备选),8,1,10,20,5,25,60,30,70,15,10,25,瞄准距圈边缘最近易弹出目标,iron
|
||||
|
@@ -0,0 +1,6 @@
|
||||
id,displayName,radius_m,radiusMin_m,radiusMax_m,mass,massMin,massMax,frictionAdd,frictionAddMin,frictionAddMax,restitution,restitutionMin,restitutionMax,strategyNote
|
||||
glass,玻璃弹珠(标准),0.015,0.01,0.02,1.0,0.8,1.2,0.0,0.0,0.2,0.5,0.4,0.7,滚得远走线精准远距主力
|
||||
ceramic,陶瓷弹珠(粗糙),0.015,0.01,0.02,1.0,0.8,1.2,0.3,0.2,0.5,0.8,0.7,0.9,滚得近碰撞利落近距强撞
|
||||
iron,铁弹珠(重),0.012,0.008,0.015,2.5,2.0,3.0,0.1,0.0,0.3,0.6,0.5,0.8,质量大撞散目标警惕贪力反噬
|
||||
target,目标弹珠(等同玻璃),0.015,0.01,0.02,1.0,0.8,1.2,0.0,0.0,0.2,0.5,0.4,0.7,圈中央中立目标弹珠
|
||||
fallback,系统基础弹珠(等同玻璃),0.015,0.01,0.02,1.0,0.8,1.2,0.0,0.0,0.2,0.5,0.4,0.7,库存异常兜底用
|
||||
|
@@ -0,0 +1,3 @@
|
||||
id,displayName,friction,frictionMin,frictionMax,behaviorNote
|
||||
concrete,水泥地,0.3,0.2,0.5,摩擦低滚动均匀距离远走线可预测适合精准操作
|
||||
dirt,泥土地,0.6,0.4,0.8,摩擦高减速快滚动距离明显短需更大力度到达圈心
|
||||
|
@@ -26,6 +26,7 @@ public class ChildHood : ModuleRules
|
||||
PublicIncludePaths.AddRange(new string[] {
|
||||
"ChildHood",
|
||||
"ChildHood/Interaction",
|
||||
"ChildHood/Collection",
|
||||
"ChildHood/Variant_Platforming",
|
||||
"ChildHood/Variant_Platforming/Animation",
|
||||
"ChildHood/Variant_Combat",
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
// ChildHood Project - 玩家角色 实现(v1.1 Effect/Action 架构适配)
|
||||
// 变更:通过 ASystemDriver 访问 FInteractionManager + FEffectManager
|
||||
// 新增 BeginPlay 初始化 PromptComponent
|
||||
// 删除对旧 UInteractionManager 的依赖
|
||||
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "ChildHoodCharacter.h"
|
||||
@@ -11,7 +16,13 @@
|
||||
#include "EnhancedInputSubsystems.h"
|
||||
#include "InputActionValue.h"
|
||||
#include "ChildHood.h"
|
||||
#include "Interaction/InteractionPromptComponent.h"
|
||||
#include "Collection/CollectionHUDComponent.h"
|
||||
#include "SystemDriver.h"
|
||||
#include "Interaction/InteractionManager.h"
|
||||
#include "Interaction/EffectManager.h"
|
||||
#include "Interaction/InteractableComponent.h"
|
||||
#include "Interaction/InteractionTypes.h"
|
||||
|
||||
AChildHoodCharacter::AChildHoodCharacter()
|
||||
{
|
||||
@@ -55,8 +66,34 @@ AChildHoodCharacter::AChildHoodCharacter()
|
||||
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
|
||||
FollowCamera->bUsePawnControlRotation = false;
|
||||
|
||||
// 创建交互管理器组件
|
||||
InteractionManager = CreateDefaultSubobject<UInteractionManager>(TEXT("InteractionManager"));
|
||||
// 创建交互提示组件(替代旧 InteractionManager)
|
||||
PromptComponent = CreateDefaultSubobject<UInteractionPromptComponent>(TEXT("PromptComponent"));
|
||||
|
||||
// 创建收集系统 HUD 组件(表现层,挂在角色上;Tick 兜底查找 Driver)
|
||||
CollectionHUDComponent = CreateDefaultSubobject<UCollectionHUDComponent>(TEXT("CollectionHUDComponent"));
|
||||
}
|
||||
|
||||
void AChildHoodCharacter::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// 初始化 PromptComponent — 将其与 ASystemDriver 关联
|
||||
if (PromptComponent)
|
||||
{
|
||||
if (ASystemDriver* Driver = ASystemDriver::Get(this))
|
||||
{
|
||||
PromptComponent->Initialize(Driver);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化收集系统 HUD 组件(Driver 弱引用;Tick 会兜底查找,规避 BeginPlay 时序竞争)
|
||||
if (CollectionHUDComponent)
|
||||
{
|
||||
if (ASystemDriver* Driver = ASystemDriver::Get(this))
|
||||
{
|
||||
CollectionHUDComponent->Initialize(Driver);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AChildHoodCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
||||
@@ -71,7 +108,7 @@ void AChildHoodCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInput
|
||||
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AChildHoodCharacter::Look);
|
||||
EnhancedInputComponent->BindAction(MouseLookAction, ETriggerEvent::Triggered, this, &AChildHoodCharacter::Look);
|
||||
|
||||
// Interaction — delegates to InteractionManager
|
||||
// Interaction — delegates to ASystemDriver
|
||||
if (InteractAction)
|
||||
{
|
||||
EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Started, this, &AChildHoodCharacter::OnInteract);
|
||||
@@ -111,30 +148,59 @@ void AChildHoodCharacter::Look(const FInputActionValue& Value)
|
||||
|
||||
void AChildHoodCharacter::OnInteract(const FInputActionValue& Value)
|
||||
{
|
||||
if (UInteractionManager* IM = GetInteractionManager())
|
||||
ASystemDriver* Driver = ASystemDriver::Get(this);
|
||||
if (!Driver) return;
|
||||
|
||||
auto& Mgr = Driver->GetInteractionManager();
|
||||
auto& EFM = Driver->GetEffectManager();
|
||||
|
||||
// E 键三重功能:
|
||||
// 1. Free 状态:发起交互
|
||||
// 2. WaitForInput 暂停态:恢复 Effect(观察/对话类按 E 继续)
|
||||
// 3. 可取消 Effect 执行中:取消 Effect
|
||||
|
||||
if (Mgr.GetState() == EInteractionState::Free)
|
||||
{
|
||||
// E 键双重功能:Free 发起交互,Active 结束交互
|
||||
IM->BeginInteract();
|
||||
IM->CancelInteract();
|
||||
// 发起交互
|
||||
auto* Selected = Mgr.BeginInteract();
|
||||
if (!Selected) return;
|
||||
|
||||
SetInteracting(true);
|
||||
|
||||
FEffectContext Ctx;
|
||||
Ctx.Instigator = this;
|
||||
Ctx.TargetComp = Selected;
|
||||
Ctx.TargetActor = Selected->GetOwner();
|
||||
EFM.PlayEffect(Selected->GetInteractableData().InteractEffect, Ctx);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Executing 状态 → 尝试取消/恢复
|
||||
EFM.CancelEffect();
|
||||
}
|
||||
}
|
||||
|
||||
void AChildHoodCharacter::OnCancel(const FInputActionValue& Value)
|
||||
{
|
||||
if (UInteractionManager* IM = GetInteractionManager())
|
||||
{
|
||||
IM->CancelInteract();
|
||||
}
|
||||
ASystemDriver* Driver = ASystemDriver::Get(this);
|
||||
if (!Driver) return;
|
||||
|
||||
auto& EFM = Driver->GetEffectManager();
|
||||
EFM.CancelEffect();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
ASystemDriver* Driver = ASystemDriver::Get(this);
|
||||
if (!Driver) return;
|
||||
|
||||
auto& Mgr = Driver->GetInteractionManager();
|
||||
// 交互中不可切换
|
||||
if (Mgr.GetState() != EInteractionState::Free) return;
|
||||
|
||||
float DirectionValue = Value.Get<float>();
|
||||
int32 Direction = (DirectionValue >= 0.f) ? 1 : -1;
|
||||
Mgr.SwitchTarget(Direction);
|
||||
}
|
||||
|
||||
void AChildHoodCharacter::DoMove(float Right, float Forward)
|
||||
@@ -177,9 +243,14 @@ void AChildHoodCharacter::DoLook(float Yaw, float Pitch)
|
||||
// 交互系统方法
|
||||
// ============================================================================
|
||||
|
||||
UInteractionManager* AChildHoodCharacter::GetInteractionManager() const
|
||||
UInteractionPromptComponent* AChildHoodCharacter::GetPromptComponent() const
|
||||
{
|
||||
return InteractionManager;
|
||||
return PromptComponent;
|
||||
}
|
||||
|
||||
UCollectionHUDComponent* AChildHoodCharacter::GetCollectionHUDComponent() const
|
||||
{
|
||||
return CollectionHUDComponent;
|
||||
}
|
||||
|
||||
void AChildHoodCharacter::SetInteracting(bool bValue)
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// ChildHood Project - 玩家角色(v1.1 Effect/Action 架构适配)
|
||||
// 变更:移除 UInteractionManager,改用 UInteractionPromptComponent
|
||||
// 交互操作通过 ASystemDriver 访问 FInteractionManager + FEffectManager
|
||||
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
@@ -10,7 +14,9 @@
|
||||
class USpringArmComponent;
|
||||
class UCameraComponent;
|
||||
class UInputAction;
|
||||
class UInteractionManager;
|
||||
class UInteractionPromptComponent;
|
||||
class UCollectionHUDComponent;
|
||||
class ASystemDriver;
|
||||
struct FInputActionValue;
|
||||
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogTemplateCharacter, Log, All);
|
||||
@@ -18,6 +24,10 @@ DECLARE_LOG_CATEGORY_EXTERN(LogTemplateCharacter, Log, All);
|
||||
/**
|
||||
* A simple player-controllable third person character
|
||||
* Implements a controllable orbiting camera
|
||||
*
|
||||
* v1.1 架构变更:
|
||||
* - 交互操作通过 ASystemDriver 全局单例访问 FInteractionManager / FEffectManager
|
||||
* - 身上挂载 UInteractionPromptComponent 负责 Free 状态下的 Prompt UI 追踪
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class AChildHoodCharacter : public ACharacter
|
||||
@@ -58,21 +68,31 @@ protected:
|
||||
UPROPERTY(EditAnywhere, Category="Input")
|
||||
UInputAction* SwitchInteractAction;
|
||||
|
||||
/** 交互管理器组件 */
|
||||
/** 交互提示 UI 组件 — Free 状态下追踪选中交互物并显示 Prompt */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Interaction", meta = (AllowPrivateAccess = "true"))
|
||||
UInteractionManager* InteractionManager;
|
||||
UInteractionPromptComponent* PromptComponent;
|
||||
|
||||
/** 收集系统 HUD 组件 — 表现层,只读 CollectionManager 缓存驱动收集 UI */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Collection", meta = (AllowPrivateAccess = "true"))
|
||||
UCollectionHUDComponent* CollectionHUDComponent;
|
||||
|
||||
/** Whether the character is currently in an interaction */
|
||||
bool bIsInteracting = false;
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
public:
|
||||
|
||||
/** Constructor */
|
||||
AChildHoodCharacter();
|
||||
|
||||
/** 获取交互管理器组件 */
|
||||
/** 获取交互提示组件 */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Interaction")
|
||||
UInteractionManager* GetInteractionManager() const;
|
||||
UInteractionPromptComponent* GetPromptComponent() const;
|
||||
|
||||
/** 获取收集系统 HUD 组件 */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Collection")
|
||||
UCollectionHUDComponent* GetCollectionHUDComponent() const;
|
||||
|
||||
/** Set interaction state — called by interaction system */
|
||||
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||
@@ -91,21 +111,19 @@ protected:
|
||||
/** Initialize input action bindings */
|
||||
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
|
||||
|
||||
protected:
|
||||
|
||||
/** Called for movement input */
|
||||
void Move(const FInputActionValue& Value);
|
||||
|
||||
/** Called for looking input */
|
||||
void Look(const FInputActionValue& Value);
|
||||
|
||||
/** Called for interact input (E) — delegates to InteractionManager */
|
||||
/** Called for interact input (E) — delegates to ASystemDriver */
|
||||
void OnInteract(const FInputActionValue& Value);
|
||||
|
||||
/** Called for cancel input (ESC) — delegates to InteractionManager */
|
||||
/** Called for cancel input (ESC) — delegates to ASystemDriver */
|
||||
void OnCancel(const FInputActionValue& Value);
|
||||
|
||||
/** Called for switch interact target input (Tab/Wheel) — delegates to InteractionManager */
|
||||
/** Called for switch interact target input (Tab/Wheel) — delegates to ASystemDriver */
|
||||
void OnSwitchInteract(const FInputActionValue& Value);
|
||||
|
||||
public:
|
||||
|
||||
@@ -1,30 +1,5 @@
|
||||
// ChildHood Project - 跨关卡持久数据持有者 实现
|
||||
// ChildHood Project - GameInstance(v1.1 清理版)
|
||||
// 持久数据已迁移至 UPersistenceSubsystem(UGameInstanceSubsystem)
|
||||
|
||||
#include "ChildHoodGameInstance.h"
|
||||
|
||||
bool UChildHoodGameInstance::IsItemCollected(FName ItemId) const
|
||||
{
|
||||
return CollectedItemIDs.Contains(ItemId);
|
||||
}
|
||||
|
||||
void UChildHoodGameInstance::MarkItemCollected(FName ItemId)
|
||||
{
|
||||
CollectedItemIDs.Add(ItemId);
|
||||
}
|
||||
|
||||
bool UChildHoodGameInstance::IsDoorOpen(FName DoorId) const
|
||||
{
|
||||
const bool* Found = DoorStates.Find(DoorId);
|
||||
return Found ? *Found : false;
|
||||
}
|
||||
|
||||
void UChildHoodGameInstance::SetDoorOpen(FName DoorId, bool bIsOpen)
|
||||
{
|
||||
DoorStates.Add(DoorId, bIsOpen);
|
||||
}
|
||||
|
||||
void UChildHoodGameInstance::ResetAll()
|
||||
{
|
||||
CollectedItemIDs.Empty();
|
||||
DoorStates.Empty();
|
||||
}
|
||||
// v0.1: 空壳实现,持久数据由 UPersistenceSubsystem 管理
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ChildHood Project - 跨关卡持久数据持有者
|
||||
// 对应技术方案: docs/technical/交互系统-技术方案.md 第12节(跨场景持久化设计)
|
||||
// ChildHood Project - GameInstance(v1.1 清理版)
|
||||
// 持久数据已迁移至 UPersistenceSubsystem(UGameInstanceSubsystem)
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -10,11 +10,8 @@
|
||||
/**
|
||||
* UChildHoodGameInstance
|
||||
*
|
||||
* 跨关卡持久数据持有者。UE5 的 OpenLevel() 会销毁整个旧 World(包括
|
||||
* Character 及其所有 UActorComponent),因此需要将持久数据与运行时行为分离。
|
||||
*
|
||||
* v0.1 为单场景原型,此 GameInstance 以空数据初始化。
|
||||
* 后续多场景版本只需在 Project Settings 中将 GameInstance Class 设为此类即可。
|
||||
* v0.1: 空壳,持久化数据由 UPersistenceSubsystem 管理。
|
||||
* 后续可添加:全局配置、存档管理、音频设置等 GameInstance 级功能。
|
||||
*/
|
||||
UCLASS()
|
||||
class UChildHoodGameInstance : public UGameInstance
|
||||
@@ -22,35 +19,6 @@ class UChildHoodGameInstance : public UGameInstance
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
// === 物品消耗状态 ===
|
||||
|
||||
/** 查询指定物品是否已被收集(跨场景持久) */
|
||||
bool IsItemCollected(FName ItemId) const;
|
||||
|
||||
/** 标记指定物品为已收集(写入持久层) */
|
||||
void MarkItemCollected(FName ItemId);
|
||||
|
||||
// === 门状态 ===
|
||||
|
||||
/** 查询指定门的开关状态(跨场景持久),未记录的门默认为关闭 */
|
||||
bool IsDoorOpen(FName DoorId) const;
|
||||
|
||||
/** 设置指定门的开关状态(写入持久层) */
|
||||
void SetDoorOpen(FName DoorId, bool bIsOpen);
|
||||
|
||||
// === 重置 ===
|
||||
|
||||
/** 重置所有持久数据(新游戏时调用) */
|
||||
void ResetAll();
|
||||
|
||||
private:
|
||||
|
||||
/** 已收集物品ID集合 */
|
||||
UPROPERTY()
|
||||
TSet<FName> CollectedItemIDs;
|
||||
|
||||
/** 门开关状态映射(true=打开,false=关闭) */
|
||||
UPROPERTY()
|
||||
TMap<FName, bool> DoorStates;
|
||||
// v0.1: 空壳,持久化数据由 UPersistenceSubsystem 管理
|
||||
// 后续可添加:全局配置、存档管理、音频设置等 GameInstance 级功能
|
||||
};
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "ChildHoodGameMode.h"
|
||||
#include "SystemDriver.h"
|
||||
|
||||
AChildHoodGameMode::AChildHoodGameMode()
|
||||
{
|
||||
// stub
|
||||
}
|
||||
|
||||
void AChildHoodGameMode::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// 自动 Spawn ASystemDriver(全局交互系统驱动器)
|
||||
// 如果关卡中已手动放置则跳过,避免重复
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
if (!ASystemDriver::Get(this))
|
||||
{
|
||||
FActorSpawnParameters SpawnParams;
|
||||
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||
World->SpawnActor<ASystemDriver>(ASystemDriver::StaticClass(), FTransform::Identity, SpawnParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,9 @@ class AChildHoodGameMode : public AGameModeBase
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
|
||||
/** Constructor */
|
||||
AChildHoodGameMode();
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// ChildHood Project - 收集完成提示 UI 实现
|
||||
// 三段时序:淡入 → 停留 → 淡出 → 移除(自管理,HUDComponent 按需创建后无需追踪)
|
||||
|
||||
#include "Collection/CollectionCompleteWidget.h"
|
||||
#include "Components/TextBlock.h"
|
||||
#include "Engine/World.h"
|
||||
#include "TimerManager.h"
|
||||
|
||||
void UCollectionCompleteWidget::NativeOnInitialized()
|
||||
{
|
||||
Super::NativeOnInitialized();
|
||||
}
|
||||
|
||||
void UCollectionCompleteWidget::NativeDestruct()
|
||||
{
|
||||
// 清理计时器:StayTimer/FadeOutTimer 的 lambda 捕获裸 this,Widget 销毁后 fire 会野指针
|
||||
// (与 CollectionHUDComponent::EndPlay 同类卫生写法,防关卡切换/退 PIE 时的悬挂 timer)
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
World->GetTimerManager().ClearTimer(StayTimerHandle);
|
||||
World->GetTimerManager().ClearTimer(FadeOutTimerHandle);
|
||||
}
|
||||
|
||||
Super::NativeDestruct();
|
||||
}
|
||||
|
||||
void UCollectionCompleteWidget::ShowComplete(const FString& Text, float DisplayDuration, float InFadeOutDuration)
|
||||
{
|
||||
CachedFadeOutDuration = InFadeOutDuration;
|
||||
|
||||
if (CompleteTextBlock)
|
||||
{
|
||||
CompleteTextBlock->SetText(FText::FromString(Text));
|
||||
}
|
||||
|
||||
// 淡入
|
||||
if (FadeInAnim)
|
||||
{
|
||||
PlayAnimation(FadeInAnim);
|
||||
}
|
||||
|
||||
// 停留时长后触发淡出
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
World->GetTimerManager().SetTimer(StayTimerHandle, [this]()
|
||||
{
|
||||
StartFadeOut();
|
||||
}, DisplayDuration, false);
|
||||
}
|
||||
}
|
||||
|
||||
void UCollectionCompleteWidget::StartFadeOut()
|
||||
{
|
||||
if (FadeOutAnim)
|
||||
{
|
||||
PlayAnimation(FadeOutAnim);
|
||||
}
|
||||
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
World->GetTimerManager().SetTimer(FadeOutTimerHandle, [this]()
|
||||
{
|
||||
OnFadeOutComplete();
|
||||
}, CachedFadeOutDuration, false);
|
||||
}
|
||||
}
|
||||
|
||||
void UCollectionCompleteWidget::OnFadeOutComplete()
|
||||
{
|
||||
RemoveFromParent();
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// ChildHood Project - 收集完成提示 UI 基类
|
||||
// 对应技术方案: docs/technical/收集系统-技术方案.md 第3节(UCollectionCompleteWidget)+ 第7节
|
||||
// 职责:全部收集完成时的屏幕中央提示(淡入→停留→淡出→移除)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "CollectionCompleteWidget.generated.h"
|
||||
|
||||
class UTextBlock;
|
||||
|
||||
/**
|
||||
* UCollectionCompleteWidget — 全部收集完成中心提示(执行案第四节「全部收集完成表现」)。
|
||||
*
|
||||
* 屏幕正中央弹出「杂物全部收集完成!」白色大字,显示 2.0s 后淡出移除。
|
||||
* 三段时序:淡入 → 停留(DisplayDuration) → 淡出 → RemoveFromParent。
|
||||
*
|
||||
* 绑定要求(蓝图 WBP_CollectionComplete 继承本类):
|
||||
* - TextBlock 名 "CompleteTextBlock" — 完成文字
|
||||
* - WidgetAnimation 名 "FadeInAnim" — 淡入动画
|
||||
* - WidgetAnimation 名 "FadeOutAnim" — 淡出动画
|
||||
*
|
||||
* 布局要求:Anchors Center,白色大字,屏幕正中。
|
||||
*/
|
||||
UCLASS(Abstract, Blueprintable)
|
||||
class UCollectionCompleteWidget : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
/** 显示完成提示:设置文字,启动淡入→停留→淡出时序 */
|
||||
UFUNCTION(BlueprintCallable, Category = "Collection|Complete")
|
||||
void ShowComplete(const FString& Text, float DisplayDuration, float InFadeOutDuration);
|
||||
|
||||
protected:
|
||||
|
||||
/** 完成文字 TextBlock,蓝图子类必须有同名 TextBlock */
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
UTextBlock* CompleteTextBlock;
|
||||
|
||||
/** 淡入动画,蓝图子类必须有同名 WidgetAnimation */
|
||||
UPROPERTY(meta = (BindWidgetAnim), Transient)
|
||||
UWidgetAnimation* FadeInAnim;
|
||||
|
||||
/** 淡出动画,蓝图子类必须有同名 WidgetAnimation */
|
||||
UPROPERTY(meta = (BindWidgetAnim), Transient)
|
||||
UWidgetAnimation* FadeOutAnim;
|
||||
|
||||
/** 停留计时器(淡入结束后启动,到时触发淡出)*/
|
||||
FTimerHandle StayTimerHandle;
|
||||
|
||||
/** 淡出计时器(淡出开始后启动,到时移除 Widget)*/
|
||||
FTimerHandle FadeOutTimerHandle;
|
||||
|
||||
/** 淡出时长(缓存)*/
|
||||
float CachedFadeOutDuration = 0.5f;
|
||||
|
||||
virtual void NativeOnInitialized() override;
|
||||
|
||||
/** Widget 销毁时清理计时器:lambda 捕获裸 this,销毁后 fire 会野指针 */
|
||||
virtual void NativeDestruct() override;
|
||||
|
||||
private:
|
||||
|
||||
/** 停留结束 → 播放淡出动画 */
|
||||
void StartFadeOut();
|
||||
|
||||
/** 淡出时长到 → RemoveFromParent */
|
||||
void OnFadeOutComplete();
|
||||
};
|
||||
@@ -0,0 +1,265 @@
|
||||
// ChildHood Project - 收集系统表现层 实现
|
||||
// 对应技术方案: docs/technical/收集系统-技术方案.md 第5节(数据结构)+ 第6节(行为规则实现)+ 第9节
|
||||
|
||||
#include "Collection/CollectionHUDComponent.h"
|
||||
#include "SystemDriver.h"
|
||||
#include "Collection/CollectionManager.h"
|
||||
#include "Collection/CollectionNotifWidget.h"
|
||||
#include "Collection/CollectionProgressWidget.h"
|
||||
#include "Collection/CollectionCompleteWidget.h"
|
||||
#include "Engine/World.h"
|
||||
#include "TimerManager.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "ChildHood.h"
|
||||
|
||||
// ============================================================================
|
||||
// 构造 / 生命周期
|
||||
// ============================================================================
|
||||
|
||||
UCollectionHUDComponent::UCollectionHUDComponent()
|
||||
{
|
||||
// 需要每帧 Tick 读取 Manager 缓存(开销极低:几个 int32 比较)
|
||||
PrimaryComponentTick.bCanEveryTick = true;
|
||||
PrimaryComponentTick.bStartWithTickEnabled = true;
|
||||
}
|
||||
|
||||
void UCollectionHUDComponent::Initialize(ASystemDriver* InDriver)
|
||||
{
|
||||
DriverPtr = InDriver;
|
||||
}
|
||||
|
||||
void UCollectionHUDComponent::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
}
|
||||
|
||||
void UCollectionHUDComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||
{
|
||||
// 清理 timer:NotifDelayTimer 的 lambda 与 CompleteTimer 的成员函数绑定均捕获裸 this,
|
||||
// 组件销毁后 fire 会野指针。显式 ClearTimer 防御(UE5 Widget/Timer 卫生写法,与项目其他 Character 一致)。
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
World->GetTimerManager().ClearTimer(NotifDelayTimerHandle);
|
||||
World->GetTimerManager().ClearTimer(CompleteTimerHandle);
|
||||
}
|
||||
|
||||
// 移除挂载的通知 Widget
|
||||
for (UCollectionNotifWidget* Notif : ActiveNotifications)
|
||||
{
|
||||
if (Notif)
|
||||
{
|
||||
Notif->RemoveFromParent();
|
||||
}
|
||||
}
|
||||
ActiveNotifications.Empty();
|
||||
|
||||
// 移除常驻进度 Widget
|
||||
if (ProgressWidget)
|
||||
{
|
||||
ProgressWidget->RemoveFromParent();
|
||||
ProgressWidget = nullptr;
|
||||
}
|
||||
|
||||
Super::EndPlay(EndPlayReason);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tick — 只读 Manager 缓存,检测变化驱动 UI
|
||||
// ============================================================================
|
||||
|
||||
void UCollectionHUDComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
|
||||
{
|
||||
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||
|
||||
// 获取 Driver(优先缓存,失效则全局查找并更新缓存)
|
||||
ASystemDriver* Driver = DriverPtr.Get();
|
||||
if (!Driver)
|
||||
{
|
||||
Driver = ASystemDriver::Get(GetWorld());
|
||||
if (Driver)
|
||||
{
|
||||
DriverPtr = Driver;
|
||||
}
|
||||
}
|
||||
if (!Driver) return;
|
||||
|
||||
// 懒创建常驻进度 Widget(首次有效 Tick 时,确保 World 就绪 + 规避 Driver BeginPlay 时序竞争)
|
||||
if (!ProgressWidget && ProgressWidgetClass)
|
||||
{
|
||||
CreateProgressWidget(Driver);
|
||||
}
|
||||
if (!ProgressWidget) return;
|
||||
|
||||
const FCollectionManager& CollMgr = Driver->GetCollectionManager();
|
||||
const FCollectionCachedInfo& Cache = CollMgr.GetCachedInfo();
|
||||
|
||||
// === 检测进度变化(步骤4:弹收集通知)===
|
||||
// LastSeenProgress < 0 表示首次 Tick,由 CreateProgressWidget 初始化为当前进度,不触发通知
|
||||
if (Cache.Progress != LastSeenProgress && LastSeenProgress >= 0)
|
||||
{
|
||||
// 更新进度 UI(同步)+ 弹性动画
|
||||
UpdateProgress(Cache.Progress, Cache.TotalCount);
|
||||
|
||||
// 弹收集通知(延迟 NotifDelay 后创建,避免与拾取通知视觉重叠 — 执行案第二节)
|
||||
const float NotifDelay = CollMgr.GetConfig().NotifDelay;
|
||||
const FString NotifText = Cache.PendingNotificationText;
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
World->GetTimerManager().SetTimer(NotifDelayTimerHandle, [this, NotifText]()
|
||||
{
|
||||
ShowNotification(NotifText);
|
||||
}, NotifDelay, false);
|
||||
}
|
||||
}
|
||||
LastSeenProgress = Cache.Progress;
|
||||
|
||||
// === 检测完成(步骤5:完成判定 → 完成表现)===
|
||||
if (Cache.bJustCompleted && !bCompletionShown)
|
||||
{
|
||||
TriggerCompletionSequence(Driver);
|
||||
bCompletionShown = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 进度 Widget
|
||||
// ============================================================================
|
||||
|
||||
void UCollectionHUDComponent::CreateProgressWidget(ASystemDriver* Driver)
|
||||
{
|
||||
UWorld* World = GetWorld();
|
||||
if (!World) return;
|
||||
|
||||
APlayerController* PC = World->GetFirstPlayerController();
|
||||
if (!PC) return;
|
||||
|
||||
ProgressWidget = CreateWidget<UCollectionProgressWidget>(PC, ProgressWidgetClass);
|
||||
if (!ProgressWidget) return;
|
||||
|
||||
// 常驻显示(ZOrder 5,低于通知/完成提示)
|
||||
ProgressWidget->AddToViewport(5);
|
||||
|
||||
// 初始显示已恢复的进度(执行案第三节:游戏开始后始终可见)
|
||||
const FCollectionCachedInfo& Cache = Driver->GetCollectionManager().GetCachedInfo();
|
||||
ProgressWidget->UpdateProgress(Cache.Progress, Cache.TotalCount);
|
||||
if (Cache.bIsComplete)
|
||||
{
|
||||
ProgressWidget->SetCompleteStyle();
|
||||
}
|
||||
|
||||
// 记录初始进度,避免首次 Tick 误触发通知
|
||||
LastSeenProgress = Cache.Progress;
|
||||
|
||||
UE_LOG(LogChildHood, Log, TEXT("CollectionHUDComponent: 进度 Widget 创建完成,初始进度 %d/%d"),
|
||||
Cache.Progress, Cache.TotalCount);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 收集通知
|
||||
// ============================================================================
|
||||
|
||||
void UCollectionHUDComponent::ShowNotification(const FString& Text)
|
||||
{
|
||||
ASystemDriver* Driver = DriverPtr.Get();
|
||||
if (!Driver) return;
|
||||
|
||||
const FCollectionConfig& Config = Driver->GetCollectionManager().GetConfig();
|
||||
|
||||
// 同屏最大数量限制(执行案第二节)
|
||||
if (ActiveNotifications.Num() >= Config.MaxSimultaneousNotifs)
|
||||
{
|
||||
UE_LOG(LogChildHood, Verbose, TEXT("CollectionHUDComponent: 通知已达上限 %d,跳过"), Config.MaxSimultaneousNotifs);
|
||||
return;
|
||||
}
|
||||
|
||||
UWorld* World = GetWorld();
|
||||
if (!NotifWidgetClass || !World) return;
|
||||
|
||||
APlayerController* PC = World->GetFirstPlayerController();
|
||||
if (!PC) return;
|
||||
|
||||
UCollectionNotifWidget* NotifWidget = CreateWidget<UCollectionNotifWidget>(PC, NotifWidgetClass);
|
||||
if (!NotifWidget) return;
|
||||
|
||||
// 绑定完成回调(淡出移除时清理 ActiveNotifications)
|
||||
NotifWidget->OnFinished.BindUFunction(this, FName("OnNotificationRemoved"));
|
||||
|
||||
NotifWidget->InitializeNotif(Text, Config.NotifTextColor,
|
||||
Config.NotifFadeInDuration, Config.NotifStayDuration, Config.NotifFadeOutDuration);
|
||||
NotifWidget->AddToViewport(10);
|
||||
|
||||
ActiveNotifications.Add(NotifWidget);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 进度更新
|
||||
// ============================================================================
|
||||
|
||||
void UCollectionHUDComponent::UpdateProgress(int32 Current, int32 Total)
|
||||
{
|
||||
if (ProgressWidget)
|
||||
{
|
||||
ProgressWidget->UpdateProgress(Current, Total);
|
||||
// 数字弹性动画(执行案第三节)
|
||||
ProgressWidget->PlayBounceAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 完成序列
|
||||
// ============================================================================
|
||||
|
||||
void UCollectionHUDComponent::TriggerCompletionSequence(ASystemDriver* Driver)
|
||||
{
|
||||
if (!ProgressWidget) return;
|
||||
|
||||
const FCollectionConfig& Config = Driver->GetCollectionManager().GetConfig();
|
||||
|
||||
// 1. 进度 UI 变金色 + 完成弹性动画(执行案第四节)
|
||||
ProgressWidget->SetCompleteStyle();
|
||||
ProgressWidget->PlayCompleteAnimation();
|
||||
|
||||
// 2. 延迟弹出完成提示(等最后一条通知消失 + CompletePromptDelay)
|
||||
const float TotalDelay = Config.NotifDelay + Config.NotifStayDuration
|
||||
+ Config.NotifFadeOutDuration + Config.CompletePromptDelay;
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
World->GetTimerManager().SetTimer(CompleteTimerHandle, this,
|
||||
&UCollectionHUDComponent::ShowCompletionPrompt, TotalDelay, false);
|
||||
}
|
||||
|
||||
UE_LOG(LogChildHood, Log, TEXT("CollectionHUDComponent: 触发完成序列,%.2fs 后弹出完成提示"), TotalDelay);
|
||||
}
|
||||
|
||||
void UCollectionHUDComponent::ShowCompletionPrompt()
|
||||
{
|
||||
UWorld* World = GetWorld();
|
||||
if (!CompleteWidgetClass || !World) return;
|
||||
|
||||
APlayerController* PC = World->GetFirstPlayerController();
|
||||
if (!PC) return;
|
||||
|
||||
UCollectionCompleteWidget* CompleteWidget = CreateWidget<UCollectionCompleteWidget>(PC, CompleteWidgetClass);
|
||||
if (!CompleteWidget) return;
|
||||
|
||||
// 完成提示参数(执行案第四节:白色大字,显示 2.0s 后淡出)
|
||||
ASystemDriver* Driver = DriverPtr.Get();
|
||||
const float CompleteDuration = Driver ? Driver->GetCollectionManager().GetConfig().CompletePromptDuration : 2.0f;
|
||||
const float FadeOutDuration = 0.5f;
|
||||
|
||||
CompleteWidget->ShowComplete(TEXT("杂物全部收集完成!"), CompleteDuration, FadeOutDuration);
|
||||
CompleteWidget->AddToViewport(20);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 通知清理回调
|
||||
// ============================================================================
|
||||
|
||||
void UCollectionHUDComponent::OnNotificationRemoved(UCollectionNotifWidget* Widget)
|
||||
{
|
||||
if (Widget)
|
||||
{
|
||||
ActiveNotifications.RemoveSingle(Widget);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// ChildHood Project - 收集系统表现层组件
|
||||
// 对应技术方案: docs/technical/收集系统-技术方案.md 第3节(UCollectionHUDComponent)+ 第5节 + 第6节
|
||||
// 职责:只读 FCollectionManager 缓存,驱动收集通知/进度/完成 UI
|
||||
// 架构:UActorComponent(挂在 AChildHoodCharacter 上),自有 TickComponent,
|
||||
// 只读 Manager 缓存,绝不访问 InteractableComponent 或 UPersistenceSubsystem
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "CollectionHUDComponent.generated.h"
|
||||
|
||||
class ASystemDriver;
|
||||
class UCollectionNotifWidget;
|
||||
class UCollectionProgressWidget;
|
||||
class UCollectionCompleteWidget;
|
||||
|
||||
/**
|
||||
* UCollectionHUDComponent — 收集系统表现层(执行案第二/三/四节 UI)。
|
||||
*
|
||||
* 数据流(技术方案第9节 Presentation 流):
|
||||
* TickComponent 每帧
|
||||
* → Driver->GetCollectionManager().GetCachedInfo()
|
||||
* → Cache.Progress != LastSeenProgress? → UpdateProgress + SetTimer(NotifDelay) → ShowNotification
|
||||
* → Cache.bJustCompleted && !bCompletionShown? → TriggerCompletionSequence
|
||||
*
|
||||
* 不持有 Manager 所有权,不写 Manager 状态,不碰 PersistenceSubsystem(技术方案注意事项3)。
|
||||
*/
|
||||
UCLASS(ClassGroup = (Collection), meta = (BlueprintSpawnableComponent))
|
||||
class UCollectionHUDComponent : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
UCollectionHUDComponent();
|
||||
|
||||
/** 初始化(Character BeginPlay 调用,存 Driver 弱引用;可为空,Tick 兜底查找)*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Collection|HUD")
|
||||
void Initialize(ASystemDriver* InDriver);
|
||||
|
||||
protected:
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
||||
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
|
||||
|
||||
// === UI 类引用(蓝图/实例设置:创建 WBP 后在此指定)===
|
||||
|
||||
/** 收集通知 Widget 蓝图类(WBP_CollectionNotification)*/
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Collection|UI")
|
||||
TSubclassOf<UCollectionNotifWidget> NotifWidgetClass;
|
||||
|
||||
/** 进度 HUD Widget 蓝图类(WBP_CollectionProgress)*/
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Collection|UI")
|
||||
TSubclassOf<UCollectionProgressWidget> ProgressWidgetClass;
|
||||
|
||||
/** 完成提示 Widget 蓝图类(WBP_CollectionComplete)*/
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Collection|UI")
|
||||
TSubclassOf<UCollectionCompleteWidget> CompleteWidgetClass;
|
||||
|
||||
private:
|
||||
|
||||
// === Driver 弱引用(Tick 时优先用,失效则全局查找)===
|
||||
TWeakObjectPtr<ASystemDriver> DriverPtr;
|
||||
|
||||
// === 变化检测(Presentation 层状态)===
|
||||
int32 LastSeenProgress = -1; // 上次看到的进度,-1 = 未初始化
|
||||
bool bCompletionShown = false; // 完成表现是否已显示
|
||||
|
||||
// === UI 实例 ===
|
||||
UPROPERTY()
|
||||
TArray<TObjectPtr<UCollectionNotifWidget>> ActiveNotifications;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UCollectionProgressWidget> ProgressWidget = nullptr;
|
||||
|
||||
// === 定时器 ===
|
||||
FTimerHandle NotifDelayTimerHandle; // 通知延迟(0.3s)
|
||||
FTimerHandle CompleteTimerHandle; // 完成提示延迟
|
||||
|
||||
// === 内部方法 ===
|
||||
|
||||
/** 创建常驻进度 Widget(懒创建:首次有效 Tick 时,避免与 Driver BeginPlay 时序竞争)*/
|
||||
void CreateProgressWidget(ASystemDriver* Driver);
|
||||
|
||||
/** 弹出收集通知(检查同屏最大数量限制)*/
|
||||
void ShowNotification(const FString& Text);
|
||||
|
||||
/** 更新进度 UI + 弹性动画 */
|
||||
void UpdateProgress(int32 Current, int32 Total);
|
||||
|
||||
/** 触发完成序列:进度金色 + 延迟完成提示 */
|
||||
void TriggerCompletionSequence(ASystemDriver* Driver);
|
||||
|
||||
/** 显示完成提示(延迟后调用)*/
|
||||
void ShowCompletionPrompt();
|
||||
|
||||
/** 通知 Widget 淡出移除回调 — 从 ActiveNotifications 清理 */
|
||||
UFUNCTION()
|
||||
void OnNotificationRemoved(UCollectionNotifWidget* Widget);
|
||||
};
|
||||
@@ -0,0 +1,140 @@
|
||||
// ChildHood Project - 收集系统逻辑核心 实现
|
||||
// 对应技术方案: docs/technical/收集系统-技术方案.md 第5节(数据结构)+ 第6节(行为规则实现)+ 第9节(数据流)
|
||||
|
||||
#include "Collection/CollectionManager.h"
|
||||
#include "Interaction/PersistenceSubsystem.h"
|
||||
#include "ChildHood.h"
|
||||
|
||||
// ============================================================================
|
||||
// 初始化
|
||||
// ============================================================================
|
||||
|
||||
void FCollectionManager::Initialize()
|
||||
{
|
||||
// 重置本地运行时状态
|
||||
Progress = 0;
|
||||
CollectedIDs.Empty();
|
||||
bIsComplete = false;
|
||||
|
||||
// 初始化缓存(稳定态,无瞬时通知)
|
||||
CachedInfo.Progress = 0;
|
||||
CachedInfo.TotalCount = Config.TotalCount;
|
||||
CachedInfo.bIsComplete = false;
|
||||
CachedInfo.ResetTransient();
|
||||
}
|
||||
|
||||
void FCollectionManager::RestoreFromPersistence(UPersistenceSubsystem* Subsystem)
|
||||
{
|
||||
// 缓存持久层弱引用,供后续 SaveStateToPersistence 使用
|
||||
PersistencePtr = Subsystem;
|
||||
|
||||
if (!Subsystem)
|
||||
{
|
||||
UE_LOG(LogChildHood, Warning, TEXT("CollectionManager: RestoreFromPersistence - 持久层为空,保持初始状态"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 从持久层恢复
|
||||
TSet<FName> SavedIDs;
|
||||
int32 SavedProgress = 0;
|
||||
bool bSavedComplete = false;
|
||||
Subsystem->GetCollectionState(SavedIDs, SavedProgress, bSavedComplete);
|
||||
|
||||
CollectedIDs = MoveTemp(SavedIDs);
|
||||
Progress = SavedProgress;
|
||||
bIsComplete = bSavedComplete;
|
||||
|
||||
// 初始化缓存(恢复后的稳定态,无瞬时通知 — 不触发 UI 动画)
|
||||
CachedInfo.Progress = Progress;
|
||||
CachedInfo.TotalCount = Config.TotalCount;
|
||||
CachedInfo.bIsComplete = bIsComplete;
|
||||
CachedInfo.ResetTransient();
|
||||
|
||||
UE_LOG(LogChildHood, Log, TEXT("CollectionManager: 从持久层恢复 - 进度 %d/%d, 完成 %s"),
|
||||
Progress, Config.TotalCount, bIsComplete ? TEXT("是") : TEXT("否"));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 物品事件入口
|
||||
// ============================================================================
|
||||
|
||||
void FCollectionManager::OnItemObtained(FName ItemId, const FString& DisplayName, const FName& Category)
|
||||
{
|
||||
// 步骤1: 查品类 — 非追踪品类忽略(执行案第一节步骤1)
|
||||
if (Category != Config.TrackedCategory)
|
||||
{
|
||||
UE_LOG(LogChildHood, Verbose, TEXT("CollectionManager: 忽略非追踪品类物品 '%s' (Category=%s)"),
|
||||
*DisplayName, *Category.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
ProcessNewCollection(ItemId, DisplayName);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 内部:处理新收集(步骤2-5)
|
||||
// ============================================================================
|
||||
|
||||
void FCollectionManager::ProcessNewCollection(FName ItemId, const FString& DisplayName)
|
||||
{
|
||||
// 步骤2: 查重复 — 已收集则流程结束,不增加 progress,不弹收集通知
|
||||
if (CollectedIDs.Contains(ItemId))
|
||||
{
|
||||
UE_LOG(LogChildHood, Verbose, TEXT("CollectionManager: 重复物品 '%s',忽略"), *DisplayName);
|
||||
return;
|
||||
}
|
||||
|
||||
// 步骤3: 记录 — 加入集合,Progress 取 Set 大小作为权威值
|
||||
CollectedIDs.Add(ItemId);
|
||||
Progress = CollectedIDs.Num();
|
||||
SaveStateToPersistence();
|
||||
|
||||
// 步骤4: 刷新 Presentation 缓存(供 HUDComponent Tick 检测)
|
||||
// LastCollectedItemId / bJustCompleted 由本方法控制;RefreshCachedInfo 只刷新进度+通知文本
|
||||
CachedInfo.LastCollectedItemId = ItemId;
|
||||
CachedInfo.bJustCompleted = false; // 默认非完成,步骤5判定时置 true
|
||||
RefreshCachedInfo(DisplayName);
|
||||
|
||||
UE_LOG(LogChildHood, Log, TEXT("CollectionManager: 新收集 '%s',进度 %d/%d"),
|
||||
*DisplayName, Progress, Config.TotalCount);
|
||||
|
||||
// 步骤5: 完成判定 — progress 达到 TotalCount 则 isComplete = true
|
||||
if (Progress >= Config.TotalCount && !bIsComplete)
|
||||
{
|
||||
bIsComplete = true;
|
||||
CachedInfo.bJustCompleted = true; // 触发 HUDComponent 完成表现
|
||||
CachedInfo.bIsComplete = true;
|
||||
SaveStateToPersistence();
|
||||
UE_LOG(LogChildHood, Log, TEXT("CollectionManager: 全部收集完成!"));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 内部:刷新 Presentation 缓存
|
||||
// ============================================================================
|
||||
|
||||
void FCollectionManager::RefreshCachedInfo(const FString& DisplayName)
|
||||
{
|
||||
// 更新进度相关字段
|
||||
CachedInfo.Progress = Progress;
|
||||
CachedInfo.TotalCount = Config.TotalCount;
|
||||
CachedInfo.bIsComplete = bIsComplete;
|
||||
|
||||
// 生成收集通知文本(执行案第二节):「发现了新的杂物,进度 X/Y」
|
||||
CachedInfo.PendingNotificationText = FString::Printf(
|
||||
TEXT("发现了新的杂物,进度 %d/%d"), Progress, Config.TotalCount);
|
||||
|
||||
// 注意:LastCollectedItemId / bJustCompleted 由 ProcessNewCollection 控制,此处不碰
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 内部:持久化
|
||||
// ============================================================================
|
||||
|
||||
void FCollectionManager::SaveStateToPersistence()
|
||||
{
|
||||
if (UPersistenceSubsystem* Subsystem = PersistencePtr.Get())
|
||||
{
|
||||
Subsystem->SetCollectionState(CollectedIDs, Progress, bIsComplete);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// ChildHood Project - 收集系统逻辑核心(v1.0 架构解耦)
|
||||
// 对应技术方案: docs/technical/收集系统-技术方案.md 第3节(类设计)+ 第5节(数据结构)+ 第6节(行为规则实现)
|
||||
// 职责:品类检查、去重判定、记录持久化、完成判定、缓存 Presentation 数据
|
||||
// 架构:普通 C++ 类(非 UObject、非 UActorComponent),由 ASystemDriver 值持有。
|
||||
// 纯事件驱动无 Tick,不持有任何 UObject 强引用(与 FInteractionManager/FEffectManager 模式一致)。
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Collection/CollectionTypes.h"
|
||||
|
||||
class UPersistenceSubsystem;
|
||||
|
||||
/**
|
||||
* FCollectionManager
|
||||
*
|
||||
* 收集系统逻辑核心。追踪单一品类「杂物」的收集进度(执行案 v0.1 范围)。
|
||||
*
|
||||
* 数据流(技术方案第9节):
|
||||
* FEffectManager AddToBackpack Action
|
||||
* → ASystemDriver 回调 OnItemAdded(ItemId, DisplayName, Category)
|
||||
* → OnItemObtained()
|
||||
* ├─ 步骤1: 查品类 (Category == Config.TrackedCategory?)
|
||||
* ├─ 步骤2: 查重复 (CollectedIDs.Contains?)
|
||||
* ├─ 步骤3: 记录 (CollectedIDs.Add + Progress = Num()) + SaveStateToPersistence()
|
||||
* ├─ 步骤4: 刷新 Presentation 缓存(RefreshCachedInfo)
|
||||
* └─ 步骤5: 完成判定 (Progress >= TotalCount) → bJustCompleted
|
||||
*
|
||||
* Presentation 层(UCollectionHUDComponent)每帧 Tick 只读 GetCachedInfo(),
|
||||
* 通过比较 LastSeenProgress 检测变化驱动 UI,绝不访问 Manager 内部状态或 Subsystem。
|
||||
*/
|
||||
class FCollectionManager
|
||||
{
|
||||
public:
|
||||
|
||||
// === 初始化 ===
|
||||
|
||||
/** 重置本地运行时状态(由 ASystemDriver::BeginPlay 调用,先于 RestoreFromPersistence) */
|
||||
void Initialize();
|
||||
|
||||
/** 从持久层恢复收集状态,并缓存 PersistenceSubsystem 弱引用(供后续写入用)。
|
||||
* v0.1 单场景:持久层为空数据,等效于初始化为 0(技术方案第12节)。 */
|
||||
void RestoreFromPersistence(UPersistenceSubsystem* Subsystem);
|
||||
|
||||
// === 物品事件入口(由 ASystemDriver 回调触发)===
|
||||
|
||||
/** 物品获得事件 — 执行查品类→查重复→记录→完成判定(技术方案步骤1-5) */
|
||||
void OnItemObtained(FName ItemId, const FString& DisplayName, const FName& Category);
|
||||
|
||||
// === Presentation 层访问(只读)===
|
||||
|
||||
/** 获取 Presentation 缓存(HUDComponent 每帧只读) */
|
||||
const FCollectionCachedInfo& GetCachedInfo() const { return CachedInfo; }
|
||||
|
||||
/** 获取配置参数(HUDComponent 读取通知/动画参数) */
|
||||
const FCollectionConfig& GetConfig() const { return Config; }
|
||||
|
||||
private:
|
||||
|
||||
// === 配置 ===
|
||||
FCollectionConfig Config;
|
||||
|
||||
// === 运行时状态 ===
|
||||
int32 Progress = 0; // 当前已收集数(= CollectedIDs.Num(),权威值)
|
||||
TSet<FName> CollectedIDs; // 已收集物品 ID 集合(仅追踪品类)
|
||||
bool bIsComplete = false; // 是否全部收集完成
|
||||
|
||||
// === Presentation 缓存(供 HUDComponent 只读)===
|
||||
FCollectionCachedInfo CachedInfo;
|
||||
|
||||
// === 持久层弱引用(RestoreFromPersistence 时设置,SaveStateToPersistence 用)===
|
||||
TWeakObjectPtr<UPersistenceSubsystem> PersistencePtr;
|
||||
|
||||
// === 内部方法 ===
|
||||
|
||||
/** 处理新收集:记录 + 持久化 + 刷新缓存 + 完成判定(步骤2-5) */
|
||||
void ProcessNewCollection(FName ItemId, const FString& DisplayName);
|
||||
|
||||
/** 刷新 Presentation 缓存(更新进度、生成通知文本)— 保持技术方案签名(DisplayName) */
|
||||
void RefreshCachedInfo(const FString& DisplayName);
|
||||
|
||||
/** 将当前状态写入持久层(记录后 + 完成判定后调用) */
|
||||
void SaveStateToPersistence();
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
// ChildHood Project - 收集通知 UI 实现
|
||||
// 三段时序:淡入 → 停留 → 淡出 → 广播 OnFinished + 移除
|
||||
|
||||
#include "Collection/CollectionNotifWidget.h"
|
||||
#include "Components/TextBlock.h"
|
||||
#include "Engine/World.h"
|
||||
#include "TimerManager.h"
|
||||
|
||||
void UCollectionNotifWidget::NativeOnInitialized()
|
||||
{
|
||||
Super::NativeOnInitialized();
|
||||
}
|
||||
|
||||
void UCollectionNotifWidget::NativeDestruct()
|
||||
{
|
||||
// 清理计时器:StayTimer/FadeOutTimer 的 lambda 捕获裸 this,Widget 销毁后 fire 会野指针
|
||||
// (与 CollectionHUDComponent::EndPlay 同类卫生写法,防关卡切换/退 PIE 时的悬挂 timer)
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
World->GetTimerManager().ClearTimer(StayTimerHandle);
|
||||
World->GetTimerManager().ClearTimer(FadeOutTimerHandle);
|
||||
}
|
||||
|
||||
Super::NativeDestruct();
|
||||
}
|
||||
|
||||
void UCollectionNotifWidget::InitializeNotif(const FString& Text, FLinearColor TextColor,
|
||||
float InFadeInDuration, float InStayDuration, float InFadeOutDuration)
|
||||
{
|
||||
CachedFadeOutDuration = InFadeOutDuration;
|
||||
|
||||
// 设置文字 + 颜色
|
||||
if (NotifTextBlock)
|
||||
{
|
||||
NotifTextBlock->SetText(FText::FromString(Text));
|
||||
NotifTextBlock->SetColorAndOpacity(TextColor);
|
||||
}
|
||||
|
||||
// 淡入动画
|
||||
if (FadeInAnim)
|
||||
{
|
||||
PlayAnimation(FadeInAnim);
|
||||
}
|
||||
|
||||
// 停留时长后触发淡出
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
World->GetTimerManager().SetTimer(StayTimerHandle, [this]()
|
||||
{
|
||||
StartFadeOut();
|
||||
}, InStayDuration, false);
|
||||
}
|
||||
}
|
||||
|
||||
void UCollectionNotifWidget::StartFadeOut()
|
||||
{
|
||||
// 淡出动画
|
||||
if (FadeOutAnim)
|
||||
{
|
||||
PlayAnimation(FadeOutAnim);
|
||||
}
|
||||
|
||||
// 淡出时长后移除(用计时器而非动画完成委托,避免委托绑定复杂度)
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
World->GetTimerManager().SetTimer(FadeOutTimerHandle, [this]()
|
||||
{
|
||||
OnFadeOutComplete();
|
||||
}, CachedFadeOutDuration, false);
|
||||
}
|
||||
}
|
||||
|
||||
void UCollectionNotifWidget::OnFadeOutComplete()
|
||||
{
|
||||
// 通知 HUDComponent 从 ActiveNotifications 移除本 Widget
|
||||
OnFinished.ExecuteIfBound(this);
|
||||
|
||||
// 从视口移除自己
|
||||
RemoveFromParent();
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// ChildHood Project - 收集通知 UI 基类
|
||||
// 对应技术方案: docs/technical/收集系统-技术方案.md 第3节(UCollectionNotifWidget)+ 第7节(美术资源)
|
||||
// 职责:单条收集通知的三段生命周期(淡入→停留→淡出→移除)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "CollectionNotifWidget.generated.h"
|
||||
|
||||
class UTextBlock;
|
||||
class UCollectionNotifWidget;
|
||||
|
||||
/** 收集通知单条完成事件 — 淡出移除后广播,HUDComponent 据此从 ActiveNotifications 清理 */
|
||||
DECLARE_DELEGATE_OneParam(FOnCollectionNotifFinished, UCollectionNotifWidget*);
|
||||
|
||||
/**
|
||||
* UCollectionNotifWidget — 收集通知 UI 单条实例(执行案第二节「收集通知 UI」)。
|
||||
*
|
||||
* 三段生命周期:淡入(0.2s) → 停留(2.0s) → 淡出(0.3s) → 广播 OnFinished + RemoveFromParent。
|
||||
* 文字浅金色 #F0D060(与拾取通知白色区分)。
|
||||
*
|
||||
* 绑定要求(蓝图 WBP_CollectionNotification 继承本类):
|
||||
* - TextBlock 名 "NotifTextBlock" — 通知文字
|
||||
* - WidgetAnimation 名 "FadeInAnim" — 淡入动画
|
||||
* - WidgetAnimation 名 "FadeOutAnim" — 淡出动画
|
||||
*/
|
||||
UCLASS(Abstract, Blueprintable)
|
||||
class UCollectionNotifWidget : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
/** 初始化通知:设置文字/颜色,启动淡入→停留→淡出三段时序 */
|
||||
UFUNCTION(BlueprintCallable, Category = "Collection|Notification")
|
||||
void InitializeNotif(const FString& Text, FLinearColor TextColor,
|
||||
float InFadeInDuration, float InStayDuration, float InFadeOutDuration);
|
||||
|
||||
/** 通知完成事件(淡出移除后广播)— HUDComponent 绑定后清理 ActiveNotifications */
|
||||
FOnCollectionNotifFinished OnFinished;
|
||||
|
||||
protected:
|
||||
|
||||
/** 通知文字 TextBlock,蓝图子类必须有同名 TextBlock */
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
UTextBlock* NotifTextBlock;
|
||||
|
||||
/** 淡入动画,蓝图子类必须有同名 WidgetAnimation */
|
||||
UPROPERTY(meta = (BindWidgetAnim), Transient)
|
||||
UWidgetAnimation* FadeInAnim;
|
||||
|
||||
/** 淡出动画,蓝图子类必须有同名 WidgetAnimation */
|
||||
UPROPERTY(meta = (BindWidgetAnim), Transient)
|
||||
UWidgetAnimation* FadeOutAnim;
|
||||
|
||||
/** 停留计时器(淡入结束后启动,到时触发淡出)*/
|
||||
FTimerHandle StayTimerHandle;
|
||||
|
||||
/** 淡出计时器(淡出开始后启动,到时移除 Widget)*/
|
||||
FTimerHandle FadeOutTimerHandle;
|
||||
|
||||
/** 淡出时长(缓存,供淡出计时器用)*/
|
||||
float CachedFadeOutDuration = 0.3f;
|
||||
|
||||
virtual void NativeOnInitialized() override;
|
||||
|
||||
/** Widget 销毁时清理计时器:lambda 捕获裸 this,销毁后 fire 会野指针 */
|
||||
virtual void NativeDestruct() override;
|
||||
|
||||
private:
|
||||
|
||||
/** 停留结束 → 播放淡出动画 */
|
||||
void StartFadeOut();
|
||||
|
||||
/** 淡出时长到 → 广播 OnFinished + RemoveFromParent */
|
||||
void OnFadeOutComplete();
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
// ChildHood Project - 收集进度 UI 实现
|
||||
|
||||
#include "Collection/CollectionProgressWidget.h"
|
||||
#include "Components/TextBlock.h"
|
||||
|
||||
void UCollectionProgressWidget::NativeOnInitialized()
|
||||
{
|
||||
Super::NativeOnInitialized();
|
||||
}
|
||||
|
||||
void UCollectionProgressWidget::UpdateProgress(int32 Current, int32 Total)
|
||||
{
|
||||
if (ProgressTextBlock)
|
||||
{
|
||||
// 格式:「📦 杂物 X/Y」(执行案第三节)。图标 emoji 在文本中,品类名 v0.1 固定「杂物」。
|
||||
ProgressTextBlock->SetText(FText::FromString(
|
||||
FString::Printf(TEXT("📦 杂物 %d/%d"), Current, Total)));
|
||||
}
|
||||
}
|
||||
|
||||
void UCollectionProgressWidget::SetCompleteStyle()
|
||||
{
|
||||
if (ProgressTextBlock)
|
||||
{
|
||||
// 文字变金色(全部收集完成表现)
|
||||
ProgressTextBlock->SetColorAndOpacity(CompleteColor);
|
||||
}
|
||||
}
|
||||
|
||||
void UCollectionProgressWidget::PlayBounceAnimation()
|
||||
{
|
||||
if (BounceAnim)
|
||||
{
|
||||
PlayAnimation(BounceAnim);
|
||||
}
|
||||
}
|
||||
|
||||
void UCollectionProgressWidget::PlayCompleteAnimation()
|
||||
{
|
||||
if (CompleteAnim)
|
||||
{
|
||||
PlayAnimation(CompleteAnim);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// ChildHood Project - 收集进度 UI 基类
|
||||
// 对应技术方案: docs/technical/收集系统-技术方案.md 第3节(UCollectionProgressWidget)+ 第7节
|
||||
// 职责:常驻进度 HUD(图标+品类名+进度文本)+ 数字弹性动画 + 完成金色变色
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "CollectionProgressWidget.generated.h"
|
||||
|
||||
class UTextBlock;
|
||||
|
||||
/**
|
||||
* UCollectionProgressWidget — 收集进度常驻 HUD(执行案第三节「收集进度 UI」)。
|
||||
*
|
||||
* 显示格式:「📦 杂物 X/Y」,游戏开始后常驻左上角。
|
||||
* 进度变化时数字弹性缩放;全部完成时文字变金色 + 完成动画。
|
||||
*
|
||||
* 绑定要求(蓝图 WBP_CollectionProgress 继承本类):
|
||||
* - TextBlock 名 "ProgressTextBlock" — 进度文本(如 "📦 杂物 3/10")
|
||||
* - WidgetAnimation 名 "BounceAnim" — 数字弹性动画(1.2x, 0.2s)
|
||||
* - WidgetAnimation 名 "CompleteAnim" — 完成金色动画(1.4x, 0.5s)
|
||||
*
|
||||
* 布局要求:Anchors Top Left,距左/上各 3% 屏幕边距(执行案第三节)。
|
||||
*/
|
||||
UCLASS(Abstract, Blueprintable)
|
||||
class UCollectionProgressWidget : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
/** 更新进度文本(格式:「📦 杂物 X/Y」)*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Collection|Progress")
|
||||
void UpdateProgress(int32 Current, int32 Total);
|
||||
|
||||
/** 设为完成样式:文字变金色(全部收集完成时)*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Collection|Progress")
|
||||
void SetCompleteStyle();
|
||||
|
||||
/** 播放数字弹性动画(单次新收集时)*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Collection|Progress")
|
||||
void PlayBounceAnimation();
|
||||
|
||||
/** 播放完成动画(全部收集完成时,配合 SetCompleteStyle)*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Collection|Progress")
|
||||
void PlayCompleteAnimation();
|
||||
|
||||
protected:
|
||||
|
||||
/** 进度文本 TextBlock,蓝图子类必须有同名 TextBlock */
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
UTextBlock* ProgressTextBlock;
|
||||
|
||||
/** 数字弹性动画,蓝图子类必须有同名 WidgetAnimation */
|
||||
UPROPERTY(meta = (BindWidgetAnim), Transient)
|
||||
UWidgetAnimation* BounceAnim;
|
||||
|
||||
/** 完成金色动画,蓝图子类必须有同名 WidgetAnimation */
|
||||
UPROPERTY(meta = (BindWidgetAnim), Transient)
|
||||
UWidgetAnimation* CompleteAnim;
|
||||
|
||||
/** 正常文字颜色(白色)*/
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Collection|Progress")
|
||||
FLinearColor NormalColor = FLinearColor::White;
|
||||
|
||||
/** 完成文字颜色(浅金色 #F0D060)*/
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Collection|Progress")
|
||||
FLinearColor CompleteColor = FLinearColor(0.94f, 0.82f, 0.38f, 1.f);
|
||||
|
||||
virtual void NativeOnInitialized() override;
|
||||
};
|
||||
@@ -0,0 +1,162 @@
|
||||
// ChildHood Project - 收集系统数据类型定义(v1.0 架构解耦)
|
||||
// 对应技术方案: docs/technical/收集系统-技术方案.md 第5节(核心数据结构)
|
||||
// 职责:FCollectionConfig(配置,USTRUCT)+ FCollectionCachedInfo(Presentation 缓存,纯 C++)
|
||||
//
|
||||
// 设计说明:
|
||||
// - FCollectionConfig 为 USTRUCT,承载全部可调参数,默认值与执行案「可调参数汇总」一一对应。
|
||||
// v0.1 以代码默认值承载,后续版本可迁入 DataAsset / 配置表(数据驱动原则)。
|
||||
// - FCollectionCachedInfo 为普通 struct(非 USTRUCT):仅在 FCollectionManager(普通 C++)
|
||||
// 与 UCollectionHUDComponent(UActorComponent)之间传递,无需反射。
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "CollectionTypes.generated.h"
|
||||
|
||||
// ============================================================================
|
||||
// FCollectionConfig — 收集系统配置(FCollectionManager 使用)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* FCollectionConfig
|
||||
*
|
||||
* 收集系统全部可调参数。承载追踪品类、收集通知、进度 UI、完成表现四组参数。
|
||||
* 所有 ClampMin/ClampMax 与执行案「可调参数汇总」的可调范围一致。
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct FCollectionConfig
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
// === 追踪品类 ===
|
||||
|
||||
/** 追踪的物品品类名称(v0.1 固定为 "杂物") */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Category")
|
||||
FName TrackedCategory = FName("杂物");
|
||||
|
||||
/** 品类总收集数(v0.1 = 10) */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Category",
|
||||
meta = (ClampMin = "1"))
|
||||
int32 TotalCount = 10;
|
||||
|
||||
// === 收集通知参数 ===
|
||||
|
||||
/** 拾取通知后到收集通知弹出的间隔(秒) */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Notification",
|
||||
meta = (ClampMin = "0.1", ClampMax = "0.5"))
|
||||
float NotifDelay = 0.3f;
|
||||
|
||||
/** 通知淡入时长(秒) */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Notification",
|
||||
meta = (ClampMin = "0.1", ClampMax = "0.3"))
|
||||
float NotifFadeInDuration = 0.2f;
|
||||
|
||||
/** 通知停留时长(秒) */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Notification",
|
||||
meta = (ClampMin = "1.5", ClampMax = "3.0"))
|
||||
float NotifStayDuration = 2.0f;
|
||||
|
||||
/** 通知淡出时长(秒) */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Notification",
|
||||
meta = (ClampMin = "0.2", ClampMax = "0.5"))
|
||||
float NotifFadeOutDuration = 0.3f;
|
||||
|
||||
/** 通知文字颜色(浅金色 #F0D060,与拾取通知白色区分) */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Notification")
|
||||
FLinearColor NotifTextColor = FLinearColor(0.94f, 0.82f, 0.38f, 1.f);
|
||||
|
||||
/** 最大同屏收集通知数 */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Notification",
|
||||
meta = (ClampMin = "1", ClampMax = "3"))
|
||||
int32 MaxSimultaneousNotifs = 2;
|
||||
|
||||
// === 进度 UI 参数 ===
|
||||
|
||||
/** 进度 UI 距左边距(屏幕宽度比例) */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Progress",
|
||||
meta = (ClampMin = "0.02", ClampMax = "0.05"))
|
||||
float ProgressMarginX = 0.03f;
|
||||
|
||||
/** 进度 UI 距上边距(屏幕高度比例) */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Progress",
|
||||
meta = (ClampMin = "0.02", ClampMax = "0.05"))
|
||||
float ProgressMarginY = 0.03f;
|
||||
|
||||
/** 进度数字弹性缩放倍率(单次新收集时) */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Progress",
|
||||
meta = (ClampMin = "1.1", ClampMax = "1.5"))
|
||||
float BounceScale = 1.2f;
|
||||
|
||||
/** 进度数字弹性时长(秒) */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Progress",
|
||||
meta = (ClampMin = "0.1", ClampMax = "0.4"))
|
||||
float BounceDuration = 0.2f;
|
||||
|
||||
// === 完成表现参数 ===
|
||||
|
||||
/** 完成时金色缩放倍率(全部收集完成时) */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Complete",
|
||||
meta = (ClampMin = "1.2", ClampMax = "1.8"))
|
||||
float CompleteScale = 1.4f;
|
||||
|
||||
/** 完成缩放时长(秒) */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Complete",
|
||||
meta = (ClampMin = "0.3", ClampMax = "0.8"))
|
||||
float CompleteScaleDuration = 0.5f;
|
||||
|
||||
/** 收集通知消失后到完成提示弹出的间隔(秒) */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Complete",
|
||||
meta = (ClampMin = "0.3", ClampMax = "1.0"))
|
||||
float CompletePromptDelay = 0.5f;
|
||||
|
||||
/** 完成提示显示时长(秒) */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Complete",
|
||||
meta = (ClampMin = "1.5", ClampMax = "3.0"))
|
||||
float CompletePromptDuration = 2.0f;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// FCollectionCachedInfo — Presentation 层缓存(非 UObject,纯 C++)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* FCollectionCachedInfo
|
||||
*
|
||||
* 收集系统 Presentation 层缓存。与交互系统的 FSelectedInfo 模式一致。
|
||||
*
|
||||
* 写入方:FCollectionManager 在每次 OnItemObtained 成功记录新收集后,由
|
||||
* RefreshCachedInfo() 刷新此结构体。
|
||||
* 读取方:UCollectionHUDComponent 的 TickComponent 每帧只读此缓存,通过比较
|
||||
* LastSeenProgress 检测变化,驱动 UI 更新。
|
||||
*
|
||||
* 表现层绝不直接访问 FCollectionManager 的内部状态或 UPersistenceSubsystem,
|
||||
* 只通过此缓存 + GetConfig() 读取参数。
|
||||
*/
|
||||
struct FCollectionCachedInfo
|
||||
{
|
||||
/** 当前进度(已收集数) */
|
||||
int32 Progress = 0;
|
||||
|
||||
/** 总数 */
|
||||
int32 TotalCount = 10;
|
||||
|
||||
/** 是否已全部完成 */
|
||||
bool bIsComplete = false;
|
||||
|
||||
/** 是否刚刚完成(本次 OnItemObtained 触发了完成判定) */
|
||||
bool bJustCompleted = false;
|
||||
|
||||
/** 待显示的通知文本(如 "发现了新的杂物,进度 3/10") */
|
||||
FString PendingNotificationText;
|
||||
|
||||
/** 本次收集的物品 ID(用于通知回调定位 / 调试) */
|
||||
FName LastCollectedItemId;
|
||||
|
||||
/** 重置瞬时态(每次消费后调用,保留 Progress/TotalCount/bIsComplete) */
|
||||
void ResetTransient()
|
||||
{
|
||||
bJustCompleted = false;
|
||||
PendingNotificationText.Empty();
|
||||
LastCollectedItemId = FName();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
// ChildHood Project - 门 Actor 实现
|
||||
|
||||
#include "Interaction/DoorActor.h"
|
||||
#include "Interaction/DoorInteractableComponent.h"
|
||||
#include "Interaction/InteractableComponent.h"
|
||||
#include "ChildHood.h"
|
||||
|
||||
// ============================================================================
|
||||
// 构造函数
|
||||
// ============================================================================
|
||||
|
||||
ADoorActor::ADoorActor()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
|
||||
// 创建门专用交互组件,赋值给基类指针
|
||||
DoorInteractableComp = CreateDefaultSubobject<UDoorInteractableComponent>(TEXT("DoorInteractableComp"));
|
||||
InteractableComp = DoorInteractableComp;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 交互回调
|
||||
// ============================================================================
|
||||
|
||||
void ADoorActor::OnInteractStarted_Implementation(UInteractableComponent* Comp)
|
||||
{
|
||||
(void)Comp; // 参数保留以匹配基类签名(OnInteractStarted 当前无调用方)
|
||||
|
||||
// v1.1 架构:门的开关业务逻辑由 EffectManager::ExecuteToggleDoor 统一驱动,
|
||||
// 视觉旋转由 UDoorInteractableComponent::ToggleDoor 内部回调 UpdateDoorVisual 完成。
|
||||
// 此处保留空实现,避免与 Effect 路径重复 ToggleDoor 导致状态抵消。
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 视觉更新
|
||||
// ============================================================================
|
||||
|
||||
void ADoorActor::UpdateDoorVisual(bool bIsOpen)
|
||||
{
|
||||
if (!MeshComp) return;
|
||||
|
||||
// 瞬时旋转:开门偏移 OpenYawOffset 度,关门回到 0 度
|
||||
const float TargetYaw = bIsOpen ? OpenYawOffset : 0.f;
|
||||
MeshComp->SetRelativeRotation(FRotator(0.f, TargetYaw, 0.f));
|
||||
|
||||
UE_LOG(LogChildHood, Verbose, TEXT("DoorActor Visual: Yaw=%.0f (%s)"),
|
||||
TargetYaw, bIsOpen ? TEXT("Open") : TEXT("Closed"));
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// ChildHood Project - 门 Actor
|
||||
// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计)
|
||||
// 职责:门的视觉表现(Mesh 旋转)、交互回调分发
|
||||
// 派生自 AInteractableActor,使用 UDoorInteractableComponent 替代基类组件
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Interaction/InteractableActor.h"
|
||||
#include "DoorActor.generated.h"
|
||||
|
||||
class UDoorInteractableComponent;
|
||||
|
||||
/**
|
||||
* ADoorActor
|
||||
*
|
||||
* 门类型的交互物 Actor。在基类基础上:
|
||||
* - 使用 UDoorInteractableComponent 替代 UInteractableComponent
|
||||
* - v1.1:门开关业务由 EffectManager::ExecuteToggleDoor 驱动,OnInteractStarted 为空实现
|
||||
* - UpdateDoorVisual 执行实际的 Mesh Yaw 旋转(开门+90, 关门0)
|
||||
*
|
||||
* v0.1 阶段旋转为瞬时 SetRelativeRotation,不使用动画插值。
|
||||
*/
|
||||
UCLASS()
|
||||
class ADoorActor : public AInteractableActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
ADoorActor();
|
||||
|
||||
// === 组件访问 ===
|
||||
|
||||
/** 获取门交互组件 */
|
||||
UFUNCTION(BlueprintCallable, Category = "Door")
|
||||
UDoorInteractableComponent* GetDoorInteractableComp() const { return DoorInteractableComp; }
|
||||
|
||||
// === 交互回调 ===
|
||||
|
||||
/**
|
||||
* 交互开始回调(v1.1 为空实现)。
|
||||
* 门开关业务逻辑由 EffectManager::ExecuteToggleDoor 统一驱动,
|
||||
* 视觉旋转由 UDoorInteractableComponent::ToggleDoor 内部回调,Actor 不在此重复处理。
|
||||
*/
|
||||
virtual void OnInteractStarted_Implementation(UInteractableComponent* Comp) override;
|
||||
|
||||
// === 视觉更新 ===
|
||||
|
||||
/**
|
||||
* 更新门 Mesh 的视觉旋转。
|
||||
* bIsOpen=true → Yaw +90 度(打开)
|
||||
* bIsOpen=false → Yaw 0 度(关闭)
|
||||
* 由 UDoorInteractableComponent 在 ToggleDoor/RestorePersistentState 中调用。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Door")
|
||||
void UpdateDoorVisual(bool bIsOpen);
|
||||
|
||||
protected:
|
||||
|
||||
/** 门交互组件(替代基类的 InteractableComp,提供门特有状态和行为) */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
|
||||
TObjectPtr<UDoorInteractableComponent> DoorInteractableComp;
|
||||
|
||||
/** 门打开时的 Yaw 偏移(度),可在编辑器中调整 */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Door", meta = (ClampMin = "0", ClampMax = "180"))
|
||||
float OpenYawOffset = 90.f;
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
// ChildHood Project - 门交互组件 实现
|
||||
|
||||
#include "Interaction/DoorInteractableComponent.h"
|
||||
#include "Interaction/DoorActor.h"
|
||||
#include "Interaction/PersistenceSubsystem.h"
|
||||
#include "ChildHood.h"
|
||||
|
||||
#include "Engine/World.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
|
||||
// ============================================================================
|
||||
// 构造函数
|
||||
// ============================================================================
|
||||
|
||||
UDoorInteractableComponent::UDoorInteractableComponent()
|
||||
{
|
||||
// 门不需要 Tick,状态变更由交互事件驱动
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 门操作
|
||||
// ============================================================================
|
||||
|
||||
void UDoorInteractableComponent::ToggleDoor()
|
||||
{
|
||||
bDoorIsOpen = !bDoorIsOpen;
|
||||
|
||||
// 通知 ADoorActor 更新视觉旋转
|
||||
if (AActor* Owner = GetOwner())
|
||||
{
|
||||
if (ADoorActor* DoorActor = Cast<ADoorActor>(Owner))
|
||||
{
|
||||
DoorActor->UpdateDoorVisual(bDoorIsOpen);
|
||||
}
|
||||
}
|
||||
|
||||
// 持久化门状态
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
if (UPersistenceSubsystem* Persistence = World->GetGameInstance()->GetSubsystem<UPersistenceSubsystem>())
|
||||
{
|
||||
Persistence->SetDoorOpen(Data.ItemId, bDoorIsOpen);
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogChildHood, Log, TEXT("Door Toggle: %s → %s"),
|
||||
*Data.ItemId.ToString(),
|
||||
bDoorIsOpen ? TEXT("Open") : TEXT("Closed"));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 提示文本
|
||||
// ============================================================================
|
||||
|
||||
FString UDoorInteractableComponent::GetPromptText_Implementation() const
|
||||
{
|
||||
// 根据门状态选择动词:已打开用 Alt 动词(如"关上"),已关闭用主动词(如"打开")
|
||||
const FString& Verb = bDoorIsOpen ? Data.InteractVerbAlt : Data.InteractVerb;
|
||||
return FString::Printf(TEXT("[E] %s %s"), *Verb, *Data.DisplayName);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 跨场景持久状态恢复
|
||||
// ============================================================================
|
||||
|
||||
void UDoorInteractableComponent::RestorePersistentState(UPersistenceSubsystem* Persistence)
|
||||
{
|
||||
if (!Persistence) return;
|
||||
|
||||
// 1. 先调用基类恢复消耗状态
|
||||
Super::RestorePersistentState(Persistence);
|
||||
|
||||
// 2. 恢复门开关状态
|
||||
bDoorIsOpen = Persistence->IsDoorOpen(Data.ItemId);
|
||||
|
||||
// 3. 通知 DoorActor 更新视觉旋转
|
||||
if (AActor* Owner = GetOwner())
|
||||
{
|
||||
if (ADoorActor* DoorActor = Cast<ADoorActor>(Owner))
|
||||
{
|
||||
DoorActor->UpdateDoorVisual(bDoorIsOpen);
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogChildHood, Log, TEXT("Door RestorePersistentState: %s → %s"),
|
||||
*Data.ItemId.ToString(),
|
||||
bDoorIsOpen ? TEXT("Open") : TEXT("Closed"));
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// ChildHood Project - 门交互组件
|
||||
// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计)
|
||||
// 职责:门的开关状态管理、动态提示文本、跨场景持久状态恢复
|
||||
// 派生自 UInteractableComponent,增加门特有的状态和行为
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Interaction/InteractableComponent.h"
|
||||
#include "DoorInteractableComponent.generated.h"
|
||||
|
||||
class UPersistenceSubsystem;
|
||||
|
||||
/**
|
||||
* UDoorInteractableComponent
|
||||
*
|
||||
* 挂载在 ADoorActor 上的门交互组件。
|
||||
* 在基类(消耗状态、触发检测、Effect 配置)基础上,增加:
|
||||
* - 门开关状态 (bDoorIsOpen)
|
||||
* - 切换门 (ToggleDoor)
|
||||
* - 动态提示文本(根据开关状态选择 InteractVerb 或 InteractVerbAlt)
|
||||
* - 持久状态恢复(门的开关状态)
|
||||
*/
|
||||
UCLASS(ClassGroup=(Interaction), meta=(BlueprintSpawnableComponent))
|
||||
class UDoorInteractableComponent : public UInteractableComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
UDoorInteractableComponent();
|
||||
|
||||
// === 门操作 ===
|
||||
|
||||
/** 切换门的开关状态,并通过 GetOwner() 通知 ADoorActor 更新视觉 */
|
||||
UFUNCTION(BlueprintCallable, Category = "Door")
|
||||
void ToggleDoor();
|
||||
|
||||
/** 查询门当前是否打开 */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Door")
|
||||
bool IsDoorOpen() const { return bDoorIsOpen; }
|
||||
|
||||
// === 基类覆盖 ===
|
||||
|
||||
/**
|
||||
* 根据门开关状态选择提示动词:
|
||||
* 已打开 → InteractVerbAlt(如"关上")
|
||||
* 已关闭 → InteractVerb(如"打开")
|
||||
*/
|
||||
virtual FString GetPromptText_Implementation() const override;
|
||||
|
||||
/**
|
||||
* 恢复跨场景持久状态:
|
||||
* 1. 调用 Super 恢复消耗状态
|
||||
* 2. 从 PersistenceSubsystem 恢复门开关状态
|
||||
* 3. 通知 DoorActor 更新视觉旋转
|
||||
*/
|
||||
virtual void RestorePersistentState(UPersistenceSubsystem* Persistence) override;
|
||||
|
||||
private:
|
||||
|
||||
/** 门开关状态(true=打开,false=关闭) */
|
||||
bool bDoorIsOpen = false;
|
||||
};
|
||||
@@ -0,0 +1,476 @@
|
||||
// ChildHood Project - Effect Timeline 调度器 实现
|
||||
// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节 + 第6节
|
||||
|
||||
#include "Interaction/EffectManager.h"
|
||||
#include "Engine/World.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "GameFramework/Character.h"
|
||||
#include "Interaction/InteractableComponent.h"
|
||||
#include "Interaction/PersistenceSubsystem.h"
|
||||
#include "Interaction/PushCameraActor.h"
|
||||
#include "Components/SphereComponent.h"
|
||||
#include "Interaction/DoorInteractableComponent.h"
|
||||
#include "Interaction/DoorActor.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "Animation/AnimInstance.h"
|
||||
|
||||
// ============================================================================
|
||||
// 公开接口
|
||||
// ============================================================================
|
||||
|
||||
void FEffectManager::SetOnEffectComplete(FOnEffectComplete&& Callback)
|
||||
{
|
||||
OnEffectComplete = MoveTemp(Callback);
|
||||
}
|
||||
|
||||
void FEffectManager::SetOnItemAddedCallback(FOnItemAdded&& Callback)
|
||||
{
|
||||
OnItemAdded = MoveTemp(Callback);
|
||||
}
|
||||
|
||||
void FEffectManager::PlayEffect(const FInteractEffect& Effect, const FEffectContext& Context)
|
||||
{
|
||||
// 如果有正在播放的 Effect,先强制清理
|
||||
if (bIsPlaying)
|
||||
{
|
||||
PerformCleanup();
|
||||
}
|
||||
|
||||
CurrentEffect = Effect;
|
||||
CurrentContext = Context;
|
||||
ElapsedTime = 0.f;
|
||||
NextActionIndex = 0;
|
||||
bIsPlaying = true;
|
||||
bIsWaitingForInput = false;
|
||||
TurnState.Reset();
|
||||
LastNarrativeIndex = -1;
|
||||
CachedPushCamera = nullptr;
|
||||
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: PlayEffect '%s' (Duration=%.2f, Actions=%d)"),
|
||||
*CurrentEffect.EffectName, CurrentEffect.TotalDuration, CurrentEffect.Actions.Num());
|
||||
}
|
||||
|
||||
void FEffectManager::CancelEffect()
|
||||
{
|
||||
if (!bIsPlaying) return;
|
||||
if (!CurrentEffect.bCancellable) return;
|
||||
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: CancelEffect (WaitingForInput=%s)"),
|
||||
bIsWaitingForInput ? TEXT("true") : TEXT("false"));
|
||||
|
||||
if (bIsWaitingForInput)
|
||||
{
|
||||
// 用户输入是期望的恢复信号 -> 走正常恢复
|
||||
ResumeFromWait();
|
||||
return;
|
||||
}
|
||||
|
||||
// 非暂停态强制取消
|
||||
bIsPlaying = false;
|
||||
PerformCleanup();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 每帧更新
|
||||
// ============================================================================
|
||||
|
||||
void FEffectManager::Update(float DeltaTime)
|
||||
{
|
||||
if (!bIsPlaying) return;
|
||||
if (bIsWaitingForInput) return; // 暂停等待用户输入
|
||||
|
||||
// 1. 推进时间
|
||||
ElapsedTime += DeltaTime;
|
||||
|
||||
// 2. 检查是否到达 TotalDuration
|
||||
if (ElapsedTime >= CurrentEffect.TotalDuration)
|
||||
{
|
||||
FinishEffect();
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 触发到时的 Action
|
||||
while (NextActionIndex < CurrentEffect.Actions.Num())
|
||||
{
|
||||
const FInteractAction& Action = CurrentEffect.Actions[NextActionIndex];
|
||||
if (ElapsedTime >= Action.TriggerTime)
|
||||
{
|
||||
ExecuteAction(Action);
|
||||
NextActionIndex++;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 更新 TurnToTarget 插值
|
||||
UpdateTurn(DeltaTime);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Action 分发
|
||||
// ============================================================================
|
||||
|
||||
void FEffectManager::ExecuteAction(const FInteractAction& Action)
|
||||
{
|
||||
switch (Action.Type)
|
||||
{
|
||||
case EInteractActionType::Wait:
|
||||
ExecuteWait(Action);
|
||||
break;
|
||||
case EInteractActionType::TurnToTarget:
|
||||
ExecuteTurnToTarget(Action);
|
||||
break;
|
||||
case EInteractActionType::PlayMontage:
|
||||
ExecutePlayMontage(Action);
|
||||
break;
|
||||
case EInteractActionType::CameraPush:
|
||||
ExecuteCameraPush(Action);
|
||||
break;
|
||||
case EInteractActionType::CameraReturn:
|
||||
ExecuteCameraReturn(Action);
|
||||
break;
|
||||
case EInteractActionType::Consume:
|
||||
ExecuteConsume();
|
||||
break;
|
||||
case EInteractActionType::HideActor:
|
||||
ExecuteHideActor();
|
||||
break;
|
||||
case EInteractActionType::ShowActor:
|
||||
ExecuteShowActor();
|
||||
break;
|
||||
case EInteractActionType::ShowNarrative:
|
||||
ExecuteShowNarrative(Action);
|
||||
break;
|
||||
case EInteractActionType::HideNarrative:
|
||||
ExecuteHideNarrative();
|
||||
break;
|
||||
case EInteractActionType::ShowPickupNotification:
|
||||
ExecuteShowPickupNotification();
|
||||
break;
|
||||
case EInteractActionType::ToggleDoor:
|
||||
ExecuteToggleDoor();
|
||||
break;
|
||||
// AddToBackpack:通过 Driver 回调链通知收集系统(收集系统技术方案第13节)
|
||||
case EInteractActionType::AddToBackpack:
|
||||
if (CurrentContext.TargetComp.IsValid())
|
||||
{
|
||||
const FInteractableData& ItemData = CurrentContext.TargetComp->GetInteractableData();
|
||||
if (OnItemAdded.IsBound())
|
||||
{
|
||||
// 回调连线:EffectManager → ASystemDriver → FCollectionManager::OnItemObtained
|
||||
OnItemAdded.Execute(ItemData.ItemId, ItemData.DisplayName, ItemData.Category);
|
||||
}
|
||||
UE_LOG(LogTemp, Log, TEXT("EffectManager: AddToBackpack - Item: %s (Category: %s)"),
|
||||
*ItemData.DisplayName, *ItemData.Category.ToString());
|
||||
}
|
||||
break;
|
||||
case EInteractActionType::PlayMontageOnTarget:
|
||||
case EInteractActionType::PlayDialogue:
|
||||
case EInteractActionType::PlayStory:
|
||||
case EInteractActionType::BroadcastEvent:
|
||||
UE_LOG(LogTemp, Log, TEXT("EffectManager: Action %d not implemented in v0.1"),
|
||||
static_cast<int32>(Action.Type));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Effect 结束 / 清理 / 恢复
|
||||
// ============================================================================
|
||||
|
||||
void FEffectManager::FinishEffect()
|
||||
{
|
||||
bIsPlaying = false;
|
||||
PerformCleanup();
|
||||
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: FinishEffect '%s'"), *CurrentEffect.EffectName);
|
||||
|
||||
// 广播完成回调
|
||||
if (OnEffectComplete.IsBound())
|
||||
{
|
||||
OnEffectComplete.Execute();
|
||||
}
|
||||
}
|
||||
|
||||
void FEffectManager::ResumeFromWait()
|
||||
{
|
||||
bIsWaitingForInput = false;
|
||||
// Timeline 继续推进,后续 Action 按 TriggerTime 正常触发
|
||||
}
|
||||
|
||||
void FEffectManager::PerformCleanup()
|
||||
{
|
||||
// 1. 文本底框淡出移除(v0.1 日志占位,后续替换为 Widget 操作)
|
||||
// TODO: 如果 NarrativeWidget 显示中,触发淡出动画
|
||||
|
||||
// 2. 相机回位(如果推镜过)
|
||||
if (CachedPushCamera.IsValid())
|
||||
{
|
||||
if (CurrentContext.Instigator.IsValid())
|
||||
{
|
||||
APlayerController* PC = Cast<APlayerController>(
|
||||
CurrentContext.Instigator.Get()->GetController());
|
||||
if (PC)
|
||||
{
|
||||
PC->SetViewTargetWithBlend(CurrentContext.Instigator.Get(), 0.2f);
|
||||
}
|
||||
}
|
||||
|
||||
// 销毁推镜摄像机
|
||||
APushCameraActor* PushCam = CachedPushCamera.Get();
|
||||
CachedPushCamera = nullptr;
|
||||
if (PushCam && PushCam->GetWorld())
|
||||
{
|
||||
PushCam->Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 蒙太奇停止
|
||||
if (CurrentContext.Instigator.IsValid())
|
||||
{
|
||||
ACharacter* Char = CurrentContext.Instigator.Get();
|
||||
if (Char->GetMesh() && Char->GetMesh()->GetAnimInstance())
|
||||
{
|
||||
Char->GetMesh()->GetAnimInstance()->Montage_Stop(0.2f);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. TurnToTarget 未完成 -> 瞬移到目标旋转
|
||||
if (TurnState.bIsActive && CurrentContext.Instigator.IsValid())
|
||||
{
|
||||
CurrentContext.Instigator.Get()->SetActorRotation(TurnState.TargetRotation);
|
||||
TurnState.Reset();
|
||||
}
|
||||
|
||||
// 5. 重置所有状态变量
|
||||
ElapsedTime = 0.f;
|
||||
NextActionIndex = 0;
|
||||
bIsWaitingForInput = false;
|
||||
TurnState.Reset();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 各 Action 实现
|
||||
// ============================================================================
|
||||
|
||||
void FEffectManager::ExecuteWait(const FInteractAction& Action)
|
||||
{
|
||||
// Wait Action 由 Timeline 自然推进处理,此处仅做日志
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: Wait (%.2fs)"), Action.WaitDuration);
|
||||
}
|
||||
|
||||
void FEffectManager::ExecuteTurnToTarget(const FInteractAction& Action)
|
||||
{
|
||||
if (!CurrentContext.Instigator.IsValid() || !CurrentContext.TargetActor.IsValid()) return;
|
||||
|
||||
ACharacter* Char = CurrentContext.Instigator.Get();
|
||||
FVector DirToTarget = CurrentContext.TargetActor->GetActorLocation() - Char->GetActorLocation();
|
||||
DirToTarget.Z = 0;
|
||||
DirToTarget.Normalize();
|
||||
|
||||
TurnState.bIsActive = true;
|
||||
TurnState.StartRotation = Char->GetActorRotation();
|
||||
TurnState.TargetRotation = DirToTarget.Rotation();
|
||||
TurnState.ElapsedTime = 0.f;
|
||||
TurnState.Duration = Action.TurnDuration;
|
||||
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: TurnToTarget (Duration=%.2fs)"), Action.TurnDuration);
|
||||
}
|
||||
|
||||
void FEffectManager::ExecutePlayMontage(const FInteractAction& Action)
|
||||
{
|
||||
if (!CurrentContext.Instigator.IsValid()) return;
|
||||
if (!Action.Montage) return;
|
||||
|
||||
ACharacter* Char = CurrentContext.Instigator.Get();
|
||||
if (Char->GetMesh() && Char->GetMesh()->GetAnimInstance())
|
||||
{
|
||||
Char->GetMesh()->GetAnimInstance()->Montage_Play(Action.Montage);
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: PlayMontage '%s'"),
|
||||
*Action.Montage->GetName());
|
||||
}
|
||||
}
|
||||
|
||||
void FEffectManager::ExecuteCameraPush(const FInteractAction& Action)
|
||||
{
|
||||
if (!CurrentContext.Instigator.IsValid()) return;
|
||||
|
||||
APlayerController* PC = Cast<APlayerController>(
|
||||
CurrentContext.Instigator.Get()->GetController());
|
||||
if (!PC) return;
|
||||
|
||||
// 计算推镜摄像机位置:沿角色到目标方向偏移
|
||||
FVector CharLoc = CurrentContext.Instigator.Get()->GetActorLocation();
|
||||
FVector DirToTarget(0.f, 0.f, 0.f);
|
||||
if (CurrentContext.TargetActor.IsValid())
|
||||
{
|
||||
DirToTarget = (CurrentContext.TargetActor->GetActorLocation() - CharLoc).GetSafeNormal();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 没有目标 Actor 时使用角色朝向
|
||||
DirToTarget = CurrentContext.Instigator.Get()->GetActorForwardVector();
|
||||
}
|
||||
|
||||
FVector PushLoc = CharLoc + DirToTarget * Action.PushDistance + FVector(0.f, 0.f, 80.f);
|
||||
|
||||
FActorSpawnParameters Params;
|
||||
Params.Owner = CurrentContext.Instigator.Get();
|
||||
APushCameraActor* PushCam = CurrentContext.Instigator.Get()->GetWorld()->SpawnActor<APushCameraActor>(
|
||||
PushLoc, DirToTarget.Rotation(), Params);
|
||||
|
||||
if (PushCam)
|
||||
{
|
||||
PC->SetViewTargetWithBlend(PushCam, Action.PushDuration);
|
||||
CachedPushCamera = PushCam;
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: CameraPush (Dist=%.1f, Dur=%.2fs)"),
|
||||
Action.PushDistance, Action.PushDuration);
|
||||
}
|
||||
}
|
||||
|
||||
void FEffectManager::ExecuteCameraReturn(const FInteractAction& Action)
|
||||
{
|
||||
if (!CurrentContext.Instigator.IsValid()) return;
|
||||
|
||||
APlayerController* PC = Cast<APlayerController>(
|
||||
CurrentContext.Instigator.Get()->GetController());
|
||||
if (!PC) return;
|
||||
|
||||
PC->SetViewTargetWithBlend(CurrentContext.Instigator.Get(), Action.ReturnDuration);
|
||||
|
||||
// 相机已回到角色,立即销毁推镜摄像机并清空缓存,
|
||||
// 避免 PerformCleanup 检测到有效缓存后冗余回位 + 重复销毁。
|
||||
if (APushCameraActor* PushCam = CachedPushCamera.Get())
|
||||
{
|
||||
CachedPushCamera = nullptr;
|
||||
if (PushCam->GetWorld())
|
||||
{
|
||||
PushCam->Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: CameraReturn (Dur=%.2fs)"), Action.ReturnDuration);
|
||||
}
|
||||
|
||||
void FEffectManager::ExecuteConsume()
|
||||
{
|
||||
if (!CurrentContext.TargetComp.IsValid()) return;
|
||||
|
||||
CurrentContext.TargetComp->Consume();
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: Consume - %s"),
|
||||
*CurrentContext.TargetComp->GetInteractableData().DisplayName);
|
||||
}
|
||||
|
||||
void FEffectManager::ExecuteHideActor()
|
||||
{
|
||||
if (!CurrentContext.TargetActor.IsValid()) return;
|
||||
|
||||
CurrentContext.TargetActor.Get()->SetActorHiddenInGame(true);
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: HideActor"));
|
||||
}
|
||||
|
||||
void FEffectManager::ExecuteShowActor()
|
||||
{
|
||||
if (!CurrentContext.TargetActor.IsValid()) return;
|
||||
|
||||
CurrentContext.TargetActor.Get()->SetActorHiddenInGame(false);
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: ShowActor"));
|
||||
}
|
||||
|
||||
void FEffectManager::ExecuteShowNarrative(const FInteractAction& Action)
|
||||
{
|
||||
FString SelectedText;
|
||||
if (Action.bRandomSelect && Action.NarrativeTexts.Num() > 1)
|
||||
{
|
||||
SelectedText = PickRandomNarrative(Action.NarrativeTexts);
|
||||
}
|
||||
else if (Action.NarrativeTexts.Num() > 0)
|
||||
{
|
||||
SelectedText = Action.NarrativeTexts[0];
|
||||
}
|
||||
|
||||
// v0.1: 日志输出(后续替换为 Widget 显示)
|
||||
UE_LOG(LogTemp, Log, TEXT("EffectManager: ShowNarrative - %s"), *SelectedText);
|
||||
|
||||
if (Action.bWaitForInput)
|
||||
{
|
||||
bIsWaitingForInput = true;
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: WaitForInput paused"));
|
||||
}
|
||||
}
|
||||
|
||||
void FEffectManager::ExecuteHideNarrative()
|
||||
{
|
||||
// v0.1: 日志占位(后续替换为 Widget 操作)
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: HideNarrative"));
|
||||
}
|
||||
|
||||
void FEffectManager::ExecuteShowPickupNotification()
|
||||
{
|
||||
// v0.1: 日志占位(后续替换为 Widget 显示)
|
||||
if (CurrentContext.TargetComp.IsValid())
|
||||
{
|
||||
UE_LOG(LogTemp, Log, TEXT("EffectManager: ShowPickupNotification - %s"),
|
||||
*CurrentContext.TargetComp->GetInteractableData().DisplayName);
|
||||
}
|
||||
}
|
||||
|
||||
void FEffectManager::ExecuteToggleDoor()
|
||||
{
|
||||
if (!CurrentContext.TargetComp.IsValid()) return;
|
||||
|
||||
if (UDoorInteractableComponent* DoorComp = Cast<UDoorInteractableComponent>(CurrentContext.TargetComp.Get()))
|
||||
{
|
||||
DoorComp->ToggleDoor();
|
||||
UE_LOG(LogTemp, Log, TEXT("EffectManager: ToggleDoor executed"));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TurnToTarget 插值
|
||||
// ============================================================================
|
||||
|
||||
void FEffectManager::UpdateTurn(float DeltaTime)
|
||||
{
|
||||
if (!TurnState.bIsActive) return;
|
||||
|
||||
TurnState.ElapsedTime += DeltaTime;
|
||||
|
||||
if (TurnState.IsComplete())
|
||||
{
|
||||
if (CurrentContext.Instigator.IsValid())
|
||||
{
|
||||
CurrentContext.Instigator.Get()->SetActorRotation(TurnState.TargetRotation);
|
||||
}
|
||||
TurnState.bIsActive = false;
|
||||
return;
|
||||
}
|
||||
|
||||
float Alpha = TurnState.GetAlpha();
|
||||
FRotator NewRot = FMath::Lerp(TurnState.StartRotation, TurnState.TargetRotation, Alpha);
|
||||
if (CurrentContext.Instigator.IsValid())
|
||||
{
|
||||
CurrentContext.Instigator.Get()->SetActorRotation(NewRot);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 辅助方法
|
||||
// ============================================================================
|
||||
|
||||
FString FEffectManager::PickRandomNarrative(const TArray<FString>& Texts)
|
||||
{
|
||||
if (Texts.Num() == 0) return TEXT("");
|
||||
if (Texts.Num() == 1) return Texts[0];
|
||||
|
||||
int32 CandidateIndex;
|
||||
do
|
||||
{
|
||||
CandidateIndex = FMath::RandRange(0, Texts.Num() - 1);
|
||||
} while (CandidateIndex == LastNarrativeIndex && Texts.Num() > 1);
|
||||
|
||||
LastNarrativeIndex = CandidateIndex;
|
||||
return Texts[CandidateIndex];
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// ChildHood Project - Effect Timeline 调度器(v1.1 Effect/Action 架构)
|
||||
// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计)+ 第6节(复杂接口描述)
|
||||
// 职责:Effect Timeline 调度、Action 执行分发、TurnToTarget 插值、WaitForInput 暂停机制
|
||||
// 注意:普通 C++ 类,非 UObject,由 ASystemDriver 持有并 Tick 驱动
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "InteractionTypes.h"
|
||||
|
||||
class APushCameraActor;
|
||||
|
||||
/**
|
||||
* FEffectManager
|
||||
*
|
||||
* Effect Timeline 调度器,管理一次交互 Effect 的完整生命周期:
|
||||
* - 按 TriggerTime 依次触发各 Action
|
||||
* - 维护 TurnToTarget 跨帧插值
|
||||
* - 支持 WaitForInput 暂停/恢复机制
|
||||
* - 支持取消(CancelEffect)
|
||||
* - Effect 结束时通过 OnEffectComplete 回调通知上层
|
||||
*
|
||||
* 生命周期:由 ASystemDriver 在 BeginPlay 中构造,每帧由 ASystemDriver::Tick 调用 Update。
|
||||
*/
|
||||
class FEffectManager
|
||||
{
|
||||
public:
|
||||
|
||||
// === 公开接口 ===
|
||||
|
||||
/** 设置完成回调(由 ASystemDriver 在 BeginPlay 中设置) */
|
||||
void SetOnEffectComplete(FOnEffectComplete&& Callback);
|
||||
|
||||
/** 设置物品获得回调(AddToBackpack Action 执行时触发)。
|
||||
* 由 ASystemDriver 绑定到 FCollectionManager::OnItemObtained(收集系统技术方案第13节)。 */
|
||||
void SetOnItemAddedCallback(FOnItemAdded&& Callback);
|
||||
|
||||
/** 播放 Effect — 初始化状态,开始 Timeline 推进 */
|
||||
void PlayEffect(const FInteractEffect& Effect, const FEffectContext& Context);
|
||||
|
||||
/** 取消 Effect(用户按 E/ESC) — 区分 WaitForInput 恢复 vs 强制取消 */
|
||||
void CancelEffect();
|
||||
|
||||
/** 当前是否正在播放 Effect */
|
||||
bool IsPlaying() const { return bIsPlaying; }
|
||||
|
||||
/** 当前是否暂停等待用户输入 */
|
||||
bool IsWaitingForInput() const { return bIsWaitingForInput; }
|
||||
|
||||
/** 每帧更新(由 ASystemDriver::Tick 调用) */
|
||||
void Update(float DeltaTime);
|
||||
|
||||
private:
|
||||
|
||||
// === Effect 状态 ===
|
||||
|
||||
/** 当前播放中的 Effect 数据 */
|
||||
FInteractEffect CurrentEffect;
|
||||
|
||||
/** 当前 Effect 的运行时上下文 */
|
||||
FEffectContext CurrentContext;
|
||||
|
||||
/** 从 Effect 开始累计的经过时间 */
|
||||
float ElapsedTime = 0.f;
|
||||
|
||||
/** 下一个待触发的 Action 索引(Actions 按 TriggerTime 排序) */
|
||||
int32 NextActionIndex = 0;
|
||||
|
||||
/** 是否正在播放 Effect */
|
||||
bool bIsPlaying = false;
|
||||
|
||||
/** 是否暂停等待用户输入(ShowNarrative bWaitForInput=true 时设置) */
|
||||
bool bIsWaitingForInput = false;
|
||||
|
||||
/** 上一次随机选取的叙事文本索引(用于避免连续重复) */
|
||||
int32 LastNarrativeIndex = -1;
|
||||
|
||||
// === Turn 插值 ===
|
||||
|
||||
/** TurnToTarget 跨帧插值状态 */
|
||||
FTurnState TurnState;
|
||||
|
||||
// === 推镜缓存 ===
|
||||
|
||||
/** 推镜期间 Spawn 的临时摄像机(弱引用,Effect 结束时销毁) */
|
||||
TWeakObjectPtr<APushCameraActor> CachedPushCamera;
|
||||
|
||||
// === 回调 ===
|
||||
|
||||
/** Effect 完成回调(正常结束或取消后清理完毕时触发) */
|
||||
FOnEffectComplete OnEffectComplete;
|
||||
|
||||
/** 物品获得回调(AddToBackpack Action 触发,传 ItemId/DisplayName/Category) */
|
||||
FOnItemAdded OnItemAdded;
|
||||
|
||||
// === 内部方法 ===
|
||||
|
||||
/** 执行单个 Action(按 Type 分发到对应 ExecuteXxx) */
|
||||
void ExecuteAction(const FInteractAction& Action);
|
||||
|
||||
/** Effect 正常结束 — 清理状态 + 广播回调 */
|
||||
void FinishEffect();
|
||||
|
||||
/** 从 WaitForInput 暂停恢复 */
|
||||
void ResumeFromWait();
|
||||
|
||||
/** 清理所有运行时状态(推镜相机、蒙太奇、转向等) */
|
||||
void PerformCleanup();
|
||||
|
||||
/** 从 NarrativeTexts 中随机选取一条(避免与上次重复) */
|
||||
FString PickRandomNarrative(const TArray<FString>& Texts);
|
||||
|
||||
// === 各 Action 处理 ===
|
||||
|
||||
void ExecuteWait(const FInteractAction& Action);
|
||||
void ExecuteTurnToTarget(const FInteractAction& Action);
|
||||
void ExecutePlayMontage(const FInteractAction& Action);
|
||||
void ExecuteCameraPush(const FInteractAction& Action);
|
||||
void ExecuteCameraReturn(const FInteractAction& Action);
|
||||
void ExecuteConsume();
|
||||
void ExecuteHideActor();
|
||||
void ExecuteShowActor();
|
||||
void ExecuteShowNarrative(const FInteractAction& Action);
|
||||
void ExecuteHideNarrative();
|
||||
void ExecuteShowPickupNotification();
|
||||
void ExecuteToggleDoor();
|
||||
|
||||
/** 更新 TurnToTarget 跨帧插值(在 Update 末尾调用) */
|
||||
void UpdateTurn(float DeltaTime);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
// ChildHood Project - 通用交互物 Actor 基类 实现
|
||||
// ChildHood Project - 通用交互物 Actor 基类 实现(v1.1 Effect/Action 架构)
|
||||
|
||||
#include "Interaction/InteractableActor.h"
|
||||
#include "Interaction/InteractableComponent.h"
|
||||
@@ -18,8 +18,7 @@ AInteractableActor::AInteractableActor()
|
||||
RootComponent = MeshComp;
|
||||
MeshComp->SetCollisionProfileName(TEXT("BlockAll"));
|
||||
|
||||
// 创建交互组件(UActorComponent 不需要 SetupAttachment,数据驱动型组件)
|
||||
InteractableComp = CreateDefaultSubobject<UInteractableComponent>(TEXT("InteractableComp"));
|
||||
// InteractableComp 由子类创建(本类为 abstract)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -28,17 +27,7 @@ AInteractableActor::AInteractableActor()
|
||||
|
||||
void AInteractableActor::OnInteractStarted_Implementation(UInteractableComponent* Comp)
|
||||
{
|
||||
if (!Comp || !MeshComp) return;
|
||||
|
||||
const FInteractableData& Data = Comp->GetInteractableData();
|
||||
|
||||
// 门类型:根据开关状态旋转 Mesh
|
||||
if (Data.Type == EInteractType::Operate && Data.OperateSubType == EOperateSubType::Door)
|
||||
{
|
||||
FRotator CurrentRot = MeshComp->GetRelativeRotation();
|
||||
float TargetYaw = Comp->IsDoorOpen() ? 90.0f : 0.0f;
|
||||
MeshComp->SetRelativeRotation(FRotator(CurrentRot.Pitch, TargetYaw, CurrentRot.Roll));
|
||||
}
|
||||
// 默认空实现,子类可覆盖(如 ADoorActor 旋转 Mesh)
|
||||
}
|
||||
|
||||
void AInteractableActor::OnInteractEnded_Implementation(UInteractableComponent* Comp)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// ChildHood Project - 通用交互物 Actor 基类
|
||||
// ChildHood Project - 通用交互物 Actor 基类(v1.1 Effect/Action 架构)
|
||||
// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计)
|
||||
// 职责:持有 Mesh + InteractableComponent,提供可被子类覆盖的交互回调
|
||||
// 职责:持有 Mesh,提供可被子类覆盖的交互回调
|
||||
// 注意:本类为 abstract,子类必须自行创建并赋值 InteractableComp
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -9,21 +10,19 @@
|
||||
#include "InteractableActor.generated.h"
|
||||
|
||||
class UInteractableComponent;
|
||||
class UStaticMeshComponent;
|
||||
|
||||
/**
|
||||
* AInteractableActor
|
||||
*
|
||||
* 通用交互物 Actor 基类。场景中的每个交互物(冰箱、门、NPC 等)
|
||||
* 均为该类的实例或其子类的实例。
|
||||
* 交互物 Actor 抽象基类。提供 Mesh 和交互回调框架。
|
||||
* 子类必须在构造函数中创建合适的 InteractableComponent 并赋值给 InteractableComp。
|
||||
*
|
||||
* 组成:
|
||||
* - UStaticMeshComponent* MeshComp(根组件,承载可视模型)
|
||||
* - UInteractableComponent* InteractableComp(交互逻辑与数据)
|
||||
*
|
||||
* 子类可覆盖 OnInteractStarted / OnInteractEnded 以实现类型特定行为,
|
||||
* 如门开关时的 Mesh 旋转、拾取物的消失效果等。
|
||||
* - UInteractableComponent* InteractableComp(由子类创建)
|
||||
*/
|
||||
UCLASS()
|
||||
UCLASS(abstract)
|
||||
class AInteractableActor : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
@@ -45,15 +44,20 @@ public:
|
||||
// === 可被子类覆盖的交互回调 ===
|
||||
|
||||
/**
|
||||
* 交互开始时调用(门开关切换、状态恢复时也调用)。
|
||||
* 默认实现:门类型根据开关状态对 Mesh 做 Yaw 旋转(开门+90度/关门0度)。
|
||||
* 交互开始时调用。
|
||||
* BlueprintNativeEvent,子类可在蓝图中或 C++ 中覆盖实现。
|
||||
* 默认为空实现。
|
||||
*/
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Interact")
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Interact", meta = (DisplayName = "On Interact Started"))
|
||||
void OnInteractStarted(UInteractableComponent* Comp);
|
||||
virtual void OnInteractStarted_Implementation(UInteractableComponent* Comp);
|
||||
|
||||
/** 交互结束时调用。默认为空实现,子类可覆盖。 */
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Interact")
|
||||
/**
|
||||
* 交互结束时调用。
|
||||
* BlueprintNativeEvent,子类可在蓝图中或 C++ 中覆盖实现。
|
||||
* 默认为空实现。
|
||||
*/
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Interact", meta = (DisplayName = "On Interact Ended"))
|
||||
void OnInteractEnded(UInteractableComponent* Comp);
|
||||
virtual void OnInteractEnded_Implementation(UInteractableComponent* Comp);
|
||||
|
||||
@@ -61,9 +65,12 @@ protected:
|
||||
|
||||
/** 交互物的可视模型 */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
|
||||
UStaticMeshComponent* MeshComp;
|
||||
TObjectPtr<UStaticMeshComponent> MeshComp;
|
||||
|
||||
/** 交互物的交互组件(数据 + 检测 + 状态) */
|
||||
/**
|
||||
* 交互物的交互组件(数据 + 检测 + 状态)。
|
||||
* 由子类在构造函数中创建并赋值。
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
|
||||
UInteractableComponent* InteractableComp;
|
||||
TObjectPtr<UInteractableComponent> InteractableComp;
|
||||
};
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
// ChildHood Project - 可交互物组件 实现
|
||||
// ChildHood Project - 可交互物组件 实现(v1.1 Effect/Action 架构)
|
||||
|
||||
#include "Interaction/InteractableComponent.h"
|
||||
#include "Interaction/InteractableActor.h"
|
||||
#include "Interaction/PersistenceSubsystem.h"
|
||||
#include "SystemDriver.h"
|
||||
#include "Interaction/InteractionManager.h"
|
||||
#include "ChildHoodGameInstance.h"
|
||||
#include "ChildHood.h"
|
||||
|
||||
#include "Components/SphereComponent.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "GameFramework/Character.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
|
||||
// ============================================================================
|
||||
// 构造函数
|
||||
@@ -36,7 +37,7 @@ void UInteractableComponent::BeginPlay()
|
||||
return;
|
||||
}
|
||||
|
||||
// --- 创建检测球 ---
|
||||
// --- 创建检测球(BeginPlay 中动态创建,因为需要 Data.TriggerRadius)---
|
||||
DetectionSphere = NewObject<USphereComponent>(this, USphereComponent::StaticClass(), TEXT("DetectionSphere"));
|
||||
if (DetectionSphere)
|
||||
{
|
||||
@@ -58,14 +59,15 @@ void UInteractableComponent::BeginPlay()
|
||||
}
|
||||
|
||||
// --- 恢复跨场景持久状态 ---
|
||||
if (UChildHoodGameInstance* GI = GetWorld()->GetGameInstance<UChildHoodGameInstance>())
|
||||
UGameInstance* GI = Owner->GetGameInstance();
|
||||
if (UPersistenceSubsystem* Persistence = GI ? GI->GetSubsystem<UPersistenceSubsystem>() : nullptr)
|
||||
{
|
||||
RestorePersistentState(GI);
|
||||
RestorePersistentState(Persistence);
|
||||
}
|
||||
|
||||
UE_LOG(LogChildHood, Log, TEXT("InteractableComponent Init: %s (Type=%s, Radius=%f)"),
|
||||
UE_LOG(LogChildHood, Log, TEXT("InteractableComponent Init: %s (ItemId=%s, Radius=%.0f)"),
|
||||
Data.DisplayName.IsEmpty() ? TEXT("(unnamed)") : *Data.DisplayName,
|
||||
*UEnum::GetValueAsString(Data.Type),
|
||||
*Data.ItemId.ToString(),
|
||||
Data.TriggerRadius);
|
||||
}
|
||||
|
||||
@@ -73,22 +75,16 @@ void UInteractableComponent::BeginPlay()
|
||||
// 提示文本
|
||||
// ============================================================================
|
||||
|
||||
FString UInteractableComponent::GetPromptText() const
|
||||
FString UInteractableComponent::GetPromptText_Implementation() const
|
||||
{
|
||||
FString Verb = Data.InteractVerb;
|
||||
// 门类型:根据当前开关状态选择动词
|
||||
if (Data.Type == EInteractType::Operate && Data.OperateSubType == EOperateSubType::Door)
|
||||
{
|
||||
Verb = bDoorIsOpen ? Data.InteractVerbAlt : Data.InteractVerb;
|
||||
}
|
||||
return FString::Printf(TEXT("[E] %s %s"), *Verb, *Data.DisplayName);
|
||||
return FString::Printf(TEXT("[E] %s %s"), *Data.InteractVerb, *Data.DisplayName);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 消耗管理
|
||||
// ============================================================================
|
||||
|
||||
void UInteractableComponent::Consume()
|
||||
void UInteractableComponent::Consume_Implementation()
|
||||
{
|
||||
bIsConsumed = true;
|
||||
|
||||
@@ -99,29 +95,17 @@ void UInteractableComponent::Consume()
|
||||
}
|
||||
|
||||
// 写入持久层
|
||||
if (UWorld* World = GetWorld())
|
||||
if (UGameInstance* GI = GetOwner() ? GetOwner()->GetGameInstance() : nullptr)
|
||||
{
|
||||
if (UChildHoodGameInstance* GI = World->GetGameInstance<UChildHoodGameInstance>())
|
||||
if (UPersistenceSubsystem* Persistence = GI->GetSubsystem<UPersistenceSubsystem>())
|
||||
{
|
||||
GI->MarkItemCollected(Data.ItemId);
|
||||
Persistence->MarkItemCollected(Data.ItemId);
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogChildHood, Log, TEXT("InteractableComponent 已消耗: %s"), *Data.ItemId.ToString());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 门状态管理
|
||||
// ============================================================================
|
||||
|
||||
void UInteractableComponent::ToggleDoor()
|
||||
{
|
||||
bDoorIsOpen = !bDoorIsOpen;
|
||||
UE_LOG(LogChildHood, Log, TEXT("InteractableComponent 门状态切换: %s → %s"),
|
||||
*Data.ItemId.ToString(),
|
||||
bDoorIsOpen ? TEXT("开") : TEXT("关"));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 位置查询
|
||||
// ============================================================================
|
||||
@@ -136,38 +120,24 @@ FVector UInteractableComponent::GetWorldLocation() const
|
||||
// 跨场景持久状态恢复
|
||||
// ============================================================================
|
||||
|
||||
void UInteractableComponent::RestorePersistentState(UChildHoodGameInstance* GI)
|
||||
void UInteractableComponent::RestorePersistentState(UPersistenceSubsystem* Persistence)
|
||||
{
|
||||
if (!GI) return;
|
||||
if (!Persistence) return;
|
||||
|
||||
// oneShot 物品:已消耗 → 隐藏 + 禁用检测
|
||||
if (Data.bOneShot && GI->IsItemCollected(Data.ItemId))
|
||||
if (Data.bOneShot && Persistence->IsItemCollected(Data.ItemId))
|
||||
{
|
||||
bIsConsumed = true;
|
||||
if (DetectionSphere)
|
||||
{
|
||||
DetectionSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||
}
|
||||
if (AInteractableActor* Owner = Cast<AInteractableActor>(GetOwner()))
|
||||
if (AActor* Owner = GetOwner())
|
||||
{
|
||||
Owner->SetActorHiddenInGame(true);
|
||||
}
|
||||
UE_LOG(LogChildHood, Log, TEXT("RestorePersistentState: 已消耗物品隐藏: %s"), *Data.ItemId.ToString());
|
||||
}
|
||||
|
||||
// 门:恢复开关状态
|
||||
if (Data.Type == EInteractType::Operate && Data.OperateSubType == EOperateSubType::Door)
|
||||
{
|
||||
bDoorIsOpen = GI->IsDoorOpen(Data.ItemId);
|
||||
// 通知 Actor 应用视觉状态(Mesh 旋转)
|
||||
if (AInteractableActor* Owner = Cast<AInteractableActor>(GetOwner()))
|
||||
{
|
||||
Owner->OnInteractStarted(this);
|
||||
}
|
||||
UE_LOG(LogChildHood, Log, TEXT("RestorePersistentState: 门状态恢复: %s → %s"),
|
||||
*Data.ItemId.ToString(),
|
||||
bDoorIsOpen ? TEXT("开") : TEXT("关"));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -193,10 +163,14 @@ void UInteractableComponent::OnDetectionOverlapBegin(UPrimitiveComponent* Overla
|
||||
UE_LOG(LogChildHood, Verbose, TEXT("检测到角色进入范围: %s ← %s"),
|
||||
*Data.DisplayName, *OtherActor->GetName());
|
||||
|
||||
// 获取角色的 InteractionManager 并注册自己
|
||||
if (UInteractionManager* IM = Character->FindComponentByClass<UInteractionManager>())
|
||||
// 通过 ASystemDriver 注册到 InteractionManager
|
||||
if (ASystemDriver* Driver = ASystemDriver::Get(this))
|
||||
{
|
||||
IM->RegisterInteractable(this);
|
||||
Driver->GetInteractionManager().RegisterInteractable(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogChildHood, Warning, TEXT("InteractableComponent: ASystemDriver 不存在,无法注册 %s"), *Data.DisplayName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,9 +187,9 @@ void UInteractableComponent::OnDetectionOverlapEnd(UPrimitiveComponent* Overlapp
|
||||
UE_LOG(LogChildHood, Verbose, TEXT("角色离开范围: %s ← %s"),
|
||||
*Data.DisplayName, *OtherActor->GetName());
|
||||
|
||||
// 获取角色的 InteractionManager 并注销自己
|
||||
if (UInteractionManager* IM = Character->FindComponentByClass<UInteractionManager>())
|
||||
// 通过 ASystemDriver 从 InteractionManager 注销
|
||||
if (ASystemDriver* Driver = ASystemDriver::Get(this))
|
||||
{
|
||||
IM->UnregisterInteractable(this);
|
||||
Driver->GetInteractionManager().UnregisterInteractable(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// ChildHood Project - 可交互物组件
|
||||
// ChildHood Project - 可交互物组件(v1.1 Effect/Action 架构)
|
||||
// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计)+ 第12节(跨场景持久化)
|
||||
// 职责:单个交互物配置数据、触发区域、运行时状态、跨场景持久状态恢复
|
||||
// 核心变更(v0.1 → v1.1):去除所有门逻辑,只保留通用交互物功能
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -9,15 +10,21 @@
|
||||
#include "InteractionTypes.h"
|
||||
#include "InteractableComponent.generated.h"
|
||||
|
||||
class UChildHoodGameInstance;
|
||||
class UPersistenceSubsystem;
|
||||
class USphereComponent;
|
||||
class ASystemDriver;
|
||||
|
||||
/**
|
||||
* UInteractableComponent
|
||||
*
|
||||
* 挂载在 AInteractableActor(或其子类)上的组件。
|
||||
* 持有该交互物的全部配置数据(FInteractableData)、运行时触发检测球、
|
||||
* 消耗/门状态管理,以及跨场景持久状态恢复。
|
||||
* 消耗状态管理,以及跨场景持久状态恢复。
|
||||
*
|
||||
* 碰撞回调通过 ASystemDriver::Get() 获取 Driver,
|
||||
* 再调用 GetInteractionManager().RegisterInteractable(this) 注册。
|
||||
*
|
||||
* 门逻辑已移除,由 UDoorInteractableComponent 派生类实现(Phase 2)。
|
||||
*/
|
||||
UCLASS(ClassGroup=(Interaction), meta=(BlueprintSpawnableComponent))
|
||||
class UInteractableComponent : public UActorComponent
|
||||
@@ -41,29 +48,31 @@ public:
|
||||
|
||||
/**
|
||||
* 获取提示文本,格式:"[E] 动作词 名称"
|
||||
* 门类型根据当前开关状态选择动词
|
||||
* 子类可覆盖以实现动态动词(如门的"打开"/"关上")
|
||||
*/
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Interact", meta = (DisplayName = "Get Prompt Text"))
|
||||
FString GetPromptText() const;
|
||||
virtual FString GetPromptText_Implementation() const;
|
||||
|
||||
/** 该交互物是否已被消耗(oneShot 模式) */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Interact")
|
||||
bool IsConsumed() const { return bIsConsumed; }
|
||||
|
||||
/** 标记为已消耗,禁用检测球,写入持久层 */
|
||||
/** 标记为已消耗,禁用检测球 */
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Interact", meta = (DisplayName = "Consume"))
|
||||
void Consume();
|
||||
|
||||
// === 门状态管理 ===
|
||||
|
||||
/** 切换门开关状态 */
|
||||
void ToggleDoor();
|
||||
|
||||
/** 门是否处于打开状态 */
|
||||
bool IsDoorOpen() const { return bDoorIsOpen; }
|
||||
virtual void Consume_Implementation();
|
||||
|
||||
/** 获取交互物在世界空间的位置 */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Interact")
|
||||
FVector GetWorldLocation() const;
|
||||
|
||||
/** 从 GameInstance 恢复跨场景持久状态(BeginPlay 中调用) */
|
||||
void RestorePersistentState(UChildHoodGameInstance* GI);
|
||||
/**
|
||||
* 从持久层恢复跨场景持久状态(BeginPlay 中调用)。
|
||||
* oneShot 物品已消耗时隐藏 Actor 并禁用检测。
|
||||
* 子类可覆盖以恢复额外状态(如门的开关状态)。
|
||||
*/
|
||||
virtual void RestorePersistentState(UPersistenceSubsystem* Persistence);
|
||||
|
||||
protected:
|
||||
|
||||
@@ -73,17 +82,14 @@ private:
|
||||
|
||||
/** 运行时创建的检测球,用于触发玩家进入/离开交互范围 */
|
||||
UPROPERTY()
|
||||
USphereComponent* DetectionSphere;
|
||||
TObjectPtr<USphereComponent> DetectionSphere;
|
||||
|
||||
/** oneShot 物品是否已被消耗 */
|
||||
bool bIsConsumed = false;
|
||||
|
||||
/** 门是否处于打开状态(仅 Operate+Door 类型使用) */
|
||||
bool bDoorIsOpen = false;
|
||||
|
||||
// === 碰撞回调 ===
|
||||
|
||||
/** 玩家进入检测范围 */
|
||||
/** 玩家进入检测范围 — 通过 ASystemDriver 注册到 InteractionManager */
|
||||
UFUNCTION()
|
||||
void OnDetectionOverlapBegin(UPrimitiveComponent* OverlappedComponent,
|
||||
AActor* OtherActor,
|
||||
@@ -92,7 +98,7 @@ private:
|
||||
bool bFromSweep,
|
||||
const FHitResult& SweepResult);
|
||||
|
||||
/** 玩家离开检测范围 */
|
||||
/** 玩家离开检测范围 — 通过 ASystemDriver 从 InteractionManager 注销 */
|
||||
UFUNCTION()
|
||||
void OnDetectionOverlapEnd(UPrimitiveComponent* OverlappedComponent,
|
||||
AActor* OtherActor,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,225 +1,135 @@
|
||||
// ChildHood Project - 交互管理器核心组件
|
||||
// ChildHood Project - 交互管理器(v1.1 Effect/Action 架构重写)
|
||||
// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计)+ 第6节(行为规则实现)
|
||||
// 职责:交互全流程状态机、检测选择、类型分发、摄像机/转向/UI 控制
|
||||
// 职责:检测列表维护、选中管理、顶层状态(Free/Executing)
|
||||
// 架构:普通 C++ 类(非 UObject、非 UActorComponent),由 ASystemDriver 值持有
|
||||
// 特点:纯事件驱动无 Tick,所有变化由外部调用触发
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "InteractionTypes.h"
|
||||
#include "InteractionManager.generated.h"
|
||||
|
||||
class UInteractableComponent;
|
||||
class APushCameraActor;
|
||||
class AChildHoodCharacter;
|
||||
class UInteractionPromptWidget;
|
||||
class UNarrativeTextWidget;
|
||||
class ACharacter;
|
||||
|
||||
/**
|
||||
* UInteractionManager
|
||||
* FInteractionManager
|
||||
*
|
||||
* 挂载在 AChildHoodCharacter 上的组件,负责交互全流程管理:
|
||||
* - 层级状态机驱动(Free / Interacting + 子阶段)
|
||||
* - 附近交互物检测列表排序与选中管理
|
||||
* - 四种交互类型分发(观察/拾取/操作/对话)
|
||||
* - 角色自动转向插值
|
||||
* - 摄像机推镜/回位控制
|
||||
* - UI 提示与文本显示控制(v0.1 先用日志)
|
||||
* 普通C++类,负责交互检测列表维护和顶层状态管理。
|
||||
* 由 ASystemDriver 值持有,不参与 UE 反射系统。
|
||||
*
|
||||
* 数据流:
|
||||
* InteractableComponent 碰撞回调 → ASystemDriver → RegisterInteractable/UnregisterInteractable
|
||||
* Character 输入回调 → ASystemDriver → BeginInteract/CancelInteract/SwitchTarget
|
||||
* EffectManager 完成回调 → ASystemDriver → OnInteractionFinished
|
||||
*/
|
||||
UCLASS(ClassGroup=(Interaction), meta=(BlueprintSpawnableComponent))
|
||||
class UInteractionManager : public UActorComponent
|
||||
class FInteractionManager
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
UInteractionManager();
|
||||
/** 初始化管理器,绑定玩家角色引用 */
|
||||
void Initialize(ACharacter* PlayerCharacter);
|
||||
|
||||
// === 公开方法 ===
|
||||
// === 交互物注册(由 InteractableComponent 碰撞回调通过 ASystemDriver 调用)===
|
||||
|
||||
/** 发起交互(E键触发),前置检查通过后进入 Enter 阶段 */
|
||||
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||
void BeginInteract();
|
||||
|
||||
/** 取消/结束交互(ESC/E键在Active阶段触发),AnimLock期间不可取消 */
|
||||
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||
void CancelInteract();
|
||||
|
||||
/** 切换选中交互物(Tab/滚轮),Direction: +1下一个 / -1上一个 */
|
||||
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||
void SwitchTarget(int32 Direction);
|
||||
|
||||
/** 注册交互物到检测列表(由 InteractableComponent 的碰撞回调触发) */
|
||||
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||
/**
|
||||
* 注册交互物到检测列表。
|
||||
* 添加后按水平距离排序,重置选中索引为0,刷新缓存。
|
||||
*/
|
||||
void RegisterInteractable(UInteractableComponent* Interactable);
|
||||
|
||||
/** 从检测列表注销交互物(离开范围或消耗时触发) */
|
||||
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||
/**
|
||||
* 从检测列表注销交互物。
|
||||
* Executing 状态下如果移除的是 CurrentInteractable,仅从列表移除,
|
||||
* CurrentInteractable 延迟清理(因为 Effect 可能仍在执行)。
|
||||
*/
|
||||
void UnregisterInteractable(UInteractableComponent* Interactable);
|
||||
|
||||
// === 委托 ===
|
||||
// === 交互触发(Character 输入回调中调用)===
|
||||
|
||||
/** 物品拾取事件 — 通知物品系统(Pickup 类型 ExecuteAnimEvent 中广播) */
|
||||
UPROPERTY(BlueprintAssignable, Category = "Interaction|Events")
|
||||
FMLocalItemEvent OnItemPickedUp;
|
||||
/**
|
||||
* 发起交互(E键触发)。
|
||||
* 前置检查:State==Free、SelectedIndex有效、!IsConsumed()。
|
||||
* 成功后 State=Executing,返回选中的 Component。
|
||||
* 拒绝则返回 nullptr。
|
||||
*/
|
||||
UInteractableComponent* BeginInteract();
|
||||
|
||||
/** 物品获得事件 — 通知收集系统(Search 类型 OnAnimLockComplete 中广播) */
|
||||
UPROPERTY(BlueprintAssignable, Category = "Interaction|Events")
|
||||
FMLocalItemEvent OnItemFound;
|
||||
/**
|
||||
* 取消交互(ESC/E键在Executing阶段触发)。
|
||||
* v0.1 简化:仅设置状态标记,实际取消由 ASystemDriver 协调 EffectManager。
|
||||
*/
|
||||
void CancelInteract();
|
||||
|
||||
// === 可调参数 ===
|
||||
/**
|
||||
* 切换选中目标(Tab/滚轮)。
|
||||
* Direction: +1下一个 / -1上一个,循环。
|
||||
* 仅在 Free 状态下响应。
|
||||
*/
|
||||
void SwitchTarget(int32 Direction);
|
||||
|
||||
/** 角色转向时长(秒) */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction|Tuning")
|
||||
float TurnDuration = 0.2f;
|
||||
// === 状态查询 ===
|
||||
|
||||
/** 提示 UI 淡入时长(秒) */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction|Tuning")
|
||||
float PromptFadeInDuration = 0.2f;
|
||||
/** 获取当前顶层状态 */
|
||||
EInteractionState GetState() const { return State; }
|
||||
|
||||
/** 提示 UI 淡出时长(秒) */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction|Tuning")
|
||||
float PromptFadeOutDuration = 0.2f;
|
||||
/** 当前是否有有效的选中交互物 */
|
||||
bool HasSelection() const { return CachedSelected.bHasSelection; }
|
||||
|
||||
protected:
|
||||
/** 获取选中交互物的提示文本(从缓存读取,不访问 Component) */
|
||||
FString GetSelectedPromptText() const { return CachedSelected.PromptText; }
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
|
||||
/** 获取选中交互物的世界坐标(从缓存读取) */
|
||||
FVector GetSelectedWorldPosition() const { return CachedSelected.WorldPosition; }
|
||||
|
||||
/** 获取完整的选中信息缓存 */
|
||||
const FSelectedInfo& GetCachedSelectedInfo() const { return CachedSelected; }
|
||||
|
||||
// === Effect 完成回调(由 EffectManager 通过 ASystemDriver 调用)===
|
||||
|
||||
/**
|
||||
* Effect 执行完毕回调。
|
||||
* State 回到 Free,清理 CurrentInteractable,通知 Character 解除交互,
|
||||
* 重新排序和刷新缓存(因为消耗可能改变了列表)。
|
||||
*/
|
||||
void OnInteractionFinished();
|
||||
|
||||
// === 当前交互物访问(EffectManager 需要访问)===
|
||||
|
||||
/** 获取当前正在交互的交互物(Executing 状态下有效) */
|
||||
UInteractableComponent* GetCurrentInteractable() const;
|
||||
|
||||
private:
|
||||
|
||||
// === 状态机 ===
|
||||
|
||||
/** 交互系统顶层状态 */
|
||||
EInteractionState State = EInteractionState::Free;
|
||||
|
||||
/** Interacting 子阶段 */
|
||||
EInteractPhase Phase = EInteractPhase::None;
|
||||
|
||||
// === 检测与选择 ===
|
||||
|
||||
/** 当前附近的交互物列表,按水平距离排序 */
|
||||
UPROPERTY()
|
||||
TArray<UInteractableComponent*> NearbyInteractables;
|
||||
TArray<TWeakObjectPtr<UInteractableComponent>> NearbyInteractables;
|
||||
|
||||
/** 当前选中的交互物索引 */
|
||||
/** 当前选中的交互物索引(NearbyInteractables 中的下标) */
|
||||
int32 SelectedIndex = 0;
|
||||
|
||||
/** 当前正在交互的交互物(交互过程中锁定) */
|
||||
UPROPERTY()
|
||||
UInteractableComponent* CurrentInteractable = nullptr;
|
||||
/** 当前正在交互的交互物(Executing 状态下锁定) */
|
||||
TWeakObjectPtr<UInteractableComponent> CurrentInteractable;
|
||||
|
||||
// === 摄像机控制 ===
|
||||
/** 玩家角色弱引用(用于按水平距离排序的选中计算) */
|
||||
TWeakObjectPtr<ACharacter> PlayerCharacter;
|
||||
|
||||
/** 推镜用临时摄像机 Actor(BeginPlay 中 Spawn) */
|
||||
UPROPERTY()
|
||||
TObjectPtr<APushCameraActor> PushCameraActor;
|
||||
|
||||
/** 推镜是否已发起(Enter阶段一次性标志) */
|
||||
bool bPushStarted = false;
|
||||
|
||||
// === 计时器 ===
|
||||
|
||||
/** 转向已用时间 */
|
||||
float TurnElapsed = 0.0f;
|
||||
|
||||
/** 推镜后等待已用时间 */
|
||||
float WaitElapsed = 0.0f;
|
||||
|
||||
/** 动画事件计时器(如60%时物品消失) */
|
||||
FTimerHandle AnimEventTimerHandle;
|
||||
|
||||
/** 动画结束计时器 */
|
||||
FTimerHandle AnimEndTimerHandle;
|
||||
|
||||
/** 退出阶段回位计时器 */
|
||||
FTimerHandle ExitTimerHandle;
|
||||
|
||||
/** 动画事件是否已触发 */
|
||||
bool bAnimEventFired = false;
|
||||
|
||||
// === UI 配置 ===
|
||||
|
||||
/** 提示 UI 蓝图类(需在编辑器中指定为 WBP_InteractionPrompt) */
|
||||
UPROPERTY(EditAnywhere, Category = "Interaction|UI")
|
||||
TSubclassOf<UInteractionPromptWidget> PromptWidgetClass;
|
||||
|
||||
/** 文本底框 UI 蓝图类(需在编辑器中指定为 WBP_NarrativeText) */
|
||||
UPROPERTY(EditAnywhere, Category = "Interaction|UI")
|
||||
TSubclassOf<UNarrativeTextWidget> NarrativeWidgetClass;
|
||||
|
||||
/** 提示 UI 高度偏移(交互物上方的世界空间 Z 偏移,cm) */
|
||||
UPROPERTY(EditAnywhere, Category = "Interaction|UI")
|
||||
float PromptHeightOffset = 80.0f;
|
||||
|
||||
// === UI 实例 ===
|
||||
|
||||
/** 交互提示 Widget 实例 */
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInteractionPromptWidget> PromptWidget;
|
||||
|
||||
/** 文本底框 Widget 实例 */
|
||||
UPROPERTY()
|
||||
TObjectPtr<UNarrativeTextWidget> NarrativeWidget;
|
||||
|
||||
// === 文本选择 ===
|
||||
|
||||
/** 上次显示的叙事文本(用于不重复选取) */
|
||||
FString LastNarrativeText;
|
||||
/** Presentation 层缓存数据 — 选中交互物的摘要信息 */
|
||||
FSelectedInfo CachedSelected;
|
||||
|
||||
// === 内部方法 ===
|
||||
|
||||
/** 更新选中交互物(排序后 SelectedIndex = 0) */
|
||||
/**
|
||||
* 更新选中交互物:按与玩家的水平距离排序,重置 SelectedIndex=0。
|
||||
* 同时清理已失效的弱引用。
|
||||
*/
|
||||
void UpdateSelection();
|
||||
|
||||
/** 更新提示 UI 内容和位置 */
|
||||
void UpdatePromptUI();
|
||||
|
||||
/** 隐藏提示 UI */
|
||||
void HidePrompt();
|
||||
|
||||
/** 更新提示 UI 的屏幕空间位置(跟随交互物世界位置投影) */
|
||||
void UpdatePromptScreenPosition(UInteractableComponent* Interactable);
|
||||
|
||||
/** Enter 阶段 Tick:转向插值 + 推镜 + 等待 */
|
||||
void TickEnterPhase(float DeltaTime);
|
||||
|
||||
/** AnimLock 阶段 Tick(v0.1 由计时器驱动,此处可为空) */
|
||||
void TickAnimLockPhase(float DeltaTime);
|
||||
|
||||
/** Exit 阶段 Tick(v0.1 由定时器驱动回 Free) */
|
||||
void TickExitPhase(float DeltaTime);
|
||||
|
||||
/** Enter 阶段完成后,根据交互类型分发到下一阶段 */
|
||||
void TransitionFromEnter();
|
||||
|
||||
/** 进入 AnimLock 阶段:播放蒙太奇或启动计时器 */
|
||||
void EnterAnimLockPhase();
|
||||
|
||||
/** 动画事件触发点回调(如拾取物消失、门状态切换) */
|
||||
void ExecuteAnimEvent();
|
||||
|
||||
/** 动画结束回调 */
|
||||
void OnAnimLockComplete();
|
||||
|
||||
/** 显示叙事文本(v0.1 用日志输出) */
|
||||
void ShowNarrativeText(const FString& Text);
|
||||
|
||||
/** 隐藏叙事文本(v0.1 用日志输出) */
|
||||
void HideNarrativeText();
|
||||
|
||||
/** 推镜:移动 PushCameraActor 并切换 ViewTarget */
|
||||
void PushCamera(FVector Target, float Duration);
|
||||
|
||||
/** 相机回位:切换回 Character 的 FollowCamera */
|
||||
void ReturnCamera(float Duration);
|
||||
|
||||
/** 计算推镜目标位置 */
|
||||
FVector CalculatePushPosition(UInteractableComponent* Interactable);
|
||||
|
||||
/** 随机选取叙事文本,排除上次 */
|
||||
FString PickRandomNarrative(const TArray<FString>& Texts);
|
||||
|
||||
/** Exit 阶段定时器回调:回到 Free 状态 */
|
||||
void OnExitTimerComplete();
|
||||
/**
|
||||
* 从 Component 读取选中项信息并写入 CachedSelected。
|
||||
* Presentation 层每帧只读 CachedSelected,不直接访问 Component。
|
||||
*/
|
||||
void RefreshCachedSelection();
|
||||
};
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
// ChildHood Project - 交互提示追踪组件 实现
|
||||
// 对应技术方案: docs/technical/交互系统-技术方案.md 第6节(提示UI显示/隐藏)
|
||||
// 职责:Free 状态下追踪 Manager 缓存,更新 Prompt Widget 位置和文本
|
||||
|
||||
#include "Interaction/InteractionPromptComponent.h"
|
||||
#include "Interaction/InteractionPromptWidget.h"
|
||||
#include "SystemDriver.h"
|
||||
#include "Interaction/InteractionManager.h"
|
||||
#include "ChildHood.h"
|
||||
|
||||
#include "Engine/World.h"
|
||||
#include "Blueprint/WidgetLayoutLibrary.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
|
||||
// ============================================================================
|
||||
// 构造函数
|
||||
// ============================================================================
|
||||
|
||||
UInteractionPromptComponent::UInteractionPromptComponent()
|
||||
{
|
||||
// 启用 Tick,每帧更新 Prompt 位置
|
||||
PrimaryComponentTick.bCanEverTick = true;
|
||||
PrimaryComponentTick.bStartWithTickEnabled = true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 初始化
|
||||
// ============================================================================
|
||||
|
||||
void UInteractionPromptComponent::Initialize(ASystemDriver* InDriver)
|
||||
{
|
||||
Driver = InDriver;
|
||||
|
||||
UE_LOG(LogChildHood, Log, TEXT("InteractionPromptComponent: 初始化完成, Driver=%s"),
|
||||
InDriver ? *InDriver->GetName() : TEXT("nullptr"));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 生命周期
|
||||
// ============================================================================
|
||||
|
||||
void UInteractionPromptComponent::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// 根据 PromptWidgetClass 创建 Prompt Widget
|
||||
if (PromptWidgetClass)
|
||||
{
|
||||
if (APlayerController* PC = GetWorld()
|
||||
? GetWorld()->GetFirstPlayerController()
|
||||
: nullptr)
|
||||
{
|
||||
PromptWidget = CreateWidget<UInteractionPromptWidget>(PC, PromptWidgetClass);
|
||||
if (PromptWidget)
|
||||
{
|
||||
PromptWidget->AddToViewport();
|
||||
PromptWidget->SetVisibility(ESlateVisibility::Hidden);
|
||||
UE_LOG(LogChildHood, Log, TEXT("InteractionPromptComponent: Prompt Widget 创建完成"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogChildHood, Warning, TEXT("InteractionPromptComponent: 无法获取 PlayerController,Prompt Widget 未创建"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogChildHood, Warning, TEXT("InteractionPromptComponent: PromptWidgetClass 未设置,Prompt Widget 未创建"));
|
||||
}
|
||||
}
|
||||
|
||||
void UInteractionPromptComponent::TickComponent(float DeltaTime, ELevelTick TickType,
|
||||
FActorComponentTickFunction* ThisTickFunction)
|
||||
{
|
||||
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||
|
||||
// 没有 Widget 则跳过
|
||||
if (!PromptWidget)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取 Driver(弱引用检查)
|
||||
ASystemDriver* DriverPtr = Driver.Get();
|
||||
if (!DriverPtr)
|
||||
{
|
||||
HidePrompt();
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取 InteractionManager 引用
|
||||
FInteractionManager& Manager = DriverPtr->GetInteractionManager();
|
||||
|
||||
// 检查 State == Free 且 HasSelection()
|
||||
if (Manager.GetState() == EInteractionState::Free && Manager.HasSelection())
|
||||
{
|
||||
UpdatePrompt();
|
||||
}
|
||||
else
|
||||
{
|
||||
HidePrompt();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Prompt 显示/隐藏
|
||||
// ============================================================================
|
||||
|
||||
void UInteractionPromptComponent::UpdatePrompt()
|
||||
{
|
||||
ASystemDriver* DriverPtr = Driver.Get();
|
||||
if (!DriverPtr || !PromptWidget)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FInteractionManager& Manager = DriverPtr->GetInteractionManager();
|
||||
|
||||
// 从 Manager 缓存读取数据
|
||||
FString PromptText = Manager.GetSelectedPromptText();
|
||||
FVector WorldPos = Manager.GetSelectedWorldPosition();
|
||||
|
||||
// 应用高度偏移
|
||||
FVector AdjustedPos = WorldPos + FVector(0.f, 0.f, PromptHeightOffset);
|
||||
|
||||
// ProjectWorldToScreen
|
||||
APlayerController* PC = GetWorld()
|
||||
? GetWorld()->GetFirstPlayerController()
|
||||
: nullptr;
|
||||
if (!PC)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FVector2D ScreenPos;
|
||||
bool bOnScreen = PC->ProjectWorldLocationToScreen(AdjustedPos, ScreenPos, true);
|
||||
|
||||
if (!bOnScreen)
|
||||
{
|
||||
HidePrompt();
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新 Widget 位置和文本
|
||||
PromptWidget->SetPromptText(PromptText);
|
||||
PromptWidget->SetScreenPosition(ScreenPos);
|
||||
PromptWidget->Show();
|
||||
|
||||
bWasShowingPrompt = true;
|
||||
}
|
||||
|
||||
void UInteractionPromptComponent::HidePrompt()
|
||||
{
|
||||
if (PromptWidget && bWasShowingPrompt)
|
||||
{
|
||||
PromptWidget->Hide();
|
||||
bWasShowingPrompt = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// ChildHood Project - 交互提示追踪组件
|
||||
// 对应技术方案: docs/technical/交互系统-技术方案.md 第6节(提示UI显示/隐藏)
|
||||
// 职责:独立于 Effect 执行,只负责 Free 状态下的 Prompt UI 追踪
|
||||
// 架构:UActorComponent,自有 Tick,只读 InteractionManager 缓存
|
||||
// 挂载:Character 上,由 Character::BeginPlay 中调用 Initialize
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "InteractionPromptComponent.generated.h"
|
||||
|
||||
class ASystemDriver;
|
||||
class UInteractionPromptWidget;
|
||||
|
||||
/**
|
||||
* UInteractionPromptComponent
|
||||
*
|
||||
* 交互提示追踪组件。每帧检查 InteractionManager 的缓存数据,
|
||||
* 在 Free 状态且有选中交互物时显示 Prompt Widget,Executing 时隐藏。
|
||||
*
|
||||
* 数据流:
|
||||
* InteractionManager 缓存 (CachedSelected) → 此组件 Tick 只读 → Prompt Widget 显示/隐藏
|
||||
*
|
||||
* 不直接访问任何 InteractableComponent,所有数据来自 Manager 缓存。
|
||||
*/
|
||||
UCLASS(ClassGroup=(Interaction), meta=(BlueprintSpawnableComponent))
|
||||
class UInteractionPromptComponent : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
UInteractionPromptComponent();
|
||||
|
||||
/** 初始化(Character::BeginPlay 中调用) */
|
||||
void Initialize(ASystemDriver* InDriver);
|
||||
|
||||
protected:
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
virtual void TickComponent(float DeltaTime, ELevelTick TickType,
|
||||
FActorComponentTickFunction* ThisTickFunction) override;
|
||||
|
||||
private:
|
||||
|
||||
/** 弱引用 Driver */
|
||||
TWeakObjectPtr<ASystemDriver> Driver;
|
||||
|
||||
/** Prompt Widget 实例 */
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInteractionPromptWidget> PromptWidget;
|
||||
|
||||
/** Prompt Widget 蓝图类(编辑器中指定 WBP_InteractionPrompt) */
|
||||
UPROPERTY(EditAnywhere, Category = "Interaction|UI")
|
||||
TSubclassOf<UInteractionPromptWidget> PromptWidgetClass;
|
||||
|
||||
/** 提示 UI 高度偏移(cm) */
|
||||
UPROPERTY(EditAnywhere, Category = "Interaction|UI")
|
||||
float PromptHeightOffset = 80.0f;
|
||||
|
||||
/** 上一次记录的状态(用于检测状态变化) */
|
||||
bool bWasShowingPrompt = false;
|
||||
|
||||
/** 更新 Prompt 显示 */
|
||||
void UpdatePrompt();
|
||||
|
||||
/** 隐藏 Prompt */
|
||||
void HidePrompt();
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
// ChildHood Project - 交互系统数据类型定义
|
||||
// 对应技术方案: docs/technical/交互系统-技术方案.md 第5节(核心数据结构)+ 第12节(跨场景持久化)
|
||||
// ChildHood Project - 交互系统数据类型定义(v1.1 Effect/Action 架构)
|
||||
// 对应技术方案: docs/technical/交互系统-技术方案.md 第5节(核心数据结构)
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -10,169 +10,304 @@
|
||||
// 枚举
|
||||
// ============================================================================
|
||||
|
||||
/** 交互类型 */
|
||||
UENUM(BlueprintType)
|
||||
enum class EInteractType : uint8
|
||||
{
|
||||
Examine UMETA(DisplayName = "观察"),
|
||||
Pickup UMETA(DisplayName = "拾取"),
|
||||
Operate UMETA(DisplayName = "操作"),
|
||||
Talk UMETA(DisplayName = "对话")
|
||||
};
|
||||
|
||||
/** 操作子类型(仅 Operate 类型有效) */
|
||||
UENUM(BlueprintType)
|
||||
enum class EOperateSubType : uint8
|
||||
{
|
||||
Door UMETA(DisplayName = "门(开关切换)"),
|
||||
Search UMETA(DisplayName = "翻找")
|
||||
};
|
||||
|
||||
/** 交互系统顶层状态 */
|
||||
/** 交互系统顶层状态(简化为 2 阶段) */
|
||||
UENUM(BlueprintType)
|
||||
enum class EInteractionState : uint8
|
||||
{
|
||||
Free UMETA(DisplayName = "自由状态"),
|
||||
Interacting UMETA(DisplayName = "交互中"),
|
||||
SubSystem UMETA(DisplayName = "子系统(v0.1预留)"),
|
||||
Cutscene UMETA(DisplayName = "过场(v0.1预留)")
|
||||
Free, // 自由状态,检测/选择/Prompt
|
||||
Executing, // 执行 Effect 中
|
||||
};
|
||||
|
||||
/** Interacting 子阶段 */
|
||||
/** Action 类型 */
|
||||
UENUM(BlueprintType)
|
||||
enum class EInteractPhase : uint8
|
||||
enum class EInteractActionType : uint8
|
||||
{
|
||||
None UMETA(DisplayName = "无"),
|
||||
Enter UMETA(DisplayName = "进入(转向+推镜)"),
|
||||
AnimLock UMETA(DisplayName = "动画锁定"),
|
||||
Active UMETA(DisplayName = "活跃(等待输入)"),
|
||||
Exit UMETA(DisplayName = "退出(回位)")
|
||||
};
|
||||
|
||||
/** 交互结束后动作 */
|
||||
UENUM(BlueprintType)
|
||||
enum class EPostAction : uint8
|
||||
{
|
||||
Return UMETA(DisplayName = "回到Free"),
|
||||
Minigame UMETA(DisplayName = "小游戏(v0.1预留)"),
|
||||
Dialogue UMETA(DisplayName = "对话(v0.1预留)"),
|
||||
SceneChange UMETA(DisplayName = "场景切换(v0.1预留)")
|
||||
Wait UMETA(DisplayName = "等待"),
|
||||
TurnToTarget UMETA(DisplayName = "转向交互物"),
|
||||
PlayMontage UMETA(DisplayName = "播放人物动画"),
|
||||
PlayMontageOnTarget UMETA(DisplayName = "播放交互物动画"),
|
||||
CameraPush UMETA(DisplayName = "相机推镜"),
|
||||
CameraReturn UMETA(DisplayName = "相机回位"),
|
||||
Consume UMETA(DisplayName = "消耗交互物"),
|
||||
HideActor UMETA(DisplayName = "隐藏交互物"),
|
||||
ShowActor UMETA(DisplayName = "显示交互物"),
|
||||
AddToBackpack UMETA(DisplayName = "物品进背包"),
|
||||
ShowNarrative UMETA(DisplayName = "显示叙事文本"),
|
||||
HideNarrative UMETA(DisplayName = "隐藏叙事文本"),
|
||||
ShowPickupNotification UMETA(DisplayName = "显示拾取通知"),
|
||||
ToggleDoor UMETA(DisplayName = "切换门状态"),
|
||||
PlayDialogue UMETA(DisplayName = "播放对话"),
|
||||
PlayStory UMETA(DisplayName = "播放小剧情"),
|
||||
BroadcastEvent UMETA(DisplayName = "广播逻辑事件"),
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 配置数据结构
|
||||
// 前向声明
|
||||
// ============================================================================
|
||||
|
||||
/** 可交互物配置数据 — 每个交互物实例独立配置 */
|
||||
class UInteractableComponent;
|
||||
|
||||
// ============================================================================
|
||||
// Action — 原子操作定义
|
||||
// ============================================================================
|
||||
|
||||
/** 原子操作定义 — Effect 内按 TriggerTime 编排的最小执行单元 */
|
||||
USTRUCT(BlueprintType)
|
||||
struct FInteractAction
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
// === 编排 ===
|
||||
|
||||
/** Action 类型 */
|
||||
UPROPERTY(EditAnywhere, Category = "Action")
|
||||
EInteractActionType Type = EInteractActionType::Wait;
|
||||
|
||||
/** 相对于 Effect 开始的触发时间(秒) */
|
||||
UPROPERTY(EditAnywhere, Category = "Action", meta = (ClampMin = "0"))
|
||||
float TriggerTime = 0.f;
|
||||
|
||||
// === 各类型参数(按 EditCondition 折叠显示) ===
|
||||
|
||||
// --- Wait ---
|
||||
/** 等待时长(秒) */
|
||||
UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::Wait", EditConditionHides))
|
||||
float WaitDuration = 0.5f;
|
||||
|
||||
// --- TurnToTarget ---
|
||||
/** 转向时长(秒) */
|
||||
UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::TurnToTarget", EditConditionHides))
|
||||
float TurnDuration = 0.2f;
|
||||
|
||||
// --- PlayMontage ---
|
||||
/** 人物蒙太奇动画 */
|
||||
UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::PlayMontage", EditConditionHides))
|
||||
TObjectPtr<UAnimMontage> Montage;
|
||||
|
||||
// --- PlayMontageOnTarget ---
|
||||
/** 交互物蒙太奇动画 */
|
||||
UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::PlayMontageOnTarget", EditConditionHides))
|
||||
TObjectPtr<UAnimMontage> TargetMontage;
|
||||
|
||||
// --- CameraPush ---
|
||||
/** 推镜距离(cm) */
|
||||
UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::CameraPush", EditConditionHides))
|
||||
float PushDistance = 80.f;
|
||||
|
||||
/** 推镜过渡时长(秒) */
|
||||
UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::CameraPush", EditConditionHides))
|
||||
float PushDuration = 0.5f;
|
||||
|
||||
// --- CameraReturn ---
|
||||
/** 相机回位时长(秒) */
|
||||
UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::CameraReturn", EditConditionHides))
|
||||
float ReturnDuration = 0.3f;
|
||||
|
||||
// --- ShowNarrative ---
|
||||
/** 叙事文本列表 */
|
||||
UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::ShowNarrative", EditConditionHides))
|
||||
TArray<FString> NarrativeTexts;
|
||||
|
||||
/** 是否等待用户按 E/ESC 后再继续后续 Action */
|
||||
UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::ShowNarrative", EditConditionHides))
|
||||
bool bWaitForInput = false;
|
||||
|
||||
/** 是否从 NarrativeTexts 中随机选取(不重复上一次) */
|
||||
UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::ShowNarrative", EditConditionHides))
|
||||
bool bRandomSelect = true;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Effect — 交互效果定义
|
||||
// ============================================================================
|
||||
|
||||
/** Effect 定义 — 由多个 Action 按时间线编排而成的交互效果配方 */
|
||||
USTRUCT(BlueprintType)
|
||||
struct FInteractEffect
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Effect 名称(调试用) */
|
||||
UPROPERTY(EditAnywhere, Category = "Effect")
|
||||
FString EffectName;
|
||||
|
||||
/** Effect 总时长(秒),到时自动结束 */
|
||||
UPROPERTY(EditAnywhere, Category = "Effect", meta = (ClampMin = "0.1", ClampMax = "10.0"))
|
||||
float TotalDuration = 2.0f;
|
||||
|
||||
/** 是否可取消(ESC 中断) */
|
||||
UPROPERTY(EditAnywhere, Category = "Effect")
|
||||
bool bCancellable = true;
|
||||
|
||||
/** Action 列表 — 按 TriggerTime 编排,同时间并行 */
|
||||
UPROPERTY(EditAnywhere, Category = "Effect")
|
||||
TArray<FInteractAction> Actions;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 可交互物配置数据
|
||||
// ============================================================================
|
||||
|
||||
/** 可交互物配置数据 — 每个交互物实例独立配置,不再包含类型分发字段 */
|
||||
USTRUCT(BlueprintType)
|
||||
struct FInteractableData
|
||||
{
|
||||
GENERATED_BODY()
|
||||
GENERATED_BODY()
|
||||
|
||||
// === 基本信息 ===
|
||||
// === 基本信息 ===
|
||||
|
||||
/** 唯一ID(如 "fridge_home"),用于持久化存储和物品系统对接 */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Basic")
|
||||
FName ItemId;
|
||||
/** 唯一ID(如 "fridge_home"),用于持久化存储和物品系统对接 */
|
||||
UPROPERTY(EditAnywhere, Category = "Basic")
|
||||
FName ItemId;
|
||||
|
||||
/** 显示名称(如 "旧冰箱"),用于提示UI */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Basic")
|
||||
FString DisplayName;
|
||||
/** 显示名称(如 "旧冰箱"),用于提示UI */
|
||||
UPROPERTY(EditAnywhere, Category = "Basic")
|
||||
FString DisplayName;
|
||||
|
||||
/** 交互类型 */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Basic")
|
||||
EInteractType Type = EInteractType::Examine;
|
||||
/** 物品品类(如 "杂物"),用于收集系统品类过滤。
|
||||
* 拾取/翻找物填 "杂物"(收集系统追踪品类),观察物/门等非收集物留空。
|
||||
* P-013 收集系统通过 AddToBackpack 回调读取此字段做「查品类」。 */
|
||||
UPROPERTY(EditAnywhere, Category = "Basic")
|
||||
FName Category;
|
||||
|
||||
/** 操作子类型,仅 Type==Operate 时可见可编辑 */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Basic",
|
||||
meta = (EditCondition = "Type == EInteractType::Operate", EditConditionHides))
|
||||
EOperateSubType OperateSubType = EOperateSubType::Door;
|
||||
// === 触发 ===
|
||||
|
||||
// === 触发 ===
|
||||
/** 触发半径(cm),默认1.5m */
|
||||
UPROPERTY(EditAnywhere, Category = "Trigger", meta = (ClampMin = "50.0", ClampMax = "500.0"))
|
||||
float TriggerRadius = 150.f;
|
||||
|
||||
/** 触发半径(cm),默认1.5m */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Trigger",
|
||||
meta = (ClampMin = "50.0", ClampMax = "500.0"))
|
||||
float TriggerRadius = 150.0f;
|
||||
/** 是否一次性消耗(消耗后不再可交互) */
|
||||
UPROPERTY(EditAnywhere, Category = "Trigger")
|
||||
bool bOneShot = false;
|
||||
|
||||
/** 是否一次性消耗(消耗后不再可交互) */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Trigger")
|
||||
bool bOneShot = false;
|
||||
// === 提示文本 ===
|
||||
|
||||
// === 提示文本 ===
|
||||
/** 动作词(如 "查看"、"拾取"、"打开") */
|
||||
UPROPERTY(EditAnywhere, Category = "Prompt")
|
||||
FString InteractVerb;
|
||||
|
||||
/** 动作词(如 "查看"、"拾取"、"打开") */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Prompt")
|
||||
FString InteractVerb;
|
||||
/** 备选动词(如 "关上"),门打开时使用 */
|
||||
UPROPERTY(EditAnywhere, Category = "Prompt")
|
||||
FString InteractVerbAlt;
|
||||
|
||||
/** 备选动词(如 "关上"),门打开时使用,仅 Operate+Door 时可见 */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Prompt",
|
||||
meta = (EditCondition = "Type == EInteractType::Operate && OperateSubType == EOperateSubType::Door"))
|
||||
FString InteractVerbAlt;
|
||||
// === 交互 Effect ===
|
||||
|
||||
// === 叙事/对话文本 ===
|
||||
/** 交互效果定义 — 所有行为由 Effect 内的 Action 定义 */
|
||||
UPROPERTY(EditAnywhere, Category = "Effect")
|
||||
FInteractEffect InteractEffect;
|
||||
|
||||
/** 文本列表,随机选取,不重复上次 */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Text")
|
||||
TArray<FString> NarrativeTexts;
|
||||
// 已删除(由 Effect 内 Action 替代):
|
||||
// EInteractType Type, EOperateSubType OperateSubType
|
||||
// bUseCameraPush, CameraPushDistance/Duration, PushWaitDelay, CameraReturnDuration
|
||||
// InteractMontage, AnimDuration, AnimEventTimepoint
|
||||
// NarrativeTexts, EPostAction PostAction
|
||||
};
|
||||
|
||||
// === 摄像机推镜 ===
|
||||
// ============================================================================
|
||||
// 运行时上下文
|
||||
// ============================================================================
|
||||
|
||||
/** 是否使用摄像机推镜 */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Camera")
|
||||
bool bUseCameraPush = false;
|
||||
/** Effect 运行时上下文 — EffectManager 执行时传入,持有弱引用 */
|
||||
USTRUCT()
|
||||
struct FEffectContext
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** 推镜距离(cm),仅 bUseCameraPush 时可编辑 */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Camera",
|
||||
meta = (EditCondition = "bUseCameraPush", ClampMin = "10.0", ClampMax = "300.0"))
|
||||
float CameraPushDistance = 80.0f;
|
||||
/** 触发交互的角色 */
|
||||
TWeakObjectPtr<ACharacter> Instigator;
|
||||
|
||||
/** 推镜过渡时长(秒),仅 bUseCameraPush 时可编辑 */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Camera",
|
||||
meta = (EditCondition = "bUseCameraPush", ClampMin = "0.1", ClampMax = "2.0"))
|
||||
float CameraPushDuration = 0.5f;
|
||||
/** 交互物组件(配置数据来源) */
|
||||
TWeakObjectPtr<UInteractableComponent> TargetComp;
|
||||
|
||||
/** 相机回位时长(秒),仅 bUseCameraPush 时可编辑 */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Camera",
|
||||
meta = (EditCondition = "bUseCameraPush", ClampMin = "0.1", ClampMax = "2.0"))
|
||||
float CameraReturnDuration = 0.3f;
|
||||
/** 交互物 Actor(视觉操作目标) */
|
||||
TWeakObjectPtr<AActor> TargetActor;
|
||||
};
|
||||
|
||||
/** 推镜完成后到文本显示的等待时间(秒),仅 bUseCameraPush 时可编辑 */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Camera",
|
||||
meta = (EditCondition = "bUseCameraPush", ClampMin = "0.0", ClampMax = "1.0"))
|
||||
float PushWaitDelay = 0.2f;
|
||||
// ============================================================================
|
||||
// Presentation 层缓存数据(非 UObject,纯 C++)
|
||||
// ============================================================================
|
||||
|
||||
// === 动画 ===
|
||||
/** Presentation 层缓存数据 — Manager 在选择变化时从 Component 读一次并写入,
|
||||
* Prompt 组件每帧只读此缓存,不直接访问 Component */
|
||||
struct FSelectedInfo
|
||||
{
|
||||
/** 是否有有效选中 */
|
||||
bool bHasSelection = false;
|
||||
|
||||
/** 交互动画蒙太奇(可选,为空时用计时器模拟) */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Anim")
|
||||
TObjectPtr<UAnimMontage> InteractMontage;
|
||||
/** 选中交互物的提示文本(如 "[E] 查看旧冰箱") */
|
||||
FString PromptText;
|
||||
|
||||
/** 无蒙太奇时的动画持续时间(秒) */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Anim",
|
||||
meta = (ClampMin = "0.1", ClampMax = "5.0"))
|
||||
float AnimDuration = 1.0f;
|
||||
/** 选中交互物的世界坐标(用于屏幕空间投影) */
|
||||
FVector WorldPosition = FVector::ZeroVector;
|
||||
|
||||
/** 动画事件触发点(0-1比例),如0.6表示动画播放至60%时触发物品消失 */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Anim",
|
||||
meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||
float AnimEventTimepoint = 0.6f;
|
||||
/** 选中交互物是否已消耗(消耗后不再显示提示) */
|
||||
bool bIsConsumed = false;
|
||||
|
||||
// === 结束动作 ===
|
||||
void Reset()
|
||||
{
|
||||
bHasSelection = false;
|
||||
PromptText.Empty();
|
||||
WorldPosition = FVector::ZeroVector;
|
||||
bIsConsumed = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 交互结束后的动作 */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Post")
|
||||
EPostAction PostAction = EPostAction::Return;
|
||||
// ============================================================================
|
||||
// TurnToTarget 跨帧插值状态(非 UObject,纯 C++)
|
||||
// ============================================================================
|
||||
|
||||
/** TurnToTarget 跨帧插值状态 — EffectManager 在 Update 中维护,
|
||||
* 每帧 RInterpTo 驱动旋转,不能一帧完成 */
|
||||
struct FTurnState
|
||||
{
|
||||
/** 是否正在转向 */
|
||||
bool bIsActive = false;
|
||||
|
||||
/** 转向起始旋转 */
|
||||
FRotator StartRotation = FRotator::ZeroRotator;
|
||||
|
||||
/** 转向目标旋转(面朝交互物方向) */
|
||||
FRotator TargetRotation = FRotator::ZeroRotator;
|
||||
|
||||
/** 已用时间(秒) */
|
||||
float ElapsedTime = 0.f;
|
||||
|
||||
/** 转向总时长(秒) */
|
||||
float Duration = 0.2f;
|
||||
|
||||
/** 是否已完成 */
|
||||
bool IsComplete() const { return ElapsedTime >= Duration; }
|
||||
|
||||
/** 获取当前插值 Alpha [0,1] */
|
||||
float GetAlpha() const { return FMath::Clamp(ElapsedTime / Duration, 0.f, 1.f); }
|
||||
|
||||
void Reset()
|
||||
{
|
||||
bIsActive = false;
|
||||
StartRotation = FRotator::ZeroRotator;
|
||||
TargetRotation = FRotator::ZeroRotator;
|
||||
ElapsedTime = 0.f;
|
||||
Duration = 0.2f;
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 委托
|
||||
// ============================================================================
|
||||
|
||||
/** 物品拾取/获得事件 — 通知物品系统和收集系统 */
|
||||
/** 物品消耗事件(逻辑事件,Persistence/Backpack 订阅) */
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(
|
||||
FMLocalItemEvent,
|
||||
FName, ItemId,
|
||||
const FString&, DisplayName,
|
||||
const FName&, Category
|
||||
FMLocalItemEvent,
|
||||
FName, ItemId,
|
||||
const FString&, DisplayName,
|
||||
const FName&, Category
|
||||
);
|
||||
|
||||
/** Effect 完成回调 */
|
||||
DECLARE_DELEGATE(FOnEffectComplete);
|
||||
|
||||
/** 物品获得回调 — AddToBackpack Action 执行时触发,参数 (ItemId, DisplayName, Category)。
|
||||
* 由 FEffectManager 持有,ASystemDriver 在 BeginPlay 中绑定到 FCollectionManager::OnItemObtained,
|
||||
* 实现「交互系统 → 收集系统」的解耦连线(收集系统技术方案第13节)。
|
||||
* 普通委托(非动态多播):纯 C++ lambda 绑定,EffectManager 为普通 C++ 类。 */
|
||||
DECLARE_DELEGATE_ThreeParams(FOnItemAdded, FName, const FString&, const FName&);
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// ChildHood Project - 跨场景持久化子系统 实现
|
||||
|
||||
#include "Interaction/PersistenceSubsystem.h"
|
||||
#include "ChildHood.h"
|
||||
|
||||
// ============================================================================
|
||||
// 物品消耗状态
|
||||
// ============================================================================
|
||||
|
||||
bool UPersistenceSubsystem::IsItemCollected(FName ItemId) const
|
||||
{
|
||||
return CollectedItemIDs.Contains(ItemId);
|
||||
}
|
||||
|
||||
void UPersistenceSubsystem::MarkItemCollected(FName ItemId)
|
||||
{
|
||||
CollectedItemIDs.Add(ItemId);
|
||||
UE_LOG(LogChildHood, Log, TEXT("PersistenceSubsystem: 物品已收集: %s"), *ItemId.ToString());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 门状态
|
||||
// ============================================================================
|
||||
|
||||
bool UPersistenceSubsystem::IsDoorOpen(FName DoorId) const
|
||||
{
|
||||
const bool* Found = DoorStates.Find(DoorId);
|
||||
return Found ? *Found : false;
|
||||
}
|
||||
|
||||
void UPersistenceSubsystem::SetDoorOpen(FName DoorId, bool bIsOpen)
|
||||
{
|
||||
DoorStates.Add(DoorId, bIsOpen);
|
||||
UE_LOG(LogChildHood, Log, TEXT("PersistenceSubsystem: 门状态更新: %s → %s"),
|
||||
*DoorId.ToString(), bIsOpen ? TEXT("开") : TEXT("关"));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 收集系统状态
|
||||
// ============================================================================
|
||||
|
||||
void UPersistenceSubsystem::GetCollectionState(TSet<FName>& OutIDs, int32& OutProgress, bool& OutComplete) const
|
||||
{
|
||||
OutIDs = CollectionCollectedIDs;
|
||||
OutProgress = CollectionProgress;
|
||||
OutComplete = bCollectionComplete;
|
||||
}
|
||||
|
||||
void UPersistenceSubsystem::SetCollectionState(const TSet<FName>& InIDs, int32 InProgress, bool bInComplete)
|
||||
{
|
||||
CollectionCollectedIDs = InIDs;
|
||||
CollectionProgress = InProgress;
|
||||
bCollectionComplete = bInComplete;
|
||||
UE_LOG(LogChildHood, Log, TEXT("PersistenceSubsystem: 收集状态更新 - 进度 %d, 完成 %s"),
|
||||
CollectionProgress, bCollectionComplete ? TEXT("是") : TEXT("否"));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 重置
|
||||
// ============================================================================
|
||||
|
||||
void UPersistenceSubsystem::ResetAll()
|
||||
{
|
||||
CollectedItemIDs.Empty();
|
||||
DoorStates.Empty();
|
||||
CollectionCollectedIDs.Empty();
|
||||
CollectionProgress = 0;
|
||||
bCollectionComplete = false;
|
||||
UE_LOG(LogChildHood, Log, TEXT("PersistenceSubsystem: 所有持久数据已重置"));
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// ChildHood Project - 跨场景持久化子系统
|
||||
// 对应技术方案: docs/technical/交互系统-技术方案.md 第12节(跨场景持久化设计)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Subsystems/GameInstanceSubsystem.h"
|
||||
#include "PersistenceSubsystem.generated.h"
|
||||
|
||||
/**
|
||||
* UPersistenceSubsystem
|
||||
*
|
||||
* UGameInstanceSubsystem,跨场景持久数据持有者。
|
||||
* UE5 的 OpenLevel() 会销毁整个旧 World(包括所有 Actor 和 Component),
|
||||
* 因此将持久数据放在 GameInstanceSubsystem 中,与 World 生命周期解耦。
|
||||
*
|
||||
* 当前持有:
|
||||
* - CollectedItemIDs: 已收集物品 ID 集合
|
||||
* - DoorStates: 门开关状态映射
|
||||
*/
|
||||
UCLASS()
|
||||
class UPersistenceSubsystem : public UGameInstanceSubsystem
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
// === 物品消耗状态 ===
|
||||
|
||||
/** 查询指定物品是否已被收集(跨场景持久) */
|
||||
UFUNCTION(BlueprintCallable, Category = "Persistence")
|
||||
bool IsItemCollected(FName ItemId) const;
|
||||
|
||||
/** 标记指定物品为已收集(写入持久层) */
|
||||
UFUNCTION(BlueprintCallable, Category = "Persistence")
|
||||
void MarkItemCollected(FName ItemId);
|
||||
|
||||
// === 门状态 ===
|
||||
|
||||
/** 查询指定门的开关状态(跨场景持久),未记录的门默认为关闭 */
|
||||
UFUNCTION(BlueprintCallable, Category = "Persistence")
|
||||
bool IsDoorOpen(FName DoorId) const;
|
||||
|
||||
/** 设置指定门的开关状态(写入持久层) */
|
||||
UFUNCTION(BlueprintCallable, Category = "Persistence")
|
||||
void SetDoorOpen(FName DoorId, bool bIsOpen);
|
||||
|
||||
// === 重置 ===
|
||||
|
||||
/** 重置所有持久数据(新游戏时调用) */
|
||||
UFUNCTION(BlueprintCallable, Category = "Persistence")
|
||||
void ResetAll();
|
||||
|
||||
// === 收集系统状态 ===
|
||||
// 与上方 CollectedItemIDs 区分:CollectedItemIDs 记录交互物消耗状态(交互系统用),
|
||||
// 下方三个成员记录追踪品类的收集进度(收集系统用,技术方案第12节)。
|
||||
|
||||
/** 获取收集状态(恢复用):追踪品类的已收集 ID 集合、当前进度、是否全部完成 */
|
||||
UFUNCTION(BlueprintCallable, Category = "Persistence|Collection")
|
||||
void GetCollectionState(TSet<FName>& OutIDs, int32& OutProgress, bool& OutComplete) const;
|
||||
|
||||
/** 设置收集状态(收集时写入) */
|
||||
UFUNCTION(BlueprintCallable, Category = "Persistence|Collection")
|
||||
void SetCollectionState(const TSet<FName>& InIDs, int32 InProgress, bool bInComplete);
|
||||
|
||||
private:
|
||||
|
||||
/** 已收集物品 ID 集合 */
|
||||
UPROPERTY()
|
||||
TSet<FName> CollectedItemIDs;
|
||||
|
||||
/** 门开关状态映射(true=打开,false=关闭) */
|
||||
UPROPERTY()
|
||||
TMap<FName, bool> DoorStates;
|
||||
|
||||
// === 收集系统数据 ===
|
||||
|
||||
/** 追踪品类的已收集物品 ID 集合(收集系统用) */
|
||||
UPROPERTY()
|
||||
TSet<FName> CollectionCollectedIDs;
|
||||
|
||||
/** 当前收集进度(收集系统用) */
|
||||
UPROPERTY()
|
||||
int32 CollectionProgress = 0;
|
||||
|
||||
/** 是否已全部收集完成(收集系统用) */
|
||||
UPROPERTY()
|
||||
bool bCollectionComplete = false;
|
||||
};
|
||||
@@ -0,0 +1,153 @@
|
||||
// ChildHood Project - 系统驱动器 实现(v1.1 Effect/Action 架构)
|
||||
// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计)
|
||||
// 职责:全局唯一的 World 级驱动器,持有所有 Manager,负责初始化、Tick、跨 Manager 协调
|
||||
|
||||
#include "SystemDriver.h"
|
||||
#include "ChildHood.h"
|
||||
|
||||
#include "Engine/World.h"
|
||||
#include "EngineUtils.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "GameFramework/Character.h"
|
||||
#include "ChildHoodCharacter.h"
|
||||
#include "Interaction/PersistenceSubsystem.h"
|
||||
|
||||
// ============================================================================
|
||||
// 构造函数
|
||||
// ============================================================================
|
||||
|
||||
ASystemDriver::ASystemDriver()
|
||||
{
|
||||
// AInfo 默认不需要 Tick,我们在构造函数中开启
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
PrimaryActorTick.bStartWithTickEnabled = true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 生命周期
|
||||
// ============================================================================
|
||||
|
||||
void ASystemDriver::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// --- 初始化 InteractionManager ---
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
if (APlayerController* PC = World->GetFirstPlayerController())
|
||||
{
|
||||
if (ACharacter* Char = PC->GetCharacter())
|
||||
{
|
||||
InteractionManager.Initialize(Char);
|
||||
UE_LOG(LogChildHood, Log, TEXT("SystemDriver: InteractionManager 初始化完成"));
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogChildHood, Warning, TEXT("SystemDriver: PlayerController 无 Character,InteractionManager 未初始化"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogChildHood, Warning, TEXT("SystemDriver: World 中无 PlayerController"));
|
||||
}
|
||||
}
|
||||
|
||||
// --- 初始化 EffectManager ---
|
||||
{
|
||||
FOnEffectComplete Callback;
|
||||
Callback.BindLambda([this]() { this->OnEffectComplete(); });
|
||||
EffectManager.SetOnEffectComplete(MoveTemp(Callback));
|
||||
|
||||
UE_LOG(LogChildHood, Log, TEXT("SystemDriver: EffectManager 初始化完成"));
|
||||
}
|
||||
|
||||
// --- 初始化 CollectionManager(收集系统,技术方案第13节回调连线)---
|
||||
{
|
||||
// 1. 本地状态重置
|
||||
CollectionManager.Initialize();
|
||||
|
||||
// 2. 从持久层恢复进度 + 缓存 Subsystem 弱引用
|
||||
if (UGameInstance* GI = GetGameInstance())
|
||||
{
|
||||
if (UPersistenceSubsystem* Persistence = GI->GetSubsystem<UPersistenceSubsystem>())
|
||||
{
|
||||
CollectionManager.RestoreFromPersistence(Persistence);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 回调连线:EffectManager AddToBackpack Action → CollectionManager.OnItemObtained
|
||||
// 交互系统与收集系统不直接耦合,通过 Driver 的 lambda 连线(v1.0 解耦核心)
|
||||
FOnItemAdded ItemAddedCallback;
|
||||
ItemAddedCallback.BindLambda([this](FName ItemId, const FString& DisplayName, const FName& Category)
|
||||
{
|
||||
CollectionManager.OnItemObtained(ItemId, DisplayName, Category);
|
||||
});
|
||||
EffectManager.SetOnItemAddedCallback(MoveTemp(ItemAddedCallback));
|
||||
|
||||
UE_LOG(LogChildHood, Log, TEXT("SystemDriver: CollectionManager 初始化完成 + AddToBackpack 回调连线"));
|
||||
}
|
||||
}
|
||||
|
||||
void ASystemDriver::Tick(float DeltaTime)
|
||||
{
|
||||
Super::Tick(DeltaTime);
|
||||
|
||||
// --- Tick EffectManager ---
|
||||
// InteractionManager 无 Tick(纯事件驱动)
|
||||
EffectManager.Update(DeltaTime);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 全局访问
|
||||
// ============================================================================
|
||||
|
||||
ASystemDriver* ASystemDriver::Get(const UObject* WorldContextObject)
|
||||
{
|
||||
if (!WorldContextObject)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);
|
||||
if (!World)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// 使用 TActorIterator 查找场景中的 ASystemDriver
|
||||
// 全局唯一,取第一个即可
|
||||
for (TActorIterator<ASystemDriver> It(World); It; ++It)
|
||||
{
|
||||
return *It;
|
||||
}
|
||||
|
||||
// 场景中不存在时返回 nullptr
|
||||
// 应在关卡中放置一个 ASystemDriver,或在 GameMode::BeginPlay 中 Spawn
|
||||
UE_LOG(LogChildHood, Verbose, TEXT("SystemDriver::Get: 场景中未找到 ASystemDriver"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Effect 完成回调
|
||||
// ============================================================================
|
||||
|
||||
void ASystemDriver::OnEffectComplete()
|
||||
{
|
||||
// Effect 完成后通知 InteractionManager 恢复 Free 状态
|
||||
InteractionManager.OnInteractionFinished();
|
||||
|
||||
// 通知 Character 复位交互状态(解除移动屏蔽)。
|
||||
// FInteractionManager 作为纯逻辑层不操作 Character 表现状态,
|
||||
// 由 ASystemDriver 作为 World 级协调者统一驱动。
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
if (APlayerController* PC = World->GetFirstPlayerController())
|
||||
{
|
||||
if (AChildHoodCharacter* Char = Cast<AChildHoodCharacter>(PC->GetCharacter()))
|
||||
{
|
||||
Char->SetInteracting(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// ChildHood Project - 系统驱动器(v1.1 Effect/Action 架构)
|
||||
// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计)
|
||||
// 职责:全局唯一的 World 级驱动器,持有所有 Manager(FInteractionManager, FEffectManager),
|
||||
// 负责初始化、Tick 驱动、跨 Manager 协调。
|
||||
// 架构:AInfo 子类(不渲染、不占场景空间),每关只有一个实例。
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Info.h"
|
||||
#include "Interaction/InteractionManager.h"
|
||||
#include "Interaction/EffectManager.h"
|
||||
#include "Collection/CollectionManager.h"
|
||||
#include "SystemDriver.generated.h"
|
||||
|
||||
/**
|
||||
* ASystemDriver
|
||||
*
|
||||
* 全局唯一的 World 级系统驱动器。持有 FInteractionManager 和 FEffectManager,
|
||||
* 负责它们的初始化、Tick 驱动和跨 Manager 协调。
|
||||
*
|
||||
* 访问方式:ASystemDriver::Get(WorldContextObject) 静态方法。
|
||||
*
|
||||
* 数据流:
|
||||
* InteractableComponent 碰撞回调 → Get() → GetInteractionManager().Register/Unregister
|
||||
* Character 输入回调 → Get() → GetInteractionManager().BeginInteract/Cancel/Switch
|
||||
* EffectManager 完成 → OnEffectComplete → GetInteractionManager().OnInteractionFinished
|
||||
*/
|
||||
UCLASS()
|
||||
class ASystemDriver : public AInfo
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
ASystemDriver();
|
||||
|
||||
// === 生命周期 ===
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
virtual void Tick(float DeltaTime) override;
|
||||
|
||||
// === 全局访问 ===
|
||||
|
||||
/**
|
||||
* 获取当前 World 中的 ASystemDriver 实例。
|
||||
* WorldContextObject 用于获取 World 上下文(通常传 this 或 GetWorld())。
|
||||
*/
|
||||
UFUNCTION(BlueprintPure, Category = "System", meta = (WorldContext = "WorldContextObject"))
|
||||
static ASystemDriver* Get(const UObject* WorldContextObject);
|
||||
|
||||
// === Manager 访问 ===
|
||||
|
||||
/** 获取交互管理器(值引用) */
|
||||
FInteractionManager& GetInteractionManager() { return InteractionManager; }
|
||||
const FInteractionManager& GetInteractionManager() const { return InteractionManager; }
|
||||
|
||||
/** 获取效果管理器(值引用) */
|
||||
FEffectManager& GetEffectManager() { return EffectManager; }
|
||||
const FEffectManager& GetEffectManager() const { return EffectManager; }
|
||||
|
||||
/** 获取收集管理器(值引用)— 收集系统逻辑核心 */
|
||||
FCollectionManager& GetCollectionManager() { return CollectionManager; }
|
||||
const FCollectionManager& GetCollectionManager() const { return CollectionManager; }
|
||||
|
||||
private:
|
||||
|
||||
/** 交互管理器 */
|
||||
FInteractionManager InteractionManager;
|
||||
|
||||
/** 效果管理器 */
|
||||
FEffectManager EffectManager;
|
||||
|
||||
/** 收集管理器(收集系统逻辑核心) */
|
||||
FCollectionManager CollectionManager;
|
||||
|
||||
// === Effect 完成回调连线 ===
|
||||
|
||||
/** EffectManager 的 OnEffectComplete 回调目标 */
|
||||
void OnEffectComplete();
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
# docs/design/ — 策划文档索引
|
||||
|
||||
> 策划 Agent 的输出目录。所有玩法设计、系统机制、剧情叙事文档存放于此。
|
||||
> 最后更新:2026-06-08
|
||||
> 最后更新:2026-06-15
|
||||
|
||||
---
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
| [玩家控制器.md](玩家控制器.md) | 玩家角色控制——移动、摄像机、动画蓝图 | v0.1 ✅ |
|
||||
| [交互系统.md](交互系统.md) | 环境交互系统——交互组件、检测、UI 提示 | v0.1 ✅ |
|
||||
| [收集系统.md](收集系统.md) | 收集系统——拾取、进度追踪、图鉴(v0.1 基础版 → v0.4 完整版) | v0.1 ✅ v0.2+ 待补 |
|
||||
| [小游戏系统.md](小游戏系统.md) | 小游戏体系——弹珠、拍洋画、四驱车等千禧年童年游戏 | v0.2 待补执行案 |
|
||||
| [小游戏系统.md](小游戏系统.md) | 小游戏体系——弹珠、拍洋画、四驱车等千禧年童年游戏 | v0.2 弹珠执行案已产出 |
|
||||
| [叙事框架.md](叙事框架.md) | 叙事框架——暑假结构、事件设计、角色系统 | v0.3-v0.4 待补 |
|
||||
|
||||
---
|
||||
@@ -43,6 +43,8 @@
|
||||
| [交互系统-执行案.md](交互系统-执行案.md) | 交互系统 | v0.1.2 | 📝 设计中 |
|
||||
| [收集系统-执行案.md](收集系统-执行案.md) | 收集系统 | v0.1.1 | 📝 设计中 |
|
||||
| [关卡-v01-城郊小街-执行案.md](关卡-v01-城郊小街-执行案.md) | 关卡(城郊小街白盒) | v0.1 | 📝 设计中 |
|
||||
| [弹珠小游戏-执行案.md](弹珠小游戏-执行案.md) | 弹珠小游戏(v0.2 核心) | v0.2.1 | 📝 设计中 |
|
||||
| [蹲下系统-执行案.md](蹲下系统-执行案.md) | 蹲下系统(玩家控制器扩展) | v0.2.0 | 📝 设计中 |
|
||||
|
||||
---
|
||||
|
||||
@@ -51,6 +53,7 @@
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [v01-范围.md](v01-范围.md) | v0.1 功能边界、验收标准、开发优先级 |
|
||||
| [v02-范围.md](v02-范围.md) | v0.2 功能边界、验收标准、开发优先级 |
|
||||
|
||||
---
|
||||
|
||||
@@ -69,6 +72,7 @@
|
||||
| [头脑风暴-2026-06-01-愿景支柱与世界观.md](头脑风暴-2026-06-01-愿景支柱与世界观.md) | 2026-06-01 | 愿景三大支柱 + 世界观确认 |
|
||||
| [头脑风暴-2026-06-04-时代与科技灵感.md](头脑风暴-2026-06-04-时代与科技灵感.md) | 2026-06-04 | 千禧年时代技术灵感(随身听、小霸王、街机等) |
|
||||
| [头脑风暴-2026-06-05-怀旧文化.md](头脑风暴-2026-06-05-怀旧文化.md) | 2026-06-05 | 千禧年怀旧文化灵感(电视剧、广播体操、广场舞等) |
|
||||
| [头脑风暴-2026-06-15-弹珠操作手感与系统决策.md](头脑风暴-2026-06-15-弹珠操作手感与系统决策.md) | 2026-06-15 | v0.2 弹珠操作手感方案 + 系统决策 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
# v0.2 范围定义 — 弹珠小游戏
|
||||
|
||||
> 本文档明确 v0.2 原型的功能边界,作为程序和美术的需求基准。
|
||||
> 最后更新:2026-06-15
|
||||
|
||||
---
|
||||
|
||||
## v0.2 目标
|
||||
|
||||
**验证核心问题**:弹珠小游戏的操作手感成立吗?策略深度够让人玩十几把不腻吗?
|
||||
|
||||
**体验目标**:
|
||||
1. 弹弓式拖拽操作直觉流畅,方向与力度一步到位
|
||||
2. 三维策略(场地 × 弹珠属性 × 规则)让每局体验不同
|
||||
3. 蹲下探索增加场景的层次感
|
||||
4. 弹珠与收集系统联动,赢弹珠有获得感
|
||||
|
||||
**一句话**:"在空地蹲下来,选一颗弹珠,和死党 B 来一局画圈弹。"
|
||||
|
||||
---
|
||||
|
||||
## In Scope(v0.2 包含)
|
||||
|
||||
### 弹珠小游戏(核心)
|
||||
- [ ] 弹弓式拖拽操作(方向力度合一 + 轨迹预测线)
|
||||
- [ ] 斜俯视视角(45° 俯瞰场地)
|
||||
- [ ] 回合制对战(玩家 / NPC 轮流)
|
||||
- [ ] 画圈弹规则(1 种)
|
||||
- [ ] 2 种场地(水泥地 / 泥土地,摩擦差异)
|
||||
- [ ] 3 种弹珠属性(玻璃 / 陶瓷 / 铁)
|
||||
- [ ] 弹珠物理表现(摩擦 / 碰撞 / 停止判定)
|
||||
- [ ] 1 个 NPC 对手(死党 B)+ 弹射 AI
|
||||
- [ ] 对战库存管理(初始 5 颗)
|
||||
- [ ] 保底机制(库存不低于 3 颗)
|
||||
- [ ] 胜负判定与弹珠流转
|
||||
|
||||
### 蹲下系统
|
||||
- [ ] C 键切换站 / 蹲姿态
|
||||
- [ ] 蹲伏移动(2.0 m/s)
|
||||
- [ ] 蹲伏有效高度(1.0m,可钻低矮空间)
|
||||
- [ ] 蹲伏视角降低
|
||||
- [ ] 弹珠入场由系统强制蹲下
|
||||
|
||||
### 收集系统扩展
|
||||
- [ ] 弹珠作为收集品(稀有花色弹珠进图鉴的通知链路打通)
|
||||
- [ ] 对战库存 / 收藏图鉴分离
|
||||
|
||||
### NPC
|
||||
- [ ] 1 个 NPC 对手静态站位(死党 B)
|
||||
- [ ] 弹射 AI 决策(瞄准 + 力度估算 + 准度偏差)
|
||||
|
||||
### 场景
|
||||
- [ ] 空地弹珠场地(2 个:水泥地 + 泥土地)
|
||||
- [ ] 弹珠场地标识(圈 / 大树下区域标记)
|
||||
- [ ] 低矮空间(供蹲下探索,如桌下 / 矮洞)
|
||||
|
||||
### 视觉
|
||||
- [ ] 弹珠用颜色区分属性(白盒阶段)
|
||||
- [ ] 弹珠场地白盒表现(圈、起点、目标弹珠)
|
||||
|
||||
---
|
||||
|
||||
## Out of Scope(v0.2 不包含)
|
||||
|
||||
### 明确排除
|
||||
- ❌ 多规则(打坑 / 靠墙 / 斜坡)→ v0.3+
|
||||
- ❌ 手法系统(拇指法 / OK 法)→ v0.3+
|
||||
- ❌ 手柄操作 → 后续版本
|
||||
- ❌ 收藏图鉴完整 UI → v0.4(收集系统完整版)
|
||||
- ❌ 多 NPC 对手切换 → v0.2 固定 1 个对手
|
||||
- ❌ NPC 日常 AI / 移动 / 对话树 → v0.3
|
||||
- ❌ 拍洋画 / 四驱车 / FC 街机等其他小游戏 → 后续版本
|
||||
- ❌ 弹珠花色外观美术差异 → v0.2 白盒,用颜色区分
|
||||
- ❌ 玩家投入稀有弹珠对战 → v0.3+
|
||||
- ❌ 回合数上限 / 主动认输 → v0.2 弹完即结束
|
||||
- ❌ 音效(弹射 / 碰撞 / 结算)→ v0.3
|
||||
- ❌ 蹲下静音 / 潜行机制 → v0.3+
|
||||
- ❌ 匍匐 / 滑铲 → 后续版本
|
||||
- ❌ 时间循环系统 → v0.3
|
||||
- ❌ 经济系统(零花钱)→ v0.4
|
||||
- ❌ 存档 / 读档 → 后续版本
|
||||
|
||||
---
|
||||
|
||||
## 验收标准
|
||||
|
||||
### 操作手感
|
||||
- [ ] 弹弓式拖拽的方向与力度直观,松手弹射符合预期
|
||||
- [ ] 轨迹预测线清晰,玩家能据此预判弹珠路径
|
||||
- [ ] 连续玩 5 把以上不觉得操作别扭
|
||||
|
||||
### 策略深度
|
||||
- [ ] 不同弹珠属性(玻璃 / 陶瓷 / 铁)在游戏中能感知到差异
|
||||
- [ ] 水泥地 vs 泥土地表现明显不同,需要调整策略
|
||||
- [ ] 玩家想主动尝试不同弹珠 / 场地组合
|
||||
|
||||
### 物理表现
|
||||
- [ ] 弹珠滚动、碰撞、停止的表现合理
|
||||
- [ ] 不同场地的摩擦差异可感知
|
||||
|
||||
### 蹲下体验
|
||||
- [ ] C 键切换蹲 / 站过渡自然
|
||||
- [ ] 蹲伏移动速度降低、视角降低
|
||||
- [ ] 蹲下能钻过低矮空间,发现站立时进不去的区域
|
||||
|
||||
### 收集联动
|
||||
- [ ] 赢弹珠增加对战库存,输弹珠减少(有保底不归零)
|
||||
- [ ] 稀有弹珠进入收藏图鉴(通知链路打通)
|
||||
|
||||
### NPC 对战
|
||||
- [ ] 死党 B 作为对手能正常弹射,体现准度差异
|
||||
- [ ] NPC 弹射决策合理(瞄准目标弹珠、估算力度)
|
||||
|
||||
### 可重复性(主观)
|
||||
- [ ] 连续玩十几把不觉得无聊
|
||||
- [ ] 整体体验让人觉得弹珠小游戏"好玩"——游戏三大支柱之一成立
|
||||
|
||||
---
|
||||
|
||||
## 与 v0.3 的衔接点
|
||||
|
||||
v0.2 完成后,v0.3 将在以下基础上扩展:
|
||||
|
||||
1. **时间循环系统**:四时段(上午 / 下午 / 傍晚 / 夜晚)推进 + 光影联动
|
||||
2. **NPC 日常 AI**:从静态站位升级为按时段移动、有自己的生活
|
||||
3. **对话系统**:NPC 从"弹珠对手"升级为可交谈(v0.2 仅有弹射交互)
|
||||
4. **多规则弹珠**:在画圈弹基础上增加打坑 / 靠墙 / 斜坡
|
||||
5. **音效**:弹射 / 碰撞 / 环境音 / 时段过渡音
|
||||
|
||||
---
|
||||
|
||||
## 开发优先级(供程序 Agent 参考)
|
||||
|
||||
1. **P0 — 弹珠操作手感 Throwaway 原型**(2-3 天):先用最简实现验证弹弓式拖拽 + 物理参数的可行性,**验证不通过则调整方案**(路线图标注的最大风险,必须前置)
|
||||
2. **P0 — 弹珠物理系统 + 画圈弹规则**:摩擦 / 碰撞 / 停止判定 / 弹出判定
|
||||
3. **P0 — 弹弓式操作完整流程 + 斜俯视视角 + 状态机**:弹珠小游戏核心框架
|
||||
4. **P1 — 三维策略数据**:规则 / 场地 / 弹珠属性配置
|
||||
5. **P1 — NPC 对手 + 弹射 AI**:死党 B 的弹射决策
|
||||
6. **P1 — 对战库存 + 保底 + 收集联动**
|
||||
7. **P1 — 蹲下系统**:玩家控制器姿态扩展
|
||||
8. **P2 — 场地差异调优 + 场景扩展 + 参数打磨**
|
||||
|
||||
---
|
||||
|
||||
## 预估工作量(粗略)
|
||||
|
||||
| 模块 | 预估 | 说明 |
|
||||
|------|------|------|
|
||||
| 弹珠操作手感原型 | 2-3 天 | Throwaway,验证操作方案(风险前置) |
|
||||
| 弹珠物理系统 | 3-5 天 | 摩擦 / 碰撞 / 参数调优 |
|
||||
| 弹弓操作 + 斜俯视 + 状态机 | 3-5 天 | 弹珠小游戏核心框架 |
|
||||
| 三维策略数据 | 2-3 天 | 规则 / 场地 / 弹珠属性配置 |
|
||||
| NPC 对手 + AI | 3-4 天 | 死党 B 弹射决策 |
|
||||
| 蹲下系统 | 1-2 天 | 玩家控制器姿态扩展 |
|
||||
| 对战库存 + 保底 + 收集联动 | 2-3 天 | |
|
||||
| 场地 + 场景扩展 | 1-2 天 | 空地弹珠场地 + 低矮空间 |
|
||||
| 调试打磨 | 3-5 天 | 含物理调参 |
|
||||
| **合计** | **18-28 天** | 与路线图一致 |
|
||||
|
||||
---
|
||||
|
||||
## 关键风险
|
||||
|
||||
1. **操作手感**(已通过头脑风暴化解方向):弹弓式拖拽方案已明确(见 `头脑风暴-2026-06-15-弹珠操作手感与系统决策.md`),但仍需 Throwaway 原型验证物理参数的实际手感
|
||||
2. **弹珠物理对微小物体数值敏感**(弹珠半径约 0.015m):摩擦 / 碰撞参数需反复调参
|
||||
3. **轨迹预测线与实际物理的一致性**:预测线不显示碰撞后路径,需让玩家理解预测线只反映直线路径
|
||||
@@ -0,0 +1,154 @@
|
||||
# 头脑风暴 — 2026-06-15 — 弹珠操作手感与系统决策
|
||||
|
||||
> v0.2 弹珠小游戏设计的方向确认会议。
|
||||
> 参与者:用户(starmen1265)+ 策划 Agent
|
||||
> 状态:核心共识已确认 ✅,作为弹珠执行案的起草基准
|
||||
|
||||
---
|
||||
|
||||
## 背景
|
||||
|
||||
v0.1 策划设计已收口,启动 v0.2 设计准备。v0.2 核心是**弹珠小游戏**,验证假设:
|
||||
|
||||
> 弹珠四维策略系统(规则 × 场地 × 弹珠 × 手法)操作手感成立吗?策略深度够让人玩十几把不腻吗?
|
||||
|
||||
路线图标注的**最大风险**:操作手感。必须先验证操作方案,验证不通过可能增加 5-10 天。
|
||||
|
||||
**本次头脑风暴目标**:在起草弹珠执行案前,确认操作手感方案和关键系统决策,化解核心风险。
|
||||
|
||||
---
|
||||
|
||||
## 共识 1:操作取向 — 简化上手,策略为主
|
||||
|
||||
**决策**:操作简化易上手,重复可玩性来自四维策略组合(换弹珠/场地/规则/手法)。
|
||||
|
||||
**理由**:
|
||||
- 符合"十几把不腻靠策略而非操作精度"的设计意图
|
||||
- 降低开发风险(操作方案验证更容易通过)
|
||||
- 弹珠属性差异(大小/材质)已提供足够策略变化空间
|
||||
|
||||
**否决的备选**:
|
||||
- 手感至上偏写实(类似台球/高尔夫拟真手感)— 学习曲线陡,验证风险最高
|
||||
- 折中(易上手 + 进阶深度)— 设计与实现复杂度最高
|
||||
|
||||
---
|
||||
|
||||
## 共识 2:摄像机视角 — 斜俯视
|
||||
|
||||
**决策**:45° 斜俯视,俯瞰整个弹珠场地。
|
||||
|
||||
**理由**:
|
||||
- 能看到全局(场地、弹珠、规则标记如圈/坑),瞄准与轨迹直观
|
||||
- 最契合"简化上手"
|
||||
- 场景美术保持真实感(千禧年空地、泥地),仅相机抬高方便操作(呼应系统设计"真实感在视觉,玩法不必完全拟真")
|
||||
|
||||
**代价**:略损失"小孩蹲着弹"的代入感(可接受,美术真实感弥补)。
|
||||
|
||||
---
|
||||
|
||||
## 共识 3:操作模式 — 回合制
|
||||
|
||||
**决策**:玩家与 NPC 轮流弹,每人一回合。
|
||||
|
||||
**理由**:
|
||||
- 节奏清晰,策略思考空间大
|
||||
- 契合 v0.2 唯一规则"画圈弹"(轮流把对方弹珠弹出圈)
|
||||
|
||||
---
|
||||
|
||||
## 共识 4:瞄准 + 力度操作 — 弹弓式拖拽(核心)
|
||||
|
||||
**决策**:鼠标按住自己弹珠,往反方向拖拽。
|
||||
|
||||
```
|
||||
拖拽方向 = 弹射方向(反向)
|
||||
拖拽距离 = 力度
|
||||
实时显示轨迹预测线(虚线弧)
|
||||
松开鼠标 = 弹珠按预测轨迹弹出
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 一步到位完成瞄准 + 力度,最直觉(愤怒小鸟/弹珠台经典交互)
|
||||
- 轨迹预测线让策略完全可预期,降低操作不确定性
|
||||
- 方向力度合一,操作极简
|
||||
|
||||
**操作循环(玩家回合)**:
|
||||
1. 鼠标悬停自己的弹珠 → 显示可拖拽提示
|
||||
2. 按住 + 反向拖拽 → 实时更新轨迹预测线 + 力度指示(百分比)
|
||||
3. 松开 → 弹珠弹射,物理模拟结算
|
||||
4. 结算完成 → 进入 NPC 回合
|
||||
|
||||
---
|
||||
|
||||
## 共识 5:输入设备 — 键鼠为主
|
||||
|
||||
**决策**:键鼠为主,手柄作为后续加分项(v0.2 不强求)。
|
||||
|
||||
**理由**:PC 单人独立开发,键鼠是合理默认。
|
||||
|
||||
---
|
||||
|
||||
## 共识 6:手法维度 — v0.2 不做
|
||||
|
||||
**决策**:v0.2 不实现"手法"(大拇指扣打法 / OK 手弹法)差异。手法作为四维策略之一,留后续版本。
|
||||
|
||||
**理由**:
|
||||
- 弹珠属性(大小/材质)已提供足够策略变化
|
||||
- 手法仅 2 种且差异小,v0.2 聚焦验证基础手感
|
||||
- 避免范围蔓延(每个版本验证一个核心假设)
|
||||
|
||||
**影响**:四维策略在 v0.2 实际降为三维(规则 × 场地 × 弹珠属性),手法维度在执行案中标注为"v0.3+ 预留"。
|
||||
|
||||
---
|
||||
|
||||
## 共识 7:弹珠库存 / 图鉴分离
|
||||
|
||||
**决策**:弹珠分两层管理:
|
||||
- **对战库存**:普通弹珠,参与对赌输赢,有保底机制
|
||||
- **收藏图鉴**:稀有花色弹珠,拾取即永久拥有,不参与输赢
|
||||
|
||||
**理由**:
|
||||
- 避免"辛苦收集的稀有弹珠在对战中输掉"的糟糕体验
|
||||
- 对战库存的输赢不影响收集成就
|
||||
|
||||
**影响保底机制**:保底针对"对战库存"(不会输到无法继续玩)。
|
||||
|
||||
---
|
||||
|
||||
## 推论:保底机制(待执行案细化数值)
|
||||
|
||||
基于共识 7,保底机制:玩家对战库存不低于某下限(建议 3 颗),低于则不再输弹珠(或获得基础补给)。具体数值在执行案给建议范围(标注"可调")。
|
||||
|
||||
---
|
||||
|
||||
## 路线图既定项(本次未讨论,直接沿用)
|
||||
|
||||
- **规则范围**:v0.2 只做"画圈弹"1 种规则
|
||||
- **NPC**:静态站位 + 简单 AI 决策作为弹珠对手(取死党 B 或小霸王)
|
||||
- **场地**:v0.2 做 1-2 种场地(水泥地 + 泥土地)
|
||||
- **蹲下系统**:角色控制器扩展(另出执行案)
|
||||
|
||||
---
|
||||
|
||||
## 待弹珠执行案细化的设计点(不需用户预先拍板,执行案给方案 + 建议值)
|
||||
|
||||
- 物理参数范围(弹珠半径、质量、摩擦、反弹系数)
|
||||
- 三维策略 DataAsset 结构(规则/场地/弹珠属性的配置字段)
|
||||
- NPC AI 决策规则(如何选规则/场地、弹射决策)
|
||||
- 保底机制具体数值
|
||||
- "画圈弹"规则的具体判定(圈大小、弹出判定、回合上限)
|
||||
- 局流程与胜负判定(一局的开始 → 进行 → 结算 → 退出)
|
||||
- 进入/退出小游戏的流程(场景探索 → 弹珠小游戏的视角/状态切换)
|
||||
- 跨系统交互(收集系统、交互系统、玩家控制器蹲下)
|
||||
|
||||
---
|
||||
|
||||
## 下一步
|
||||
|
||||
1. 落盘本共识(本文档)✅
|
||||
2. 加载 `design-execution` 技能,了解执行案规范
|
||||
3. 起草 `弹珠小游戏-执行案.md`(含上述设计点的完整方案 + 建议值)
|
||||
4. 同步产出配置表(弹珠属性 / 场地 / 规则参数)
|
||||
5. 起草 `蹲下系统-执行案.md`(玩家控制器扩展)
|
||||
6. 产出 `v02-范围.md`(版本边界汇总)
|
||||
7. 质量门审查
|
||||
@@ -0,0 +1,535 @@
|
||||
# 弹珠小游戏执行案(v0.2 原型)
|
||||
|
||||
## 迭代记录
|
||||
|
||||
| 迭代版本 | 修改日期 | 状态 | 说明 |
|
||||
|----------|----------|------|------|
|
||||
| v0.2.0 | 2026-06-15 | 📝 设计中 | 初版执行案,覆盖画圈弹规则、弹弓式操作、NPC对手、对战库存保底的完整行为规则。基于头脑风暴-2026-06-15 共识 |
|
||||
| v0.2.1 | 2026-06-15 | 📝 设计中 | 对抗性审查修订(25 项确认问题):补全物理参数语义(摩擦组合规则、弹射初速度、目标弹珠属性、碰撞恢复系数);泥土地去随机偏差改确定性差异;陶瓷补碰撞传递优势并加策略矩阵;统一平局/容差/射手弹出代价逻辑;明确 Interacting 内不位移;量化 NPC 力度参数;修正稀有弹珠归类与术语;新增平衡性验收标准 |
|
||||
| v0.2.2 | 2026-06-15 | 📝 设计中 | game-reviewer 审查修订:明确进入触发物注册点(=玩家起点=射手弹珠生成点)与触发半径(1.5m);MatchInit 射手弹珠选择改为 UI 列表 + 可重选;贪力反噬放回“初始起点”精确化;补充质量相对值基准脚注 |
|
||||
|
||||
> 状态标记:📝 设计中 | 🔄 开发中 | ✅ 已同步
|
||||
|
||||
---
|
||||
|
||||
## 系统概述
|
||||
|
||||
弹珠小游戏是 v0.2 的核心,验证游戏三大支柱之一"好玩"是否成立。
|
||||
|
||||
**验证假设**:弹珠小游戏的操作手感成立吗?策略深度够让人玩十几把不腻吗?
|
||||
|
||||
**体验目标**:操作手感直觉流畅,策略深度来自弹珠属性选择与场地差异的组合,而非操作精度。
|
||||
|
||||
**v0.2 范围**:1 种规则(画圈弹)、2 种场地(水泥地 / 泥土地)、3 种弹珠属性(玻璃 / 陶瓷 / 铁)、1 个 NPC 对手、与对战库存联动。
|
||||
|
||||
### 核心设计决定(基于头脑风暴共识,已确认)
|
||||
|
||||
1. **简化上手,策略为主** — 力度与方向用弹弓式拖拽一步完成,重复可玩性来自策略组合,不靠操作精度
|
||||
2. **斜俯视视角** — 45° 俯瞰场地,全局可见,瞄准直观
|
||||
3. **回合制** — 玩家与 NPC 轮流弹射
|
||||
4. **弹弓式拖拽操作** — 按住弹珠反向拖拽,方向与力度合一,配轨迹预测线
|
||||
5. **键鼠为主** — 手柄 v0.2 不做
|
||||
6. **手法维度 v0.2 不做** — 头脑风暴原定的四维策略在 v0.2 降为"弹珠属性选择 + 场地差异"两个玩家可感知维度,手法(拇指法 / OK 法)留 v0.3+
|
||||
7. **库存 / 进度追踪分离** — 对战库存(输赢流转)与收集系统进度追踪(永久记录)分开管理
|
||||
|
||||
---
|
||||
|
||||
## 系统边界
|
||||
|
||||
| 弹珠小游戏负责 | 弹珠小游戏不负责 |
|
||||
|----------------|------------------|
|
||||
| 弹珠小游戏的进入 / 退出流程 | 弹珠场地的场景美术搭建(关卡) |
|
||||
| 弹弓式操作的交互与反馈 | 弹珠作为收集品的进度展示(收集系统) |
|
||||
| 画圈弹规则判定与物理结算 | NPC 的日常行为 AI(v0.3) |
|
||||
| NPC 对手的弹射决策 | 多规则 / 多场地的扩展(v0.2 仅 1 规则 2 场地) |
|
||||
| 对战库存的输赢流转与保底 | 手法系统(v0.3+) |
|
||||
| 胜负判定与奖励结算 | 拍洋画、四驱车、FC 家用机、街机厅、包机房等其他小游戏品类(详见 `小游戏系统.md` 品类架构) |
|
||||
|
||||
---
|
||||
|
||||
## 进入与退出(与交互系统衔接)
|
||||
|
||||
弹珠小游戏作为交互系统的 **SubSystem**(交互系统执行案 v0.1 已预留该状态与 `postAction: minigame` 分叉)。
|
||||
|
||||
> **关键约束**:交互系统的 Interacting 状态规定"所有移动输入被忽略,角色不移动"。因此弹珠小游戏的进入流程**不依赖 Interacting 状态内的角色位移**——玩家触发交互时必须已站在场地边缘的触发范围内,进入流程只做转向 + 蹲下表现,不发生位移。
|
||||
|
||||
### 进入流程
|
||||
|
||||
| 步骤 | 行为 |
|
||||
|------|------|
|
||||
| 1 | 玩家在空地弹珠场地附近自由走动 |
|
||||
| 2 | 玩家**自行走动**到弹珠场地的交互触发范围内。**交互触发物注册点**位于圈外"玩家起点"(即射手弹珠生成点),**触发半径**复用交互系统建议值 1.5m(可调,范围 1.0-3.0m)。玩家就位点 = 玩家起点 = 射手弹珠生成点(三点重合),保证斜俯视相机能覆盖全场 |
|
||||
| 3 | 交互系统显示提示 `[E] 来一局弹珠` |
|
||||
| 4 | 玩家按 E → 触发"弹珠场地"交互,进入 Interacting 状态 |
|
||||
| 5 | 角色原位转向场地并蹲下(就位表现,时长 0.8 秒,建议值,可调,范围 0.5-1.2 秒)。**角色不发生位移**,仅在原地完成转向 + 下蹲 |
|
||||
| 6 | 交互结束,postAction 为 minigame → 角色进入 SubSystem 状态 |
|
||||
| 7 | 进入过渡:相机从第三人称平滑切换到斜俯视(俯瞰场地),切换时长 0.6 秒(建议值,可调,范围 0.4-1.0 秒) |
|
||||
| 8 | 显示弹珠场地全貌:圈、中立目标弹珠、玩家射手弹珠、NPC 射手弹珠 |
|
||||
| 9 | 进入 MatchInit(局准备)状态 |
|
||||
|
||||
### 退出流程
|
||||
|
||||
| 步骤 | 行为 |
|
||||
|------|------|
|
||||
| 1 | MatchEnd 结算完成,玩家确认 → 相机从斜俯视切回第三人称,切换时长 0.6 秒(建议值,可调,范围 0.4-1.0 秒) |
|
||||
| 2 | 角色原位站起(站起表现时长 0.8 秒,建议值,可调,范围 0.5-1.2 秒) |
|
||||
| 3 | 弹珠小游戏 SubSystem 退出 → 回到交互系统 Free 状态,玩家恢复自由移动 |
|
||||
| 4 | 玩家可再次走近场地交互,开启新一局 |
|
||||
|
||||
---
|
||||
|
||||
## 状态定义
|
||||
|
||||
弹珠小游戏内部状态机(在交互系统 SubSystem 顶层状态之下):
|
||||
|
||||
| 状态 | 说明 |
|
||||
|------|------|
|
||||
| MatchInit | 局准备:生成场地弹珠,玩家选择射手弹珠 |
|
||||
| PlayerAim | 玩家瞄准:等待玩家按住射手弹珠开始拖拽 |
|
||||
| PlayerCharge | 玩家蓄力:拖拽中,实时更新方向 / 力度 / 预测线 |
|
||||
| Simulating | 物理结算:弹珠运动中,等待全部停止 |
|
||||
| NpcThink | NPC 思考:短暂延迟后 NPC 决策并弹射 |
|
||||
| RoundEnd | 回合结算:判定弹出弹珠,更新射手弹珠位置 |
|
||||
| MatchEnd | 局结束:胜负判定、弹珠流转、显示结果 |
|
||||
|
||||
### 状态转换
|
||||
|
||||
```
|
||||
MatchInit ──(玩家选好射手弹珠 + 按空格)──→ PlayerAim(玩家先手)
|
||||
PlayerAim ──(按住射手弹珠)──→ PlayerCharge
|
||||
PlayerCharge ──(松开 + 力度 ≥ 最小有效值)──→ Simulating
|
||||
PlayerCharge ──(松开 + 力度 < 最小有效值 或 按 ESC / 右键)──→ PlayerAim
|
||||
Simulating ──(所有弹珠停止)──→ RoundEnd
|
||||
RoundEnd ──(圈内仍有目标弹珠)──→ 切换回合:下一方 PlayerAim 或 NpcThink
|
||||
RoundEnd ──(圈内目标弹珠全部弹出)──→ MatchEnd
|
||||
NpcThink ──(思考延迟结束 + NPC 弹射)──→ Simulating
|
||||
MatchEnd ──(结算完成 + 玩家确认)──→ 退出流程
|
||||
```
|
||||
|
||||
**回合顺序**:v0.2 固定玩家先手。每局均由玩家先弹。
|
||||
|
||||
---
|
||||
|
||||
## 行为规则
|
||||
|
||||
### 一、局准备(MatchInit)
|
||||
|
||||
**触发**:进入弹珠小游戏 SubSystem、相机过渡完成后。
|
||||
|
||||
**表现**:
|
||||
- 圈中央生成 5 颗中立目标弹珠(建议值,可调,范围 3-8 颗),按紧凑圆形排列于圈心周围。目标弹珠的物理属性等同于"玻璃弹珠(标准)"(见"弹珠物理属性")
|
||||
- 玩家射手弹珠生成于圈外"玩家起点"位置
|
||||
- NPC 射手弹珠生成于圈外"NPC 起点位置"(与玩家起点关于圈心对称,位于圈的对侧)
|
||||
- 屏幕显示"选择你的射手弹珠"界面(**UI 列表面板**,非 3D 场景操作):列出玩家对战库存中所有弹珠,标注每颗的属性(玻璃 / 陶瓷 / 铁)与一句话策略定位
|
||||
- 玩家**鼠标点击列表项**选择射手弹珠,点选后该项高亮为"当前选中"。玩家可**反复点击不同弹珠切换选择**(最后一次点选生效),场上射手弹珠属性实时跟随当前选中项更新
|
||||
- 显示提示 `按 空格 开始`。玩家按空格**确认**当前选中弹珠 → 界面收起,进入 PlayerAim
|
||||
|
||||
**结束条件**:玩家按空格 → 进入 PlayerAim。
|
||||
|
||||
**边界**:玩家对战库存因保底机制保证 ≥ 3 颗,始终能选择射手弹珠。若因异常库存为空,强制使用系统基础弹珠(属性等同于"玻璃弹珠(标准)"),不阻断游戏。
|
||||
|
||||
---
|
||||
|
||||
### 二、玩家瞄准(PlayerAim)
|
||||
|
||||
**触发**:从 MatchInit 或 RoundEnd(轮到玩家)进入。
|
||||
|
||||
**表现**:
|
||||
- 玩家射手弹珠高亮(脉冲发光,频率 1Hz,建议值,可调,范围 0.5-2Hz)
|
||||
- 鼠标悬停射手弹珠 → 鼠标显示"可拖拽"提示光标
|
||||
- 屏幕底部显示操作提示:`按住弹珠拖拽瞄准 ESC 放弃本局`
|
||||
|
||||
**结束条件**:
|
||||
- 玩家在射手弹珠上按下鼠标左键 → 进入 PlayerCharge
|
||||
- 玩家按 ESC → 弹出确认"放弃本局?将判负" → 确认则按"玩家放弃"处理进入 MatchEnd(见 MatchEnd 弹珠流转),取消则留在 PlayerAim
|
||||
|
||||
**边界**:此状态无超时,玩家可无限思考。Simulating / PlayerCharge 中按 ESC 无效(操作不可中断)。
|
||||
|
||||
---
|
||||
|
||||
### 三、玩家蓄力(PlayerCharge)— 弹弓式拖拽核心
|
||||
|
||||
**触发**:PlayerAim 中玩家按住射手弹珠(鼠标左键按下)。
|
||||
|
||||
**表现**:
|
||||
- 锁定瞄准原点为射手弹珠当前位置
|
||||
- 显示力度指示器(射手弹珠旁,0%-100%)
|
||||
- 显示轨迹预测线:从射手弹珠沿"拖拽反方向"延伸的虚线弧,长度按力度比例(最长显示 3m 路径,建议值,可调,范围 2-5m)
|
||||
|
||||
**拖拽规则**:
|
||||
- **弹射方向** = 射手弹珠 → 鼠标当前位置 的反方向(鼠标向左下拖,弹珠向右上飞)
|
||||
- **力度** = 拖拽距离的线性映射:拖拽距离 0 → 力度 0%;拖拽距离达到最大值 → 力度 100%
|
||||
- **最大拖拽距离**:以屏幕高度的百分比衡量,拖拽距离达到屏幕高度的 15%(建议值,可调,范围 8%-25%)时力度为 100%。使用屏幕高度百分比保证不同分辨率下手感一致
|
||||
- 鼠标移动时,方向 / 力度 / 预测线实时刷新,无延迟
|
||||
|
||||
**轨迹预测线规则**:
|
||||
- 预测线反映弹珠弹出后的直线路径,考虑场地摩擦导致的减速(预测线随摩擦大小呈现不同长度与弧度)
|
||||
- 预测线**不显示碰撞后的路径变化**(碰撞后路径不可预测)
|
||||
- 预测线长度随力度增加而延长,达到最大显示长度后封顶
|
||||
- 预测线在所有场地都如实反映摩擦差异(见"场地"章节),泥土地的预测线因高摩擦而明显更短
|
||||
|
||||
**取消蓄力**:
|
||||
- 玩家按 ESC 或鼠标右键 → 回到 PlayerAim(不消耗回合)
|
||||
- 玩家松开鼠标左键 且 力度 < 最小有效值(建议 10%,可调,范围 5%-20%)→ 视为取消,回到 PlayerAim
|
||||
|
||||
**结束条件(确认弹射)**:
|
||||
- 玩家松开鼠标左键 且 力度 ≥ 最小有效值 → 进入 Simulating,弹珠按当前方向与力度弹射,预测线消失
|
||||
|
||||
**边界**:
|
||||
- 拖拽距离超过最大值时,力度封顶 100%,预测线不再延长
|
||||
- 鼠标拖至屏幕边缘,方向与力度按实际拖拽向量计算(拖拽距离仍以屏幕高度百分比为基准)
|
||||
|
||||
---
|
||||
|
||||
### 四、物理结算(Simulating)
|
||||
|
||||
**触发**:玩家或 NPC 弹射后。
|
||||
|
||||
**表现**:
|
||||
- 弹射手弹珠按弹射方向与初速度开始物理运动。初速度 = 力度百分比 × 最大弹射初速度(见"可调参数")。初速度对所有弹珠相同,弹珠属性差异体现在后续的滚动(摩擦)与碰撞(质量、恢复系数)阶段
|
||||
- 预测线在弹射瞬间消失
|
||||
|
||||
**运动规则**:
|
||||
- **摩擦减速**:弹珠受摩擦持续减速,摩擦数值越大减速越快。最终摩擦 = 场地摩擦 + 弹珠摩擦加成(见"场地"与"弹珠物理属性")
|
||||
- **弹珠碰撞**:弹珠碰撞其他弹珠时按物理规律传递动量,产生反弹与位移。碰撞中目标弹珠被弹飞的程度取决于射手弹珠的碰撞恢复系数(陶瓷高,传递能量多)
|
||||
- **场地边界反弹**:弹珠碰撞场地隐形边界时反弹,反弹程度由反弹系数决定(见"可调参数")
|
||||
- **目标弹珠与射手弹珠一视同仁参与碰撞**:停在圈内的射手弹珠作为物理实体,会与目标弹珠和对方射手弹珠发生碰撞
|
||||
|
||||
**结束条件**:
|
||||
- 场上所有运动弹珠的速度低于停止阈值(建议 0.05m/s,可调,范围 0.02-0.1m/s)且持续稳定时间 1 秒(建议值,可调,范围 0.5-2 秒)→ 判定全部停止,进入 RoundEnd
|
||||
- **超时保护**:物理结算超过 15 秒(建议值,可调,范围 10-30 秒)仍有弹珠未停止 → 强制停止所有弹珠,进入 RoundEnd
|
||||
|
||||
---
|
||||
|
||||
### 五、回合结算(RoundEnd)
|
||||
|
||||
**触发**:Simulating 结束(全部弹珠停止)。
|
||||
|
||||
**结算内容**:
|
||||
|
||||
| 结算项 | 规则 |
|
||||
|--------|------|
|
||||
| 弹出判定 | 逐颗检查目标弹珠位置。弹珠中心点到圈心距离 > 圈半径 + 弹出判定容差 → 判定为"被弹出"。**弹出判定始终使用带容差的阈值**(与边界情况"弹珠停在圈边界"一致) |
|
||||
| 弹出归属 | 被弹出的目标弹珠归本回合弹射方所有,计入本局收获 |
|
||||
| 射手弹珠位置(停在圈内) | 保持当前位置,下一回合从该位置继续弹射。停在圈内的射手弹珠作为物理障碍有双向影响:既可能阻挡对方弹珠(防守收益),也可能挡住自己的后续弹道(位置风险)——这是策略的一部分 |
|
||||
| 射手弹珠位置(弹出圈外) | **贪力反噬**:本回合该方弹出的目标弹珠全部作废(不计入本局收获),射手弹珠放回圈外该方**初始起点位置**(非上回合停留位置)。此规则防止"大力清场而无代价"的漏洞 |
|
||||
| 收获记录 | 本回合实际计入的收获 = 弹出的目标弹珠数(若触发贪力反噬则为 0) |
|
||||
|
||||
**结束条件**:
|
||||
- 圈内仍有目标弹珠 → 切换回合:刚才是玩家回合 → NpcThink;刚才是 NPC 回合 → PlayerAim
|
||||
- 圈内目标弹珠全部被弹出(剩余 0 颗)→ 进入 MatchEnd
|
||||
|
||||
---
|
||||
|
||||
### 六、NPC 回合(NpcThink)
|
||||
|
||||
**触发**:RoundEnd 后轮到 NPC。
|
||||
|
||||
**表现**:
|
||||
- NPC 射手弹珠高亮
|
||||
- NPC"思考"延迟 1.0 秒(建议值,可调,范围 0.5-2.0 秒),期间无操作(模拟思考)
|
||||
- 思考结束后,NPC 按决策规则自动弹射 → 进入 Simulating
|
||||
|
||||
**NPC 决策规则**:
|
||||
1. 按策略倾向选择目标弹珠(死党 B:选距离 NPC 射手弹珠最近的目标弹珠)
|
||||
2. 计算瞄准方向(NPC 射手弹珠 → 目标弹珠)
|
||||
3. 按距离估算力度:力度 = 基础力度 + 距离补偿系数 × 距离。基础力度 50%(建议值,可调,范围 30%-70%),距离补偿系数每米 +15%(建议值,可调,范围 10%-25%/m)。估算结果截断在 0%-100%
|
||||
4. 加入准度偏差与力度误差(取自 NPC 属性配置,死党 B:角度偏差 ±3°、力度误差 ±10%,详见"NPC 对手"章节)
|
||||
5. 弹射,进入 Simulating
|
||||
|
||||
NPC 的射手弹珠属性同样影响物理表现(死党 B 用玻璃弹珠标准属性)。
|
||||
|
||||
---
|
||||
|
||||
### 七、局结束与结算(MatchEnd)
|
||||
|
||||
**触发**:RoundEnd 判定圈内目标弹珠全部弹出,或玩家放弃本局。
|
||||
|
||||
**结算内容**:
|
||||
|
||||
| 步骤 | 规则 |
|
||||
|------|------|
|
||||
| 1. 胜负判定 | 比较双方本局实际收获的目标弹珠数。玩家 > NPC → 玩家胜;玩家 < NPC → NPC 胜;玩家 = NPC → 由本局最后弹出目标弹珠的一方胜("最后弹出方"由状态机记录的最近一次 Simulating 触发方唯一确定,不存在无法判定的情形) |
|
||||
| 2. 玩家放弃的特殊处理 | 若本局因玩家按 ESC 放弃而结束 → 跳过收获比较,直接判定为玩家败。玩家放弃前已收获的目标弹珠全部作废(不计入收集、不参与流转) |
|
||||
| 3. 弹珠流转(玩家胜) | 玩家对战库存 +对赌弹珠数(从 NPC 处赢得) |
|
||||
| 4. 弹珠流转(玩家败) | 触发保底:当前库存 > 保底下限 → 库存 -对赌弹珠数;当前库存 ≤ 保底下限 → 库存不变(仅本局不计收获) |
|
||||
| 5. 稀有弹珠奖励 | 若本局实际收获中包含稀有弹珠,通知物品系统(见"跨系统交互"),计入收集系统进度 |
|
||||
| 6. 结果显示 | 屏幕中央显示结果 2.0 秒(建议值,可调,范围 1.5-3.0 秒) |
|
||||
|
||||
**结果显示文本**:
|
||||
- 胜:`你赢了!获得 1 颗弹珠`
|
||||
- 败(正常扣减):`你输了…失去 1 颗弹珠`
|
||||
- 败(触发保底):`你输了…好在没失去弹珠`
|
||||
- 玩家放弃:`你放弃了这局`
|
||||
|
||||
**结束条件**:结果显示后玩家按空格 / E 确认 → 进入退出流程。
|
||||
|
||||
> **设计说明**:v0.2 采用"恒有胜负"(收获相等时由最后弹出方决胜),不设真实平局终态。理由:画圈弹弹完全部目标弹珠时,必然存在最后弹出一颗的一方,状态机可唯一确定,无需平局分支。
|
||||
|
||||
---
|
||||
|
||||
## 画圈弹规则配置
|
||||
|
||||
| 配置项 | 建议值 | 可调范围 | 说明 |
|
||||
|--------|--------|----------|------|
|
||||
| 圈半径 | 1.0m | 0.8-1.5m | 弹珠被弹出圈的判定边界 |
|
||||
| 目标弹珠数 | 5 颗 | 3-8 颗 | 圈中央初始放置 |
|
||||
| 目标弹珠排列 | 紧凑圆形 | — | 圈心周围小范围 |
|
||||
| 对赌弹珠数 | 1 颗 | 1-3 颗 | 胜负结算时的流转单位 |
|
||||
| 回合顺序 | 玩家先手 | — | v0.2 固定 |
|
||||
| 弹出判定容差 | 0.01m | 0.005-0.02m | 圈边界判定容差,避免抖动误判。**所有弹出判定统一使用 圈半径 + 容差 阈值** |
|
||||
| 场地边界外扩 | 0.5m | 0.3-1.0m | 场地周围隐形反弹边界 |
|
||||
|
||||
---
|
||||
|
||||
## 三维策略系统(v0.2 实现)
|
||||
|
||||
> **如实说明**:v0.2 中玩家可感知的策略维度有两个——**弹珠属性选择**(玩家在 MatchInit 选射手弹珠)与**场地差异**(玩家进入哪个场地就用哪个场地的物理参数)。原四维策略中的"规则"维度 v0.2 冻结(仅画圈弹,无选择空间),"手法"维度 v0.2 不做,二者作为系统结构预留,待 v0.3+ 增加内容后激活。本章节沿用"三维策略系统"标题指代系统结构上的三个配置维度(规则 / 场地 / 弹珠属性),但明确 v0.2 玩家策略实际来自弹珠属性与场地两个维度。
|
||||
|
||||
### 维度一:规则(v0.2 冻结)
|
||||
|
||||
v0.2 仅"画圈弹"一种规则,NPC 无规则选择空间,玩家也无规则选择。此维度在 v0.2 不产生策略变化,结构上预留,v0.3+ 增加规则类型后激活。
|
||||
|
||||
### 维度二:场地
|
||||
|
||||
v0.2 提供 2 种场地,通过**确定性的摩擦差异**影响弹珠滚动表现(不使用随机偏差,保证预测线如实反映场地特性)。场地由关卡决定(v0.2 空地有 2 个弹珠场地,分别使用水泥地与泥土地参数)。
|
||||
|
||||
| 场地 | 场地摩擦(建议值) | 可调范围 | 滚动表现 | 策略影响 |
|
||||
|------|---------------------|----------|----------|----------|
|
||||
| 水泥地 | 0.3 | 0.2-0.5 | 摩擦低,弹珠滚动均匀、距离远、走线可预测 | 适合远距离精准操作,玻璃弹珠的射程优势充分发挥 |
|
||||
| 泥土地 | 0.6 | 0.4-0.8 | 摩擦高,弹珠减速更快、滚动距离明显更短,需要更大力度才能到达圈心 | 玻璃弹珠射程被压缩,需贴近距离;陶瓷的碰撞优势更易发挥 |
|
||||
|
||||
**摩擦组合规则**:弹珠在场地上的最终摩擦 = 场地摩擦 + 弹珠摩擦加成。摩擦数值越大,弹珠减速越快、滚动距离越短。具体衰减计算由技术方案定义,本执行案只定义摩擦的语义、组合规则与数值。
|
||||
|
||||
### 维度三:弹珠属性(玩家选择)
|
||||
|
||||
玩家在 MatchInit 选择射手弹珠,不同属性影响物理表现。v0.2 提供 3 种基础属性组合。**目标弹珠统一使用"玻璃弹珠(标准)"属性**。
|
||||
|
||||
| 弹珠类型 | 半径(建议值) | 质量(相对值) | 摩擦加成 | 碰撞恢复系数 | 策略定位 |
|
||||
|----------|----------------|----------------|----------|--------------|----------|
|
||||
| 玻璃弹珠(标准) | 0.015m(可调,范围 0.01-0.02m) | 1.0(可调,范围 0.8-1.2) | 0.0(可调,范围 0.0-0.2) | 0.5(可调,范围 0.4-0.7) | 摩擦低、滚得远、走线精准,远距离射程的主力选择 |
|
||||
| 陶瓷弹珠(粗糙) | 0.015m(可调,范围 0.01-0.02m) | 1.0(可调,范围 0.8-1.2) | 0.3(可调,范围 0.2-0.5) | 0.8(可调,范围 0.7-0.9) | 摩擦高、滚得近,但碰撞恢复系数高,碰撞时把目标弹珠弹得更远,适合近距离把目标撞出圈 |
|
||||
| 铁弹珠(重) | 0.012m(可调,范围 0.008-0.015m) | 2.5(可调,范围 2.0-3.0) | 0.1(可调,范围 0.0-0.3) | 0.6(可调,范围 0.5-0.8) | 质量大、碰撞动量大,能把多颗目标弹珠撞散,适合清场;注意贪力反噬规则,射手弹出会作废本回合收获 |
|
||||
|
||||
**碰撞恢复系数语义**:弹珠碰撞目标弹珠时,目标弹珠被弹飞的位移程度与射手弹珠的碰撞恢复系数正相关。陶瓷最高(0.8,碰撞传递能量多)、铁次之(0.6)、玻璃最低(0.5)。这是陶瓷相对玻璃的核心差异化优势,补偿其滚动距离近的劣势。
|
||||
|
||||
**碰撞推力**:弹珠碰撞的总动量由质量决定(铁弹珠质量大,碰撞推力强),按物理规律自动计算。
|
||||
|
||||
> **质量基准**:质量为相对单位,玻璃弹珠质量 1.0 为基准(实际物理质量由技术方案按基准换算),其他弹珠质量为相对玻璃的倍数。
|
||||
|
||||
### 策略矩阵(3 种弹珠 × 2 种场地)
|
||||
|
||||
| 弹珠 \ 场地 | 水泥地(摩擦 0.3,滚得远) | 泥土地(摩擦 0.6,滚得近) |
|
||||
|--------------|----------------------------|----------------------------|
|
||||
| 玻璃 | 摩擦叠加 0.3,射程最远,精准弹出远距目标,**主力场景** | 摩擦叠加 0.6,射程压缩,需更大力度,精准优势仍在 |
|
||||
| 陶瓷 | 摩擦叠加 0.6,滚得近,靠高碰撞恢复系数近距离把目标撞出 | 摩擦叠加 0.9,滚得最近,必须贴近目标,碰撞优势最大化发挥 |
|
||||
| 铁 | 摩擦叠加 0.4,质量大清场,大力撞散目标群,警惕贪力反噬 | 摩擦叠加 0.7,减速快,需贴近距离发挥质量碰撞,警惕贪力反噬 |
|
||||
|
||||
> 矩阵中"摩擦叠加"= 场地摩擦 + 弹珠摩擦加成,数值越小弹珠滚得越远。
|
||||
|
||||
---
|
||||
|
||||
## 平衡性验收标准
|
||||
|
||||
用于白盒调参阶段判断三维策略是否成立。所有数值为建议值,调参时以此为目标逼近:
|
||||
|
||||
| 验收项 | 判据 |
|
||||
|--------|------|
|
||||
| 无死选项 | 3 种弹珠在 2 种场地共 6 种组合下,每种组合都存在一个"最优力度区间"使玩家能弹出至少 1 颗目标弹珠,无任何组合完全无法弹出 |
|
||||
| 策略可感知 | 换弹珠后,玩家弹出同等距离目标所需的"最优力度"应有可感知差异(建议玻璃最优力度明显低于陶瓷,陶瓷低于铁) |
|
||||
| 场地可感知 | 同一弹珠在水泥地与泥土地,玩家应感知到需要明显更大的力度才能达到相同射程 |
|
||||
| 无碾压选项 | 3 种弹珠对同一 NPC 的胜率分布应落在合理区间(建议目标 40%-60%,通过白盒调参逼近,无某种弹珠胜率显著碾压) |
|
||||
| 弹珠定位成立 | 玻璃的远距精准、陶瓷的近距强撞、铁的清场,三者应各有适用场景,不存在某弹珠在所有场地下都最优 |
|
||||
|
||||
---
|
||||
|
||||
## 弹珠库存与收集进度
|
||||
|
||||
### 对战库存(弹珠小游戏管理)
|
||||
|
||||
| 项目 | 规则 |
|
||||
|------|------|
|
||||
| 初始数量 | 5 颗(建议值,可调,范围 3-10 颗) |
|
||||
| 弹珠构成 | 玻璃 / 陶瓷 / 铁混合,玩家选射手弹珠时能看到每颗属性与策略定位 |
|
||||
| 射手弹珠归属 | 每局从库存选 1 颗作为射手弹珠,库存总数不变,用完归还 |
|
||||
| 输赢流转 | 按对赌单位 ±库存,受保底机制约束 |
|
||||
| 保底下限 | 3 颗(建议值,可调,范围 2-5 颗),玩家败时库存不会低于此值 |
|
||||
|
||||
### 收集系统进度追踪(收集系统管理,v0.2 打通通知链路)
|
||||
|
||||
| 项目 | 规则 |
|
||||
|------|------|
|
||||
| 归属 | 稀有弹珠通过收集系统的"获得物品"通知链路计入收集进度(v0.2 收集系统仅有进度追踪与收集通知能力,**无独立图鉴数据结构**,图鉴 UI 留 v0.4 收集系统完整版) |
|
||||
| v0.2 稀有弹珠 | 仅"玻璃弹珠(猫眼)"1 种(与收集系统杂物清单的稀有归类一致;"弹珠(透明冰花)"在收集系统中归类为普通,不作为稀有弹珠)。后续版本扩展更多稀有花色 |
|
||||
| v0.2 链路 | 弹珠小游戏中赢得稀有弹珠 → 通知物品系统 → 收集系统判定新收集 → 弹出收集通知,计入杂物收集进度 |
|
||||
| 通知载荷 | 通知须携带三字段:物品名称(稀有弹珠名)+ 品类("杂物")+ 物品内部编号。稀有弹珠须预先注册进收集系统的杂物清单 |
|
||||
| 完整展示 | v0.2 仅打通通知链路与进度计入,图鉴完整 UI 留 v0.4 |
|
||||
|
||||
### 库存与收集进度的区分(边界)
|
||||
|
||||
- 对战库存的输赢(普通弹珠)不影响收集进度,已收集的稀有弹珠不会因输局丢失
|
||||
- 收集进度的弹珠不直接进入对战库存计数(玩家主动将稀有弹珠"投入对战"的功能 v0.2 不做,留 v0.3+)
|
||||
|
||||
---
|
||||
|
||||
## NPC 对手(v0.2)
|
||||
|
||||
v0.2 提供 1 个 NPC 对手。默认采用死党 B(叙事框架角色列表中"家境差但乐观,弹珠高手"),小霸王作为备选配置。
|
||||
|
||||
| 配置项 | 死党 B(默认) | 小霸王(备选) | 说明 |
|
||||
|--------|----------------|----------------|------|
|
||||
| 准度(角度偏差) | ±3°(可调,范围 ±1°-±10°) | ±8°(可调,范围 ±1°-±10°) | 偏差越小越准 |
|
||||
| 力度误差 | ±10%(可调,范围 ±5%-±25%) | ±20%(可调,范围 ±5%-±25%) | 力度偏差 |
|
||||
| 基础力度 | 50%(可调,范围 30%-70%) | 60%(可调,范围 30%-70%) | 力度估算公式基数 |
|
||||
| 距离补偿系数 | 每米 +15%(可调,范围 10%-25%/m) | 每米 +15%(可调,范围 10%-25%/m) | 距离远则加大力度 |
|
||||
| 策略倾向 | 瞄准最近目标弹珠 | 瞄准距圈边缘最近、最易弹出的目标 | 目标选择方式 |
|
||||
| 射手弹珠属性 | 玻璃弹珠(标准) | 铁弹珠(重) | NPC 射手弹珠沿用对应玩家弹珠属性表的数值 |
|
||||
|
||||
**默认选择理由**:死党 B 准度高但力度稳健,让玩家前期有挑战又能赢,体验友好,契合叙事框架中"用便宜弹珠也能赢的高手"定位。
|
||||
|
||||
---
|
||||
|
||||
## 边界情况处理
|
||||
|
||||
### 玩家对战库存耗尽
|
||||
保底机制保证库存 ≥ 3 颗,玩家总能选到射手弹珠。若因异常库存为空,MatchInit 强制使用系统基础弹珠(属性等同于"玻璃弹珠(标准)"),不阻断游戏。
|
||||
|
||||
### 弹珠停在圈边界
|
||||
弹出判定统一使用带容差阈值:弹珠中心点到圈心距离 > 圈半径 + 容差 → 判定弹出;≤ 圈半径 + 容差 → 视为圈内(未弹出)。容差避免边界抖动导致的误判。此规则在 RoundEnd 弹出判定与边界情况中完全一致。
|
||||
|
||||
### 弹珠飞向场地边界
|
||||
场地周围有隐形边界(场地外扩 0.5m),弹珠碰撞后按反弹系数反弹回场地内,防止飞到不可达位置。
|
||||
|
||||
### 所有目标弹珠同一回合全部弹出
|
||||
RoundEnd 直接判定圈内剩余 0 颗 → 进入 MatchEnd,正常结算。
|
||||
|
||||
### 玩家中途退出
|
||||
玩家在 PlayerAim 按 ESC → 弹出"放弃本局?将判负"确认 → 确认则按"玩家放弃"处理(MatchEnd 跳过收获比较直接判玩家败,已收获弹珠作废),进入退出流程。PlayerCharge / Simulating 中 ESC 无效。
|
||||
|
||||
### NPC 弹射后弹珠几乎不动(力度极小)
|
||||
仍走 Simulating → 快速停止 → RoundEnd → 切换回合,正常流转,不判定为异常。
|
||||
|
||||
### 物理结算卡死(弹珠持续微小运动)
|
||||
超时保护 15 秒强制停止所有弹珠,进入 RoundEnd。
|
||||
|
||||
### 收获相等时的胜负
|
||||
由状态机记录的最近一次 Simulating 触发方(最后弹出方)决胜,不存在无法判定的情形。
|
||||
|
||||
---
|
||||
|
||||
## 跨系统交互
|
||||
|
||||
### 与交互系统(入口 / 出口)
|
||||
- 弹珠小游戏作为交互系统的 SubSystem
|
||||
- **进入**:玩家自行走到场地边缘触发范围内 → 交互系统"弹珠场地"交互物 → 原位转向 + 蹲下表现(不位移)→ postAction: minigame → SubSystem
|
||||
- **退出**:MatchEnd 结算 + 玩家确认 → 原位站起(不位移)→ 退出 SubSystem → 交互系统回 Free 状态
|
||||
- 进入 / 退出全程不在交互系统 Interacting 状态内移动角色,符合交互系统"Interacting 内角色不移动"的规则
|
||||
- 弹珠小游戏内部使用独立的鼠标拖拽输入
|
||||
|
||||
### 与玩家控制器(视角与蹲下)
|
||||
- **进入时**:角色蹲下(蹲下系统),相机切斜俯视
|
||||
- **退出时**:角色站起,相机回第三人称
|
||||
- 弹珠小游戏进行中角色不移动,玩家控制器的移动输入被忽略
|
||||
|
||||
### 与收集系统 / 物品系统(弹珠流转)
|
||||
- 玩家赢得稀有弹珠 → 弹珠小游戏通知物品系统,载荷为三字段:物品名称(稀有弹珠名)+ 品类("杂物")+ 物品内部编号 → 物品系统弹出拾取通知 → 收集系统判定新收集弹出收集通知(计入杂物收集进度)
|
||||
- 对战库存的普通弹珠输赢不经过物品系统,由弹珠小游戏内部管理
|
||||
- 弹珠小游戏不直接与收集系统通信,通过物品系统中转(与现有架构一致)
|
||||
- 稀有弹珠须预先注册进收集系统的杂物清单("玻璃弹珠(猫眼)"已存在于收集系统杂物清单并标为稀有)
|
||||
|
||||
### 与蹲下系统
|
||||
- 弹珠小游戏入场使用蹲下表现(角色蹲在场地边操作弹珠),调用蹲下系统的"强制蹲下"接口
|
||||
- 退出时调用蹲下系统的"强制站起"接口
|
||||
- 蹲下系统的独立行为规则(蹲伏移动 / 待机 / 切换)见 `蹲下系统-执行案.md`
|
||||
|
||||
---
|
||||
|
||||
## v0.2 不实现清单
|
||||
|
||||
| 功能 | 排除原因 |
|
||||
|------|----------|
|
||||
| 多规则(打坑 / 靠墙 / 斜坡) | v0.2 仅画圈弹验证核心,多规则留 v0.3+ |
|
||||
| 手法系统(拇指法 / OK 法) | 四维降维,手法差异小,留 v0.3+ |
|
||||
| 手柄操作 | 键鼠为主,手柄留后续 |
|
||||
| 收集图鉴完整 UI | 留 v0.4 收集系统完整版 |
|
||||
| 多 NPC 对手切换 | v0.2 固定 1 个对手 |
|
||||
| NPC 日常 AI / 移动 | 留 v0.3(v0.2 NPC 仅静态站位 + 弹射 AI) |
|
||||
| 拍洋画、四驱车、FC 家用机、街机厅、包机房、肢体活动类等其他小游戏品类 | 留后续版本(详见 `小游戏系统.md` 品类架构) |
|
||||
| 弹珠花色外观美术差异 | v0.2 白盒,弹珠用颜色区分属性 |
|
||||
| 玩家投入稀有弹珠对战 | 留 v0.3+ |
|
||||
| 回合数上限 / 主动认输系统 | v0.2 弹完即结束,无回合上限 |
|
||||
| 音效(弹射 / 碰撞 / 结算) | 留 v0.3 |
|
||||
|
||||
---
|
||||
|
||||
## 可调参数汇总
|
||||
|
||||
| 参数 | 建议值 | 可调范围 | 说明 |
|
||||
|------|--------|----------|------|
|
||||
| 就位表现时长 | 0.8 秒 | 0.5-1.2 秒 | 进入时角色原位转向 + 蹲下 |
|
||||
| 进入相机切换时长 | 0.6 秒 | 0.4-1.0 秒 | 第三人称 → 斜俯视 |
|
||||
| 退出相机切换时长 | 0.6 秒 | 0.4-1.0 秒 | 斜俯视 → 第三人称 |
|
||||
| 站起表现时长 | 0.8 秒 | 0.5-1.2 秒 | 退出时角色站起 |
|
||||
| 射手弹珠高亮频率 | 1Hz | 0.5-2Hz | PlayerAim 脉冲发光 |
|
||||
| 最大拖拽距离(屏幕高度占比) | 15% | 8%-25% | 力度 100% 对应的拖拽距离,相对屏幕高度 |
|
||||
| 最小有效力度 | 10% | 5%-20% | 低于此松开视为取消 |
|
||||
| 轨迹预测线最大显示长度 | 3m | 2-5m | 预测线最长路径 |
|
||||
| 最大弹射初速度(100% 力度) | 8 m/s | 5-12 m/s | 力度 100% 时弹珠出膛初速度,对所有弹珠相同 |
|
||||
| 圈半径 | 1.0m | 0.8-1.5m | 弹出判定边界 |
|
||||
| 目标弹珠数 | 5 颗 | 3-8 颗 | 圈中央初始放置 |
|
||||
| 对赌弹珠数 | 1 颗 | 1-3 颗 | 胜负流转单位 |
|
||||
| 弹出判定容差 | 0.01m | 0.005-0.02m | 圈边界判定(统一带容差) |
|
||||
| 场地边界外扩 | 0.5m | 0.3-1.0m | 隐形反弹边界 |
|
||||
| 初始对战库存 | 5 颗 | 3-10 颗 | 玩家起始弹珠数 |
|
||||
| 保底下限 | 3 颗 | 2-5 颗 | 库存不低于此值 |
|
||||
| 弹珠停止速度阈值 | 0.05m/s | 0.02-0.1m/s | 判定停止 |
|
||||
| 稳定时间 | 1 秒 | 0.5-2 秒 | 低于阈值持续至此判定停止 |
|
||||
| 超时保护 | 15 秒 | 10-30 秒 | 物理结算强制停止 |
|
||||
| 水泥地摩擦 | 0.3 | 0.2-0.5 | 场地摩擦(低) |
|
||||
| 泥土地摩擦 | 0.6 | 0.4-0.8 | 场地摩擦(高) |
|
||||
| 玻璃弹珠半径 | 0.015m | 0.01-0.02m | 标准弹珠 |
|
||||
| 玻璃弹珠质量 | 1.0 | 0.8-1.2 | 相对单位 |
|
||||
| 玻璃弹珠摩擦加成 | 0.0 | 0.0-0.2 | 低摩擦 |
|
||||
| 玻璃弹珠碰撞恢复系数 | 0.5 | 0.4-0.7 | 碰撞传递中等 |
|
||||
| 陶瓷弹珠半径 | 0.015m | 0.01-0.02m | 标准弹珠 |
|
||||
| 陶瓷弹珠质量 | 1.0 | 0.8-1.2 | 相对单位 |
|
||||
| 陶瓷弹珠摩擦加成 | 0.3 | 0.2-0.5 | 高摩擦 |
|
||||
| 陶瓷弹珠碰撞恢复系数 | 0.8 | 0.7-0.9 | 碰撞传递高(核心优势) |
|
||||
| 铁弹珠半径 | 0.012m | 0.008-0.015m | 小而重 |
|
||||
| 铁弹珠质量 | 2.5 | 2.0-3.0 | 相对单位(重) |
|
||||
| 铁弹珠摩擦加成 | 0.1 | 0.0-0.3 | 低摩擦 |
|
||||
| 铁弹珠碰撞恢复系数 | 0.6 | 0.5-0.8 | 碰撞传递中等 |
|
||||
| 弹珠-场地边界反弹系数 | 0.5 | 0.3-0.7 | 弹珠碰撞场地边界的反弹程度 |
|
||||
| 目标弹珠属性 | 等同玻璃弹珠(标准) | — | 圈中央目标弹珠统一属性 |
|
||||
| 系统基础弹珠属性 | 等同玻璃弹珠(标准) | — | 异常兜底用弹珠 |
|
||||
| NPC 思考延迟 | 1.0 秒 | 0.5-2.0 秒 | NpcThink 状态 |
|
||||
| 死党 B 准度(角度偏差) | ±3° | ±1°-±10° | NPC 弹射准度 |
|
||||
| 死党 B 力度误差 | ±10% | ±5%-±25% | NPC 力度偏差 |
|
||||
| 死党 B 基础力度 | 50% | 30%-70% | 力度估算基数 |
|
||||
| 死党 B 距离补偿系数 | 每米 +15% | 10%-25%/m | 距离补偿 |
|
||||
| 结果显示时长 | 2.0 秒 | 1.5-3.0 秒 | 胜负文本 |
|
||||
|
||||
> 摩擦组合规则:最终摩擦 = 场地摩擦 + 弹珠摩擦加成。碰撞推力按物理规律根据质量自动计算。
|
||||
|
||||
---
|
||||
|
||||
## 文本示例
|
||||
|
||||
> 所有文本为小孩当下视角,无成人回顾。
|
||||
|
||||
### 操作提示
|
||||
- MatchInit:`选择你的射手弹珠` → `按 空格 开始`
|
||||
- PlayerAim:`按住弹珠拖拽瞄准 ESC 放弃本局`
|
||||
- 放弃确认:`放弃本局?将判负 [确认] [取消]`
|
||||
- 结束确认:`按 空格 离开`
|
||||
|
||||
### 局结算
|
||||
- 胜:`你赢了!获得 1 颗弹珠`
|
||||
- 败(正常):`你输了…失去 1 颗弹珠`
|
||||
- 败(保底):`你输了…好在没失去弹珠`
|
||||
- 玩家放弃:`你放弃了这局`
|
||||
|
||||
### 射手弹珠选择界面(属性标注,措辞对称)
|
||||
- `玻璃弹珠 · 滚得远,走线精准`
|
||||
- `陶瓷弹珠 · 滚得近,碰撞利落`
|
||||
- `铁弹珠 · 质量大,撞散目标`
|
||||
@@ -0,0 +1,220 @@
|
||||
# 蹲下系统执行案(v0.2 原型)
|
||||
|
||||
## 迭代记录
|
||||
|
||||
| 迭代版本 | 修改日期 | 状态 | 说明 |
|
||||
|----------|----------|------|------|
|
||||
| v0.2.0 | 2026-06-15 | 📝 设计中 | 初版执行案,蹲伏姿态、蹲伏移动、视角与高度变化、低矮空间通行、与弹珠入场衔接 |
|
||||
| v0.2.1 | 2026-06-15 | 📝 设计中 | game-reviewer 审查修订:修正"蹲伏中进入弹珠"与弹珠执行案"不位移"的矛盾(改为原地转向+下蹲,不位移);补充蹲伏动画缺失时的兜底表现声明 |
|
||||
|
||||
> 状态标记:📝 设计中 | 🔄 开发中 | ✅ 已同步
|
||||
|
||||
---
|
||||
|
||||
## 系统概述
|
||||
|
||||
蹲下系统是玩家控制器的扩展。v0.1 仅实现站立姿态(Idle / Running),v0.2 增加蹲伏姿态。
|
||||
|
||||
**蹲下在 v0.2 的三个用途**:
|
||||
1. **弹珠小游戏入场** — 角色蹲在场地边操作弹珠(由弹珠系统调用,系统强制蹲下)
|
||||
2. **探索低矮空间** — 钻入桌下、矮洞、低矮门洞,发现隐藏物品
|
||||
3. **探索层次感** — 蹲下降低视角,看到接近地面的细节,增加探索丰富度
|
||||
|
||||
### 核心设计决定(已确认)
|
||||
|
||||
1. **切换式蹲下(按 C 键切换站 / 蹲)** — 非长按。适合探索,蹲下找东西不需持续按键
|
||||
2. **蹲伏移动速度低于跑步** — 蹲下是谨慎 / 探索状态,速度约为跑步的一半
|
||||
3. **蹲伏降低有效高度** — 可钻过低矮空间(站立进不去的地方)
|
||||
4. **蹲伏降低视角** — 摄像机跟随蹲下的头部高度
|
||||
5. **系统可强制蹲下** — 弹珠入场由系统控制姿态,玩家 C 键无效
|
||||
|
||||
---
|
||||
|
||||
## 系统边界
|
||||
|
||||
| 蹲下系统负责 | 蹲下系统不负责 |
|
||||
|--------------|----------------|
|
||||
| 蹲 / 站姿态切换与过渡表现 | 蹲下专属的隐藏物品内容(交互 / 收集系统) |
|
||||
| 蹲伏移动速度与转向 | 低矮空间的场景搭建(关卡) |
|
||||
| 蹲伏时有效高度与视角变化 | 弹珠小游戏的内部逻辑(弹珠系统) |
|
||||
| 为弹珠入场提供蹲下姿态接口 | 交互物"仅蹲伏激活"的具体配置(交互系统) |
|
||||
|
||||
---
|
||||
|
||||
## 状态定义(扩展玩家控制器)
|
||||
|
||||
在 v0.1 的 Idle / Running 基础上引入"姿态层 + 移动层"二维状态:
|
||||
|
||||
| 姿态 | 移动 | 组合状态 | 说明 |
|
||||
|------|------|----------|------|
|
||||
| Stand | Idle | 站立待机 | v0.1 的 Idle |
|
||||
| Stand | Moving | 跑步 | v0.1 的 Running,4.5 m/s |
|
||||
| Crouch | Idle | 蹲伏待机 | v0.2 新增 |
|
||||
| Crouch | Moving | 蹲伏移动 | v0.2 新增,2.0 m/s |
|
||||
|
||||
### 状态转换
|
||||
|
||||
```
|
||||
Stand-Idle ──(按 C)──→ Crouch-Idle
|
||||
Stand-Moving ──(按 C)──→ Crouch-Moving
|
||||
Crouch-Idle ──(按 C + 头顶无障碍)──→ Stand-Idle
|
||||
Crouch-Moving ──(按 C + 头顶无障碍)──→ Stand-Moving
|
||||
Crouch-Idle ──(按移动键)──→ Crouch-Moving
|
||||
Crouch-Moving ──(松开移动键)──→ Crouch-Idle
|
||||
```
|
||||
|
||||
**站起障碍检测**:从蹲到站,若头顶存在障碍物(处于低矮空间内)→ 无法站起,保持蹲伏。
|
||||
|
||||
**弹珠系统强制切换**:弹珠入场时角色强制进入 Crouch 姿态(系统触发,非玩家按 C);退出时强制回到 Stand。
|
||||
|
||||
---
|
||||
|
||||
## 行为规则
|
||||
|
||||
### 一、姿态切换(站 ↔ 蹲)
|
||||
|
||||
#### 玩家主动切换
|
||||
|
||||
| 操作 | 触发 | 表现 |
|
||||
|------|------|------|
|
||||
| 站 → 蹲 | 玩家按 C 键 | 立即开始下蹲过渡,时长 0.3 秒(建议值,可调,范围 0.2-0.5 秒)。过渡中可移动(按蹲伏速度) |
|
||||
| 蹲 → 站 | 玩家按 C 键 | 先检测头顶障碍。无障碍 → 站起过渡 0.3 秒;有障碍 → 拒绝站起,保持蹲伏 |
|
||||
| 过渡中再按 C | 取消当前过渡,反向切换 | 例:下蹲过渡中按 C → 转为站起 |
|
||||
|
||||
#### 系统强制切换(弹珠入场 / 退出)
|
||||
|
||||
| 场景 | 行为 |
|
||||
|------|------|
|
||||
| 弹珠入场 | 系统强制角色进入 Crouch 姿态(就位表现 0.8 秒,含下蹲过渡) |
|
||||
| 弹珠退出 | 系统强制角色回到 Stand 姿态(站起表现 0.8 秒,含站起过渡) |
|
||||
| 强制期间 | 玩家的 C 键输入被忽略,姿态由弹珠系统控制 |
|
||||
|
||||
---
|
||||
|
||||
### 二、蹲伏移动
|
||||
|
||||
#### 速度与加减速
|
||||
|
||||
| 项目 | 建议值 | 可调范围 | 说明 |
|
||||
|------|--------|----------|------|
|
||||
| 蹲伏移动速度 | 2.0 m/s | 1.5-3.0 m/s | 约跑步(4.5)一半 |
|
||||
| 蹲伏起步加速时间 | 0.2 秒 | 0.1-0.4 秒 | 与站立一致 |
|
||||
| 蹲伏停止减速时间 | 0.3 秒 | 0.2-0.5 秒 | 与站立一致 |
|
||||
|
||||
#### 方向与转向
|
||||
|
||||
- **移动方向**:相对摄像机水平朝向(与站立移动逻辑一致)
|
||||
- **蹲伏转向速度**:540°/秒(建议值,可调,范围 360°-720°/秒),低于跑步(720°/秒),蹲下转身更慢、更谨慎
|
||||
- **连续移动**:蹲伏中旋转摄像机,移动方向实时跟随,与站立行为一致
|
||||
|
||||
#### 动画
|
||||
|
||||
- **蹲伏待机**:蹲姿静止(v0.2 占位动画)
|
||||
- **蹲伏移动**:蹲姿挪步(v0.2 占位动画)
|
||||
- **动画兜底**:若无占位动画资源,蹲伏状态复用站立动画 + 蹲伏有效高度偏移作为最低兜底表现(与玩家控制器执行案的动画兜底策略一致)
|
||||
|
||||
---
|
||||
|
||||
### 三、蹲伏时的有效高度与通行
|
||||
|
||||
#### 有效高度
|
||||
|
||||
| 姿态 | 有效高度(建议值) | 可调范围 |
|
||||
|------|---------------------|----------|
|
||||
| 站立 | 1.6m | 1.4-1.8m |
|
||||
| 蹲伏 | 1.0m | 0.8-1.2m |
|
||||
|
||||
#### 通行规则
|
||||
|
||||
- 角色有效高度 ≤ 空间净高 → 可通过
|
||||
- 角色有效高度 > 空间净高 → 被阻挡
|
||||
- **效果**:蹲伏时可通过净高在 1.0m-1.6m 之间的低矮空间(桌下、矮洞、低矮门洞),站立无法进入
|
||||
- 蹲伏通过低矮空间时保持蹲伏姿态;离开低矮空间(头顶已无障碍)后可按 C 站起
|
||||
|
||||
---
|
||||
|
||||
### 四、蹲伏时的摄像机
|
||||
|
||||
| 项目 | 站立 | 蹲伏 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 摄像机高度(头部上方) | 0.8m | 0.4m(建议值,可调,范围 0.3-0.6m) | 蹲伏视角降低 |
|
||||
| 摄像机俯角 | 15° | 15° | 保持一致 |
|
||||
| 跟随方式 | 软跟随 | 软跟随 | 规则不变 |
|
||||
|
||||
**姿态切换时摄像机过渡**:摄像机高度随姿态过渡同步平滑变化(0.3 秒),与角色下蹲 / 站起同步。
|
||||
|
||||
**效果**:蹲下时视角降低,看到更接近地面的细节,适合寻找隐藏在低处的物品。
|
||||
|
||||
---
|
||||
|
||||
## 边界情况处理
|
||||
|
||||
### 在低矮空间内尝试站起
|
||||
头顶障碍检测阻止站起,角色保持蹲伏。v0.2 不强制提示文字,仅阻止动作(后续可加"上方有东西"的轻微反馈)。
|
||||
|
||||
### 蹲伏中头顶突然出现障碍(动态物体掉落)
|
||||
v0.2 无动态物理掉落系统,不存在此情况。后续若加入,需定义"被压住时强制保持蹲伏"规则。
|
||||
|
||||
### 蹲伏中进入交互
|
||||
- 蹲伏姿态可正常触发交互,交互系统不因姿态拒绝触发
|
||||
- 交互期间姿态冻结(保持触发交互时的姿态),交互结束后恢复玩家控制
|
||||
- 某些低矮交互物(桌下物品)仅在蹲伏姿态下激活其触发范围
|
||||
|
||||
### 蹲伏中进入弹珠小游戏
|
||||
弹珠入场强制蹲下。若角色已在蹲伏姿态,就位表现中姿态无变化(仍为蹲伏),仅播放原地转向 + 下蹲表现(与站立入场一致,**不发生位移**——玩家在触发交互前已自行走动到场地边缘就位点,详见 `弹珠小游戏-执行案.md` 进入流程)。
|
||||
|
||||
### 姿态切换过渡中按移动键
|
||||
过渡中接受移动输入,按目标姿态的速度移动。例:站 → 蹲过渡中按住 W → 角色以蹲伏速度开始移动。
|
||||
|
||||
### 姿态切换过渡中进入交互
|
||||
交互触发时冻结当前姿态(无论过渡进行到哪)。交互结束后回到冻结时的姿态。
|
||||
|
||||
---
|
||||
|
||||
## 跨系统交互
|
||||
|
||||
### 与玩家控制器(移动系统)
|
||||
- 蹲下系统扩展玩家控制器的姿态层,移动方向逻辑复用
|
||||
- 蹲伏移动速度、转向速度由蹲下系统提供数值
|
||||
|
||||
### 与交互系统
|
||||
- 蹲伏姿态可正常触发交互
|
||||
- 蹲下系统向交互系统提供"当前姿态"信号;某些低矮交互物仅在 Crouch 姿态激活触发范围(由交互物配置决定)
|
||||
|
||||
### 与弹珠小游戏
|
||||
- 弹珠入场调用蹲下系统的"强制蹲下"接口
|
||||
- 弹珠退出调用蹲下系统的"强制站起"接口
|
||||
- 弹珠小游戏进行期间,姿态由弹珠系统控制,玩家 C 键无效
|
||||
|
||||
### 与收集系统(间接)
|
||||
- 蹲伏发现的隐藏物品 → 交互拾取 → 通知物品系统 → 收集系统处理
|
||||
- 蹲下系统不直接与收集系统通信
|
||||
|
||||
---
|
||||
|
||||
## v0.2 不实现清单
|
||||
|
||||
| 功能 | 排除原因 |
|
||||
|------|----------|
|
||||
| 长按式蹲下 | 采用切换式,更适配探索 |
|
||||
| 匍匐(趴下,更低姿态) | 留后续版本 |
|
||||
| 蹲下静音 / 潜行机制 | 无 NPC 听觉 AI(v0.3+ 才有 NPC) |
|
||||
| 蹲下专属精细动画 | v0.2 使用占位动画 |
|
||||
| 滑铲 / 翻滚 | 留后续版本 |
|
||||
| 站起失败的文字提示 | v0.2 仅阻止动作,不加提示 |
|
||||
|
||||
---
|
||||
|
||||
## 可调参数汇总
|
||||
|
||||
| 参数 | 建议值 | 可调范围 | 说明 |
|
||||
|------|--------|----------|------|
|
||||
| 蹲伏移动速度 | 2.0 m/s | 1.5-3.0 m/s | 约跑步一半 |
|
||||
| 蹲伏转向速度 | 540°/秒 | 360°-720°/秒 | 低于跑步 720°/秒 |
|
||||
| 站立有效高度 | 1.6m | 1.4-1.8m | 小孩身高 |
|
||||
| 蹲伏有效高度 | 1.0m | 0.8-1.2m | 蹲下高度 |
|
||||
| 蹲伏摄像机高度 | 0.4m | 0.3-0.6m | 蹲伏头部上方 |
|
||||
| 姿态切换过渡时长 | 0.3 秒 | 0.2-0.5 秒 | 站 ↔ 蹲 |
|
||||
| 蹲伏起步加速时间 | 0.2 秒 | 0.1-0.4 秒 | 与站立一致 |
|
||||
| 蹲伏停止减速时间 | 0.3 秒 | 0.2-0.5 秒 | 与站立一致 |
|
||||
| 弹珠就位表现时长 | 0.8 秒 | 0.5-1.2 秒 | 含下蹲过渡(与弹珠执行案一致) |
|
||||
@@ -0,0 +1,113 @@
|
||||
# 收集系统(P-013)实现偏差报告
|
||||
|
||||
- **任务ID**: P-013
|
||||
- **对应执行案**: `docs/design/收集系统-执行案.md` (v0.1.1)
|
||||
- **对应技术方案**: `docs/technical/收集系统-技术方案.md` (v1.0 架构解耦)
|
||||
- **实现日期**: 2026-06-15
|
||||
- **实现者**: 程序 Agent
|
||||
- **审查**: 多维度对抗性静态审查(10 agent),结论 0 critical / 0 major / 2 minor(已修复)
|
||||
|
||||
---
|
||||
|
||||
## 一、行为规则覆盖核对(对照执行案逐条)
|
||||
|
||||
执行案全部行为规则均已实现覆盖,**无行为规则层面的重大偏差**。
|
||||
|
||||
| 执行案条目 | 实现位置 | 状态 |
|
||||
|------------|---------|------|
|
||||
| **一、5步流程** | | |
|
||||
| 步骤1 查品类 | `FCollectionManager::OnItemObtained` 比对 `Config.TrackedCategory` | ✅ |
|
||||
| 步骤2 查重复 | `ProcessNewCollection` 的 `CollectedIDs.Contains` | ✅ |
|
||||
| 步骤3 记录 | `CollectedIDs.Add` + `Progress = Num()` + `SaveStateToPersistence` | ✅ |
|
||||
| 步骤4 弹通知 | `RefreshCachedInfo` 生成文本 → HUDComponent Tick 检测 → 延迟创建 | ✅ |
|
||||
| 步骤5 完成判定 | `Progress >= TotalCount` → `bIsComplete`/`bJustCompleted` | ✅ |
|
||||
| **二、收集通知UI** | | |
|
||||
| 新收集触发 | HUDComponent Tick 检测 Progress 变化 | ✅ |
|
||||
| 延迟 0.3s | `NotifDelayTimerHandle` + `Config.NotifDelay` | ✅ |
|
||||
| 内容「发现了新的杂物,进度X/Y」 | `RefreshCachedInfo` 的 `FString::Printf` | ✅ |
|
||||
| 浅金色 #F0D060 | `Config.NotifTextColor` → Widget `SetColorAndOpacity` | ✅ |
|
||||
| 淡入0.2/停留2.0/淡出0.3 | Widget `InitializeNotif` 三段时序 | ✅ |
|
||||
| 最大同屏2条 | `ActiveNotifications.Num() >= MaxSimultaneousNotifs` | ✅ |
|
||||
| **三、进度UI** | | |
|
||||
| 左上角3%边距 | 蓝图布局(指南已说明) | ✅ |
|
||||
| 格式「📦杂物X/Y」 | `UCollectionProgressWidget::UpdateProgress` | ✅ |
|
||||
| 常驻显示 | `CreateProgressWidget` + `AddToViewport(5)` | ✅ |
|
||||
| 弹性1.2x/0.2s | `BounceAnim` 动画 | ✅ |
|
||||
| **四、完成表现** | | |
|
||||
| 进度金色1.4x/0.5s | `SetCompleteStyle` + `CompleteAnim` | ✅ |
|
||||
| 通知消失+0.5s后中心提示 | `TriggerCompletionSequence` 的 `TotalDelay` | ✅ |
|
||||
| 「杂物全部收集完成!」2.0s | `ShowComplete` | ✅ |
|
||||
| **五、边界情况** | | |
|
||||
| 重复物品 | `CollectedIDs` 查重拦截 | ✅ |
|
||||
| 完成后继续通知 | 查重 + `bCompletionShown` 一次性 | ✅ |
|
||||
| 重启恢复 | `RestoreFromPersistence`(Driver.BeginPlay 调用) | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 二、实现偏差清单
|
||||
|
||||
### 偏差 1:`FInteractableData` 新增 `Category` 字段(跨交互系统数据结构)
|
||||
- **对应执行案条目**: 第一节步骤1「查品类」
|
||||
- **偏差等级**: 轻微
|
||||
- **原方案**: 技术方案第13节回调签名 `(ItemId, DisplayName, Category)` 隐含要求 Category 数据,但 `FInteractableData`(交互系统)原无此字段
|
||||
- **实际实现**: 在 `Interaction/InteractionTypes.h` 的 `FInteractableData` 新增 `UPROPERTY Category`(FName,拾取/翻找物填"杂物",非收集物留空)
|
||||
- **偏差原因**: 收集系统查品类必须有 Category 数据源,原数据结构缺失,需补字段。此为实现技术方案的必要数据结构扩展,不改任何行为规则
|
||||
- **建议处理**: 文档同步——在交互系统技术方案补记 `FInteractableData.Category` 字段;交互物配置时需填写(P-016 配置拾取物时执行)
|
||||
|
||||
### 偏差 2:`FCollectionCachedInfo::Reset()` 改名为 `ResetTransient()`
|
||||
- **对应执行案条目**: 无(技术方案数据结构细节)
|
||||
- **偏差等级**: 轻微
|
||||
- **原方案**: 技术方案第5节定义 `void Reset()`
|
||||
- **实际实现**: 改名为 `ResetTransient()`
|
||||
- **偏差原因**: 原 `Reset()` 名易误解为重置全部字段,实际只重置瞬时态(`bJustCompleted`/`PendingNotificationText`/`LastCollectedItemId`),保留 `Progress`/`TotalCount`/`bIsComplete`。新名准确表达语义,防误用
|
||||
- **建议处理**: 文档同步——技术方案第5节同步改名
|
||||
|
||||
### 偏差 3:进度 Widget 懒创建(非 Initialize 即创建)
|
||||
- **对应执行案条目**: 第三节「游戏开始后始终可见」
|
||||
- **偏差等级**: 轻微(行为不变,仅创建时机延迟一帧)
|
||||
- **原方案**: 技术方案第6节 `CreateProgressWidget` 在 `Initialize`/`BeginPlay` 时创建
|
||||
- **实际实现**: `UCollectionHUDComponent` 改为首次有效 Tick 时懒创建 `ProgressWidget`
|
||||
- **偏差原因**: 规避 `AChildHoodCharacter::BeginPlay` 与 `ASystemDriver::BeginPlay` 的时序竞争——Character 可能先于 Driver 初始化,此时 `ASystemDriver::Get` 返回的 Driver 可能尚未完成 `CollectionManager.RestoreFromPersistence`。懒创建确保 World 就绪 + Driver 完成初始化后再创建,进度值正确
|
||||
- **建议处理**: 保留(更稳健);文档同步——技术方案第6节补记懒创建策略
|
||||
|
||||
### 偏差 4:Widget 三段时序用 `SetTimer` 而非动画完成委托
|
||||
- **对应执行案条目**: 第二节通知「淡入→停留→淡出」
|
||||
- **偏差等级**: 轻微(行为等价)
|
||||
- **原方案**: 技术方案提及 UMG Animation 三段生命周期
|
||||
- **实际实现**: 淡出阶段用 `SetTimer(FadeOutDuration)` 计时后 `RemoveFromParent`,而非监听 `UUserWidget::OnAnimationFinished` 委托
|
||||
- **偏差原因**: 避免动画完成委托绑定的复杂度;计时器时长与动画时长一致,行为完全等价
|
||||
- **建议处理**: 保留(实现更简洁)
|
||||
|
||||
### 偏差 5:3 个 WBP Widget 蓝图资产未创建(交付边界划分)
|
||||
- **对应执行案条目**: 技术方案第7节美术资源(3个 UMG Widget)
|
||||
- **偏差等级**: 轻微(逻辑层完整,UI 资产归 P-016)
|
||||
- **原方案**: 技术方案交付物含 3 个 WBP 蓝图
|
||||
- **实际实现**: 3 个 Widget 的 **C++ 基类已全部实现**(`UCollectionNotifWidget`/`UCollectionProgressWidget`/`UCollectionCompleteWidget`,含 `BindWidget` 绑定契约);WBP 蓝图(.uasset 视觉布局)未创建
|
||||
- **偏差原因**: 与用户对齐的交付边界——C++ 基类属 P-013(程序),WBP 蓝图创建属 P-016(Widget 表现层接通,用户已决定新开任务)。无 WBP 时 C++ 判空跳过 UI 创建,收集逻辑仍正常运行
|
||||
- **建议处理**: 已产出创建指南 `docs/technical/笔记/P-013-收集系统Widget蓝图创建指南.md`;WBP 创建纳入 P-016 执行
|
||||
|
||||
### 偏差 6:§14g 集成测试(完整拾取流程 PIE 端到端验证)未执行 — 推迟至 P-017 完成后
|
||||
- **对应执行案条目**: 技术方案 §14g「集成测试(与交互系统联调)·验证方式:完整拾取流程验证」
|
||||
- **偏差等级**: 中等(范围项推迟,非实现缺陷)
|
||||
- **原方案**: 技术方案 §14g 要求完成端到端集成测试——在 PIE 中走通"拾取交互物 → AddToBackpack 回调 → 收集系统记账 → 通知/进度/完成 UI"全链路
|
||||
- **实际实现**: §14g 的代码前置(§14c 回调连线)与测试载体(`BP_Pickup.uasset`,commit 9742774)已就位;但**端到端 PIE 验证未执行**,全仓库无测试记录
|
||||
- **偏差原因**: §14g 端到端验证依赖 P-016 的 3 个 WBP 蓝图资产(偏差5)+ P-017 的拾取物配置(Category/ItemId/Effect.Actions=AddToBackpack)。二者均未开始:无 WBP 时 C++ 判空跳过 UI、无拾取配置则链路无法触发,PIE 无法走通。属任务编排的前后依赖,非实现遗漏
|
||||
- **建议处理**: §14g 端到端验证作为 **follow-up**,在 P-017(交互物放置 + 拾取配置)完成后补做。本任务 P-013 收尾范围限定为【代码层 + §14c 回调连线 + 三层架构】,经分阶段审查通过后解除 blocked;§14g 验证结果回填本报告或新开小任务追踪
|
||||
|
||||
---
|
||||
|
||||
## 三、审查修复(非偏差,对抗性审查后的质量提升)
|
||||
|
||||
多维度审查 workflow(5 维度:编译/架构/规则/集成/生命周期)发现并已修复 2 项 minor:
|
||||
|
||||
1. **日志一致性**:`CollectionHUDComponent.cpp` 的 `UE_LOG(LogTemp, ...)` 改为项目类别 `UE_LOG(LogChildHood, ...)`,与逻辑/驱动/持久层一致
|
||||
2. **EndPlay 生命周期清理**:`UCollectionHUDComponent` 补 `EndPlay` override,清理 `NotifDelayTimerHandle`/`CompleteTimerHandle` + 移除挂载 Widget,消除角色 Respawn 时悬挂 timer 的野指针风险(与项目其他 Character 的卫生写法一致)
|
||||
|
||||
---
|
||||
|
||||
## 四、总结
|
||||
|
||||
- **行为规则覆盖**:执行案全部规则已实现,无重大行为偏差
|
||||
- **实现偏差**:6 项(5 轻微 + 1 中等):偏差 1/2/3/4 为技术方案层面的实现决策/命名/时序优化,偏差 5 为交付边界划分(WBP 蓝图归 P-016),偏差 6 为范围项推迟(§14g 集成验证依赖 P-016/P-017)
|
||||
- **建议**:偏差 1/2/3 在下次技术方案文档同步时更新;偏差 5 纳入 P-016 执行;偏差 4 保留;偏差 6 作为 P-017 完成后的 follow-up
|
||||
- **遗留**:3 个 WBP 蓝图(属 P-016)+ 拾取物配置(属 P-017)+ §14g 端到端 PIE 集成验证(属 P-017 完成后的 follow-up)
|
||||
+236
-17
@@ -2,7 +2,7 @@
|
||||
|
||||
> 对应执行案: `docs/design/交互系统-执行案.md` (v0.1.2)
|
||||
> 关联系统: 角色控制系统、物品系统、收集系统、GameInstance 持久化层
|
||||
> 日期: 2026-06-11(v1.0 全面重构:Effect/Action 数据驱动架构)
|
||||
> 日期: 2026-06-11(2026-06-11 更新:审查修订 v1.1)
|
||||
|
||||
---
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|------|------|----------|
|
||||
| v0.1 | 2026-06-07 | 初版技术方案,基于类型分发的状态机架构 |
|
||||
| v1.0 | 2026-06-11 | 全面重构:Effect/Action 数据驱动架构;普通 C++ Manager 挂 AInfo;门派生类;持久化 Subsystem;逻辑/表现解耦 |
|
||||
| v1.1 | 2026-06-11 | 审查修订:补充 ShowNarrative 的 bWaitForInput/bRandomSelect;补充 FSelectedInfo/FTurnState 定义;补充 CancelEffect 行为;修正模板 A/E 关闭流程;修正时序图标注 |
|
||||
|
||||
---
|
||||
|
||||
@@ -238,6 +239,8 @@ classDiagram
|
||||
-float ElapsedTime
|
||||
-int32 NextActionIndex
|
||||
-bool bIsPlaying
|
||||
-bool bIsWaitingForInput
|
||||
-int32 LastNarrativeIndex
|
||||
-FTurnState TurnState
|
||||
-TFunction~void~ OnEffectComplete
|
||||
+PlayEffect(FInteractEffect, FEffectContext) void
|
||||
@@ -246,6 +249,9 @@ classDiagram
|
||||
+Update(float DeltaTime) void
|
||||
-ExecuteAction(FInteractAction, FEffectContext) void
|
||||
-FinishEffect() void
|
||||
-ResumeFromWait() void
|
||||
-PerformCleanup() void
|
||||
-PickRandomNarrative(TArray~FString~) FString
|
||||
Note: 普通C++类,非UObject
|
||||
Note: 所有UObject引用用TWeakObjectPtr
|
||||
Note: Tick由Driver显式调用
|
||||
@@ -387,7 +393,7 @@ sequenceDiagram
|
||||
autonumber
|
||||
actor Player as 玩家
|
||||
participant Char as AChildHoodCharacter
|
||||
participant Mgr as InteractionManager<br/>(GameInstance)
|
||||
participant Mgr as FInteractionManager<br/>(ASystemDriver持有)
|
||||
participant Comp as UInteractableComponent
|
||||
participant EFM as EffectManager
|
||||
participant Persist as PersistenceSubsystem
|
||||
@@ -397,7 +403,7 @@ sequenceDiagram
|
||||
|
||||
Note over Player,Driver: === Phase 0: 进入检测范围 ===
|
||||
Player->>Char: 移动进入 TriggerRadius
|
||||
Comp->>Mgr: OnOverlap → GetGI()->GetMgr()->RegisterInteractable(this)
|
||||
Comp->>Mgr: OnOverlap → Driver->GetInteractionManager().RegisterInteractable(this)
|
||||
Mgr->>Mgr: Sort by Dist2D, SelectedIndex = 0
|
||||
|
||||
Note over Player,Driver: === Phase 1: Prompt 显示 (Pres 每帧 Polling) ===
|
||||
@@ -411,7 +417,7 @@ sequenceDiagram
|
||||
|
||||
Note over Player,Driver: === Phase 2: 按 E 触发 Effect ===
|
||||
Player->>Char: 按 E
|
||||
Char->>Mgr: GetGI()->GetMgr()->BeginInteract()
|
||||
Char->>Mgr: Driver->GetInteractionManager().BeginInteract()
|
||||
Mgr->>Mgr: State = Executing
|
||||
Mgr->>Char: SetInteracting(true)
|
||||
Mgr->>EFM: PlayEffect(Comp.Data.InteractEffect, Context)
|
||||
@@ -481,6 +487,7 @@ enum class EInteractActionType : uint8
|
||||
ShowActor UMETA(DisplayName = "显示交互物"),
|
||||
AddToBackpack UMETA(DisplayName = "物品进背包"),
|
||||
ShowNarrative UMETA(DisplayName = "显示叙事文本"),
|
||||
HideNarrative UMETA(DisplayName = "隐藏叙事文本"),
|
||||
ShowPickupNotification UMETA(DisplayName = "显示拾取通知"),
|
||||
ToggleDoor UMETA(DisplayName = "切换门状态"),
|
||||
PlayDialogue UMETA(DisplayName = "播放对话"),
|
||||
@@ -537,6 +544,14 @@ struct FInteractAction
|
||||
// --- ShowNarrative ---
|
||||
UPROPERTY(EditAnywhere, meta = (EditCondition = "Type==EInteractActionType::ShowNarrative", EditConditionHides))
|
||||
TArray<FString> NarrativeTexts;
|
||||
|
||||
/** 是否等待用户按 E/ESC 后再继续后续 Action */
|
||||
UPROPERTY(EditAnywhere, meta = (EditCondition = "Type==EInteractActionType::ShowNarrative", EditConditionHides))
|
||||
bool bWaitForInput = false;
|
||||
|
||||
/** 是否从 NarrativeTexts 中随机选取(不重复上一次) */
|
||||
UPROPERTY(EditAnywhere, meta = (EditCondition = "Type==EInteractActionType::ShowNarrative", EditConditionHides))
|
||||
bool bRandomSelect = true;
|
||||
};
|
||||
```
|
||||
|
||||
@@ -619,6 +634,80 @@ struct FEffectContext
|
||||
};
|
||||
```
|
||||
|
||||
### FSelectedInfo — Presentation 层缓存数据
|
||||
|
||||
> Manager 在选择变化时从 Component 读一次并写入此结构体,Prompt 组件每帧只读此缓存,不直接访问 Component。
|
||||
|
||||
```cpp
|
||||
struct FSelectedInfo
|
||||
{
|
||||
/** 是否有有效选中 */
|
||||
bool bHasSelection = false;
|
||||
|
||||
/** 选中交互物的提示文本(如 "[E] 查看旧冰箱") */
|
||||
FString PromptText;
|
||||
|
||||
/** 选中交互物的世界坐标(用于屏幕空间投影) */
|
||||
FVector WorldPosition = FVector::ZeroVector;
|
||||
|
||||
/** 选中交互物是否已消耗(消耗后不再显示提示) */
|
||||
bool bIsConsumed = false;
|
||||
|
||||
void Reset()
|
||||
{
|
||||
bHasSelection = false;
|
||||
PromptText.Empty();
|
||||
WorldPosition = FVector::ZeroVector;
|
||||
bIsConsumed = false;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**写入时机**:`RefreshCachedSelection()` — 在 `RegisterInteractable` / `UnregisterInteractable` / `SwitchTarget` 中调用。
|
||||
|
||||
**读取方**:`UInteractionPromptComponent::TickComponent` 每帧读取 `CachedSelected.PromptText` 和 `CachedSelected.WorldPosition`,不碰任何 Component。
|
||||
|
||||
### FTurnState — TurnToTarget 跨帧插值状态
|
||||
|
||||
> TurnToTarget Action 需要每帧 RInterpTo 驱动旋转,不能一帧完成。EffectManager 在 Update 中维护此状态。
|
||||
|
||||
```cpp
|
||||
struct FTurnState
|
||||
{
|
||||
/** 是否正在转向 */
|
||||
bool bIsActive = false;
|
||||
|
||||
/** 转向起始旋转 */
|
||||
FRotator StartRotation = FRotator::ZeroRotator;
|
||||
|
||||
/** 转向目标旋转(面朝交互物方向) */
|
||||
FRotator TargetRotation = FRotator::ZeroRotator;
|
||||
|
||||
/** 已用时间(秒) */
|
||||
float ElapsedTime = 0.f;
|
||||
|
||||
/** 转向总时长(秒) */
|
||||
float Duration = 0.2f;
|
||||
|
||||
/** 是否已完成 */
|
||||
bool IsComplete() const { return ElapsedTime >= Duration; }
|
||||
|
||||
/** 获取当前插值 Alpha [0,1] */
|
||||
float GetAlpha() const { return FMath::Clamp(ElapsedTime / Duration, 0.f, 1.f); }
|
||||
|
||||
void Reset()
|
||||
{
|
||||
bIsActive = false;
|
||||
StartRotation = FRotator::ZeroRotator;
|
||||
TargetRotation = FRotator::ZeroRotator;
|
||||
ElapsedTime = 0.f;
|
||||
Duration = 0.2f;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**生命周期**:`TurnToTarget` Action 触发时初始化 → `EffectManager::Update` 每帧驱动插值 → `IsComplete()` 后停止。
|
||||
|
||||
### 委托
|
||||
|
||||
```cpp
|
||||
@@ -648,19 +737,23 @@ DECLARE_DELEGATE(FOnEffectComplete);
|
||||
|
||||
```
|
||||
Effect: "观察物品"
|
||||
TotalDuration: 3.0s, Cancellable: true
|
||||
TotalDuration: 10.0s (安全上限,实际由 bWaitForInput 控制结束), Cancellable: true
|
||||
|
||||
TriggerTime | Action | 参数
|
||||
----------- | ------------- | ----
|
||||
0.0s | TurnToTarget | TurnDuration=0.2
|
||||
0.0s | CameraPush | PushDistance=90, PushDuration=0.5
|
||||
0.7s | ShowNarrative | Texts=["冰箱嗡嗡响。里面有冰棒!"]
|
||||
0.7s | (等待用户输入,ShowNarrative 持续显示)
|
||||
0.7s | ShowNarrative | Texts=["冰箱嗡嗡响。里面有冰棒!"], bWaitForInput=true, bRandomSelect=true
|
||||
(用户按E/ESC后继续)
|
||||
~0.7s+ | HideNarrative | (淡出 0.2s)
|
||||
~0.7s+ | CameraReturn | ReturnDuration=0.3
|
||||
```
|
||||
|
||||
> ShowNarrative Action 执行后进入"等待用户输入"状态,用户按 E/ESC 后 Effect 继续。
|
||||
> 此行为通过在 ShowNarrative Action 中标记 `bWaitForInput=true` 实现(后续版本扩展)。
|
||||
> v0.1 简化:ShowNarrative 不等待,用 Effect TotalDuration 控制总时长。
|
||||
> **关闭流程**(对应执行案 E6):ShowNarrative 标记 `bWaitForInput=true`,EffectManager 暂停 Timeline 等待用户按 E/ESC。用户输入后 Effect 恢复,执行 HideNarrative(底框淡出 0.2s)+ CameraReturn(相机回位 0.3s ease-in),Effect 完成。
|
||||
>
|
||||
> **随机文本选取**(对应执行案 E5):ShowNarrative 标记 `bRandomSelect=true`,执行时从 NarrativeTexts 列表中随机选取一条,不重复上一次的内容。若列表只有一条则每次显示同一条。
|
||||
>
|
||||
> **可重复交互**(对应执行案 E5):观察类 Effect 的 bOneShot=false,不调用 Consume。每次交互重新执行 Effect,ShowNarrative 每次随机选取新文本。
|
||||
|
||||
#### 模板 B:拾取类 Effect
|
||||
|
||||
@@ -721,20 +814,139 @@ TriggerTime | Action | 参数
|
||||
|
||||
```
|
||||
Effect: "NPC对话"
|
||||
TotalDuration: 4.0s, Cancellable: true
|
||||
TotalDuration: 10.0s (安全上限,实际由 bWaitForInput 控制结束), Cancellable: true
|
||||
|
||||
TriggerTime | Action | 参数
|
||||
----------- | ------------- | ----
|
||||
0.0s | TurnToTarget | TurnDuration=0.2
|
||||
0.0s | CameraPush | PushDistance=90, PushDuration=0.5
|
||||
0.7s | ShowNarrative | Texts=["zzz"]
|
||||
0.7s | ShowNarrative | Texts=["zzz"], bWaitForInput=true, bRandomSelect=true
|
||||
(用户按E/ESC后继续)
|
||||
~0.7s+ | HideNarrative | (淡出 0.2s)
|
||||
~0.7s+ | CameraReturn | ReturnDuration=0.3
|
||||
```
|
||||
|
||||
> **关闭流程**(对应执行案 K6):与观察类完全一致。ShowNarrative 标记 `bWaitForInput=true`,用户按 E/ESC 后执行 HideNarrative + CameraReturn。
|
||||
>
|
||||
> **随机对话选取**(对应执行案 K6):ShowNarrative 标记 `bRandomSelect=true`,从对话文本列表中随机选取,不重复上一次。若列表只有一条则每次相同。
|
||||
|
||||
### CancelEffect 行为定义
|
||||
|
||||
> 对应审查问题:执行案要求观察/对话类可按 E/ESC 中断,需明确定义 Effect 取消时的完整行为。
|
||||
|
||||
`EffectManager::CancelEffect()` 在用户按 E/ESC 且当前 Effect 的 `bCancellable=true` 时触发。完整行为如下:
|
||||
|
||||
```cpp
|
||||
void FEffectManager::CancelEffect()
|
||||
{
|
||||
if (!bIsPlaying) return;
|
||||
if (!CurrentEffect.bCancellable) return;
|
||||
|
||||
// 1. 检查是否处于 WaitForInput 暂停态
|
||||
if (bIsWaitingForInput)
|
||||
{
|
||||
// 用户输入正是期望的恢复信号 → 走正常恢复流程
|
||||
// 执行 WaitForInput 之后的所有剩余 Action(HideNarrative, CameraReturn 等)
|
||||
ResumeFromWait();
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 非暂停态的强制取消(如安全超时)
|
||||
// 停止 Timeline
|
||||
bIsPlaying = false;
|
||||
|
||||
// 3. 执行收尾清理
|
||||
PerformCleanup();
|
||||
}
|
||||
```
|
||||
|
||||
#### 收尾清理行为(PerformCleanup)
|
||||
|
||||
无论 CancelEffect 走哪条路径,Effect 结束时必须确保以下清理:
|
||||
|
||||
| 清理项 | 条件 | 行为 |
|
||||
|--------|------|------|
|
||||
| 文本底框 | ShowNarrative 曾显示但未 HideNarrative | 强制淡出(0.2s)并移除 Widget |
|
||||
| 相机回位 | CameraPush 曾执行但未 CameraReturn | 强制回位(0.3s ease-in),`SetViewTargetWithBlend(Character)` |
|
||||
| 蒙太奇停止 | PlayMontage 正在播放 | `Montage_Stop(0.2f)` 淡出停止 |
|
||||
| 转向状态 | TurnToTarget 未完成 | 立即设为目标旋转(瞬移到最终位置) |
|
||||
| Timeline 状态 | 任何 | 重置 ElapsedTime、NextActionIndex、TurnState |
|
||||
|
||||
#### WaitForInput 暂停机制
|
||||
|
||||
当 ShowNarrative Action 标记 `bWaitForInput=true` 时:
|
||||
|
||||
1. EffectManager 执行 ShowNarrative(显示文本底框)
|
||||
2. 设置 `bIsWaitingForInput = true`,暂停 Timeline(不再推进 ElapsedTime)
|
||||
3. 用户按 E/ESC → Character 输入回调 → `EffectManager.CancelEffect()`
|
||||
4. CancelEffect 检测到 `bIsWaitingForInput == true` → 调用 `ResumeFromWait()`
|
||||
5. ResumeFromWait 设置 `bIsWaitingForInput = false`,恢复 Timeline
|
||||
6. 后续 Action(HideNarrative、CameraReturn)按 TriggerTime 正常执行
|
||||
7. Timeline 到达 TotalDuration → `FinishEffect()` → 回到 Free 状态
|
||||
|
||||
```cpp
|
||||
void FEffectManager::ResumeFromWait()
|
||||
{
|
||||
bIsWaitingForInput = false;
|
||||
// 继续 Update 中正常触发后续 Action
|
||||
// HideNarrative/CameraReturn 的 TriggerTime 应设在 ShowNarrative 之后
|
||||
}
|
||||
```
|
||||
|
||||
> **设计要点**:WaitForInput 不是取消,而是"暂停等待"。用户按 E/ESC 是期望的恢复信号,不是强制中断。CancelEffect 检测到暂停态时走恢复路径,而非强制清理路径。
|
||||
|
||||
### ShowNarrative Action 执行逻辑
|
||||
|
||||
> 对应执行案:观察类「文本从列表中随机选取,不重复上一次」、对话类「每次触发随机选取,不重复上一次」。
|
||||
|
||||
```cpp
|
||||
void FEffectManager::ExecuteShowNarrative(const FInteractAction& Action, const FEffectContext& Context)
|
||||
{
|
||||
// 1. 选取文本
|
||||
FString SelectedText;
|
||||
if (Action.bRandomSelect && Action.NarrativeTexts.Num() > 1)
|
||||
{
|
||||
SelectedText = PickRandomNarrative(Action.NarrativeTexts);
|
||||
}
|
||||
else if (Action.NarrativeTexts.Num() > 0)
|
||||
{
|
||||
SelectedText = Action.NarrativeTexts[0];
|
||||
}
|
||||
|
||||
// 2. 显示文本底框
|
||||
// 通过 Context 中的 Instigator 获取 PC,显示 NarrativeWidget
|
||||
ShowNarrativeWidget(SelectedText);
|
||||
|
||||
// 3. 如果需要等待用户输入
|
||||
if (Action.bWaitForInput)
|
||||
{
|
||||
bIsWaitingForInput = true;
|
||||
// Update() 中检测到 bIsWaitingForInput 后不再推进 ElapsedTime
|
||||
}
|
||||
}
|
||||
|
||||
FString FEffectManager::PickRandomNarrative(const TArray<FString>& Texts)
|
||||
{
|
||||
if (Texts.Num() == 0) return TEXT("");
|
||||
if (Texts.Num() == 1) return Texts[0];
|
||||
|
||||
int32 CandidateIndex;
|
||||
do {
|
||||
CandidateIndex = FMath::RandRange(0, Texts.Num() - 1);
|
||||
} while (CandidateIndex == LastNarrativeIndex && Texts.Num() > 1);
|
||||
|
||||
LastNarrativeIndex = CandidateIndex;
|
||||
return Texts[CandidateIndex];
|
||||
}
|
||||
```
|
||||
|
||||
> **LastNarrativeIndex** 是 FEffectManager 的成员变量,跨 Effect 调用保持(同一交互物的多次交互不重复)。当交互物切换时由 InteractionManager 重置。
|
||||
|
||||
### 通用规则实现
|
||||
|
||||
#### 规则 1: 触发条件
|
||||
|
||||
**实现**:`UInteractableComponent::OnDetectionOverlapBegin` → `GetGI()->GetMgr()->RegisterInteractable(this)`。Manager 的 `BeginInteract()` 检查 State==Free、SelectedIndex 有效、!IsConsumed()。
|
||||
**实现**:`UInteractableComponent::OnDetectionOverlapBegin` → `ASystemDriver::Get(World)->GetInteractionManager().RegisterInteractable(this)`。Manager 的 `BeginInteract()` 检查 State==Free、SelectedIndex 有效、!IsConsumed()。
|
||||
|
||||
#### 规则 2-5: Prompt UI 显示/隐藏
|
||||
|
||||
@@ -749,7 +961,7 @@ Manager 在 `RegisterInteractable` / `UnregisterInteractable` / `SwitchTarget`
|
||||
|
||||
#### 规则 6-10: 多交互物选择与切换
|
||||
|
||||
**实现**:同 v0.1,但 Manager 在 GameInstance 中。`RegisterInteractable` / `UnregisterInteractable` / `SwitchTarget` / `UpdateSelection` 逻辑不变。
|
||||
**实现**:同 v0.1,但 Manager 在 ASystemDriver 中(非 GameInstance)。`RegisterInteractable` / `UnregisterInteractable` / `SwitchTarget` / `UpdateSelection` 逻辑不变。
|
||||
|
||||
#### 规则 11-14: 交互触发与锁定
|
||||
|
||||
@@ -777,7 +989,13 @@ void AChildHoodCharacter::OnInteract()
|
||||
}
|
||||
```
|
||||
|
||||
`BeginInteract()` 返回选中的 Component(或 nullptr 拒绝),设置 `State = Executing`。Effect 执行期间所有输入被忽略(CancelInteract 检查 bCancellable)。
|
||||
`BeginInteract()` 返回选中的 Component(或 nullptr 拒绝),设置 `State = Executing`。
|
||||
|
||||
**CancelInteract(E/ESC)行为**:
|
||||
- 若 EffectManager 处于 `bIsWaitingForInput` 暂停态 → `ResumeFromWait()`(恢复 Timeline,执行关闭流程 Action)
|
||||
- 若 Effect 的 `bCancellable=true` 但非暂停态 → `CancelEffect()` 强制取消 + `PerformCleanup()` 清理
|
||||
- 若 Effect 的 `bCancellable=false` → 忽略输入(拾取、门操作、翻找期间)
|
||||
- Interacting 状态下 Tab/滚轮/新 E 键触发交互 → 全部忽略
|
||||
|
||||
Effect 完成通过 Driver 中 `std::function` 连线:`EffectManager.OnEffectComplete → InteractionManager.OnInteractionFinished()`。
|
||||
|
||||
@@ -822,7 +1040,7 @@ Effect 完成通过 Driver 中 `std::function` 连线:`EffectManager.OnEffectC
|
||||
| Consume 触发同步 EndOverlap | 🟡中 | Consume() 禁用碰撞同步触发 EndOverlap → UnregisterInteractable,可能清空 CurrentInteractable | Manager 的 UnregisterInteractable 在 Executing 状态下延迟清理 |
|
||||
| TurnToTarget 跨帧驱动 | 🟡中 | TurnToTarget Action 需要每帧 RInterpTo,不能一帧完成 | EffectManager.Update 中维护 TurnState,每帧驱动插值 |
|
||||
| EditCondition 折叠大量 Action 参数 | 🟢低 | FInteractAction 一个 struct 含所有 Action 类型参数,编辑器中按类型折叠 | 使用 EditConditionHides 自动隐藏不相关字段 |
|
||||
| ShowNarrative 等待用户输入 | 🟡中 | 观察/对话类需要等用户按 E/ESC,不能靠 TotalDuration 硬控 | v0.1 用 TotalDuration 硬控;后续版本给 ShowNarrative 加 bWaitForInput 标记,Effect 暂停 Timeline 等待 |
|
||||
| ShowNarrative 等待用户输入 | 🟡中 | 观察/对话类需要等用户按 E/ESC,不能靠 TotalDuration 硬控 | ShowNarrative Action 支持 `bWaitForInput=true`,EffectManager 暂停 Timeline 等待用户输入后恢复,TotalDuration 仅作安全上限 |
|
||||
| AInfo 驱动器生命周期 | 🟢低 | 关卡加载/卸载时需正确 Spawn/销毁 | OnWorldReady 中 Spawn,关卡卸载时 World 销毁自动清理 |
|
||||
|
||||
### 注意事项
|
||||
@@ -832,7 +1050,8 @@ Effect 完成通过 Driver 中 `std::function` 连线:`EffectManager.OnEffectC
|
||||
3. **Manager 是普通 C++**:`FInteractionManager` / `FEffectManager` 不是 UObject,不参与 GC。所有 UObject 引用使用 `TWeakObjectPtr`,回调使用 `TFunction<void()>`
|
||||
4. **InteractionManager 无 Tick**:所有操作由碰撞事件、输入回调、EffectManager 完成回调驱动,无每帧逻辑
|
||||
5. **GameInstance 不持有交互系统**:只有 `UPersistenceSubsystem` 跨 World 存活,所有交互逻辑随 World 生灭
|
||||
4. **Prompt 组件独立**:不参与 Effect 执行,只负责 Free 状态下的 UI 追踪
|
||||
6. **Prompt 组件独立**:不参与 Effect 执行,只负责 Free 状态下的 UI 追踪
|
||||
7. **WaitForInput 暂停机制**:ShowNarrative Action 支持 `bWaitForInput=true`,EffectManager 暂停 Timeline 等待用户输入。用户按 E/ESC 走 ResumeFromWait(恢复后续 Action),而非强制取消
|
||||
|
||||
---
|
||||
|
||||
|
||||
+660
-568
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,90 @@
|
||||
# 收集系统 Widget 蓝图创建指南(P-013)
|
||||
|
||||
> 配套 C++ 代码:`Source/ChildHood/Collection/` 下的 3 个 Widget 基类
|
||||
> 对应技术方案:`docs/technical/收集系统-技术方案.md` 第7节(美术资源)
|
||||
> 状态:C++ 基类已就位,需在 UE 编辑器创建 3 个 UMG 蓝图并配置角色
|
||||
|
||||
---
|
||||
|
||||
## 创建步骤
|
||||
|
||||
3 个 Widget 蓝图都需**继承对应 C++ 基类**,并确保组件名/动画名与 `BindWidget`/`BindWidgetAnim` 精确匹配(否则运行时绑定失败、C++ 判空跳过对应逻辑)。
|
||||
|
||||
### 1. WBP_CollectionNotification(收集通知)
|
||||
|
||||
| 项 | 值 |
|
||||
|----|----|
|
||||
| 父类 | `UCollectionNotifWidget` |
|
||||
| 路径 | `Content/UI/WBP_CollectionNotification` |
|
||||
| ZOrder | 10(C++ `AddToViewport(10)`) |
|
||||
|
||||
**必需绑定组件**(名字必须完全一致):
|
||||
- `TextBlock` 名 **`NotifTextBlock`** — 通知文字
|
||||
- `WidgetAnimation` 名 **`FadeInAnim`** — 淡入(整体 Opacity 0→1,0.2s)
|
||||
- `WidgetAnimation` 名 **`FadeOutAnim`** — 淡出(整体 Opacity 1→0,0.3s)
|
||||
|
||||
**布局**:右上角锚点(与拾取通知错位,执行案第二节「在拾取通知下方」)。
|
||||
**文字颜色**:由 C++ 通过 `NotifTextColor`(浅金色 #F0D060)运行时设置,蓝图 TextBlock 默认颜色无所谓。
|
||||
|
||||
---
|
||||
|
||||
### 2. WBP_CollectionProgress(进度 HUD)
|
||||
|
||||
| 项 | 值 |
|
||||
|----|----|
|
||||
| 父类 | `UCollectionProgressWidget` |
|
||||
| 路径 | `Content/UI/WBP_CollectionProgress` |
|
||||
| ZOrder | 5 |
|
||||
|
||||
**必需绑定组件**:
|
||||
- `TextBlock` 名 **`ProgressTextBlock`** — 进度文本(C++ 设为 `📦 杂物 X/Y`)
|
||||
- `WidgetAnimation` 名 **`BounceAnim`** — 数字弹性(`ProgressTextBlock` 的 RenderScale 1.0→1.2→1.0,0.2s)
|
||||
- `WidgetAnimation` 名 **`CompleteAnim`** — 完成弹性(RenderScale→1.4x,0.5s)
|
||||
|
||||
**布局**:左上角锚点(Anchors Top Left),距左 3% 屏宽、距上 3% 屏高(执行案第三节)。
|
||||
**颜色**:C++ `NormalColor`(白)/`CompleteColor`(金 #F0D060)运行时切换,蓝图默认白色即可。
|
||||
|
||||
---
|
||||
|
||||
### 3. WBP_CollectionComplete(完成提示)
|
||||
|
||||
| 项 | 值 |
|
||||
|----|----|
|
||||
| 父类 | `UCollectionCompleteWidget` |
|
||||
| 路径 | `Content/UI/WBP_CollectionComplete` |
|
||||
| ZOrder | 20 |
|
||||
|
||||
**必需绑定组件**:
|
||||
- `TextBlock` 名 **`CompleteTextBlock`** — 完成文字(C++ 设为「杂物全部收集完成!」)
|
||||
- `WidgetAnimation` 名 **`FadeInAnim`** — 淡入
|
||||
- `WidgetAnimation` 名 **`FadeOutAnim`** — 淡出
|
||||
|
||||
**布局**:中心锚点(Anchors Center),白色大字,屏幕正中。
|
||||
**显示时序**:C++ 控制(淡入→停留 2.0s→淡出→自移除),蓝图只做视觉。
|
||||
|
||||
---
|
||||
|
||||
## 配置角色蓝图
|
||||
|
||||
在角色蓝图(`BP_ChildHoodCharacter` 或当前使用的角色蓝图)中,选中 `CollectionHUDComponent`,在 Details 面板设置三个 WidgetClass:
|
||||
|
||||
```
|
||||
NotifWidgetClass = WBP_CollectionNotification
|
||||
ProgressWidgetClass = WBP_CollectionProgress
|
||||
CompleteWidgetClass = WBP_CollectionComplete
|
||||
```
|
||||
|
||||
> 未设置时 C++ 判空跳过 UI 创建,收集逻辑仍正常运行(仅无 UI 显示)。补全蓝图后即完整可玩。
|
||||
|
||||
---
|
||||
|
||||
## 配置拾取物交互物(与交互系统 P-016 配合)
|
||||
|
||||
收集系统通过 `AddToBackpack` Action 接收物品事件。每个拾取/翻找物的 `InteractableComponent.Data` 需配置:
|
||||
|
||||
- `Category` = **`杂物`**(关键!收集系统据此判定追踪品类)
|
||||
- `ItemId` = 唯一 ID(如 `marble_blue`、`card_wusong`)
|
||||
- `DisplayName` = 显示名(如 `彩色弹珠(蓝色)`)
|
||||
- `InteractEffect` 含 `AddToBackpack` Action(拾取物在动画物品消失时间点触发)
|
||||
|
||||
详见执行案「v0.1 杂物品类清单」10 个物品的 ItemId/名称映射。
|
||||
Reference in New Issue
Block a user