初次提交

This commit is contained in:
2026-07-20 10:56:52 +08:00
commit 7bcc0026e0
462 changed files with 50191 additions and 0 deletions
+284
View File
@@ -0,0 +1,284 @@
# FTUE / 新手引导系统架构设计
# Tutorial & Onboarding System Architecture
> **范围**:本文档涵盖游戏的首次用户体验(FTUE)、上下文提示、引导状态机,以及
> `TutorialManager` Autoload 的完整设计规范。
> 本文档以 `architecture_design.md` 为基础,所有运行时接口遵循 §6.1 语言分区原则。
---
## 一、设计目标
| 目标 | 度量标准 |
| :--- | :--- |
| 前 3 分钟玩家留存 | 90%+ 玩家完成首波(Wave 1)且尝试第一次施法 |
| 零文字强制阅读 | 教学信息通过上下文提示传达,不弹出强制阅读对话框 |
| 可跳过原则 | 所有引导步骤可在设置中永久关闭,或在进行中跳过 |
| 渐进式复杂度 | W1–W3 仅展示核心移动+射击;W4+ 逐步解锁进阶提示 |
---
## 二、TutorialManager Autoload
### 2.1 职责边界
- **GDScript 层**`tutorial_manager.gd`):状态读写、EventBus 订阅、提示节点显示/隐藏。
- **不含热路径**:提示触发频率极低(每场景级别),全部保留在 GDScript。
```gdscript
# scripts/autoloads/tutorial_manager.gd
extends Node
## 教学步骤枚举(顺序即解锁顺序)
enum TutorialStep {
MOVE = 0, # WASD 移动
CAST = 1, # 首次自动施法(Wand 冷却后自动提示)
PICK_SPELL = 2, # 首次拾取法术卡
OPEN_SHOP = 3, # 首次进入商店
EQUIP_CORE = 4, # 首次装备 Core
KILL_10 = 5, # 击杀 10 个敌人
WAVE_CLEARED = 6, # 通过 Wave 1
RESONANCE_HINT = 7, # 首次触发共鸣(S3 解锁)
COMPLETED = 99, # 引导全部完成
}
## 已完成步骤的持久化集合(存入 ProfileManager
var _completed_steps: PackedInt32Array = PackedInt32Array()
## 当前激活提示步骤(-1 = 无)
var _active_step: int = -1
func _ready() -> void:
# 从 ProfileManager 恢复已完成状态
_completed_steps = ProfileManager.get_int_array(
"tutorial_completed", PackedInt32Array())
# 订阅进度事件
EventBus.subscribe(EventID.PLAYER_SPAWNED, _on_player_spawned)
EventBus.subscribe(EventID.SPELL_CAST, _on_spell_cast)
EventBus.subscribe(EventID.SHOP_OPENED, _on_shop_opened)
EventBus.subscribe(EventID.ENEMY_KILLED, _on_enemy_killed)
EventBus.subscribe(EventID.WAVE_CLEARED, _on_wave_cleared)
EventBus.subscribe(EventID.RESONANCE_TRIGGERED, _on_resonance)
```
### 2.2 状态机定义
```
[初始状态]
MOVE ──────────────── 检测:玩家首帧输入 WASD 后标记完成
CAST ──────────────── 检测:EventID.SPELL_CAST 首次触发
PICK_SPELL ─────────── 检测:拾取 loot 掉落的法术卡
OPEN_SHOP ──────────── 检测:Wave 1 通关后自动弹出商店提示
EQUIP_CORE ─────────── 检测:首次从商店装备任意 Core
KILL_10 ────────────── 检测:ENEMY_KILLED 事件累计 10 次
WAVE_CLEARED ───────── 检测:WAVE_CLEAREDwave_num=1
RESONANCE_HINT ─────── 检测:RESONANCE_TRIGGEREDS3 起激活)
COMPLETED ──────────── 持久化到 ProfileManager,停止所有提示
```
- **前向跳跃**:若玩家自行完成某步(无提示出现),步骤自动标记完成,无需显示提示。
- **后向保护**:已完成步骤不会因新 Run 而重置(存 ProfileManager 而非 RunData)。
### 2.3 核心接口
```gdscript
# 查询某步骤是否已完成
func is_completed(step: TutorialStep) -> bool:
return step in _completed_steps
# 手动标记完成(商店系统 / 法杖编辑器调用)
func mark_complete(step: TutorialStep) -> void:
if is_completed(step): return
_completed_steps.append(int(step))
ProfileManager.set_int_array("tutorial_completed", _completed_steps)
_hide_hint()
EventBus.emit(EventID.TUTORIAL_STEP_COMPLETED, step)
# 显示上下文提示(锚定到 UI 节点旁)
func show_hint(step: TutorialStep, anchor: Control,
offset: Vector2 = Vector2.ZERO) -> void:
if is_completed(step): return
_active_step = step
# 委托给 UIManager 渲染气泡提示
UIManager.show_tutorial_hint(
TutorialHintData.build(step), anchor, offset)
func _hide_hint() -> void:
if _active_step == -1: return
UIManager.hide_tutorial_hint()
_active_step = -1
```
---
## 三、上下文提示系统(Contextual Hints
### 3.1 提示数据结构
```gdscript
# resources/tutorial/tutorial_hint_data.gd
class_name TutorialHintData
extends Resource
@export var step: int # TutorialStep 枚举值
@export var icon_key: String # 对应 UIAtlas 中的图标键(如 "wasd_icon"
@export var text_key: String # i18n 键(tr(text_key) 使用)
@export var arrow_dir: int # 0=上, 1=右, 2=下, 3=左(箭头指向目标)
@export var duration: float = 0.0 # 0 = 手动关闭,>0 = 自动消失秒数
static func build(step: TutorialManager.TutorialStep) -> TutorialHintData:
return _HINT_TABLE[step]
# 提示内容表(运行时常量,避免 JSON 热加载)
const _HINT_TABLE: Dictionary = {
0: preload("res://resources/tutorial/hint_move.tres"),
1: preload("res://resources/tutorial/hint_cast.tres"),
2: preload("res://resources/tutorial/hint_pick_spell.tres"),
3: preload("res://resources/tutorial/hint_shop.tres"),
4: preload("res://resources/tutorial/hint_equip_core.tres"),
5: preload("res://resources/tutorial/hint_kill10.tres"),
6: preload("res://resources/tutorial/hint_wave_cleared.tres"),
7: preload("res://resources/tutorial/hint_resonance.tres"),
}
```
### 3.2 提示触发时机
| 步骤 | 触发时机 | 自动消失 | 跳过方式 |
| :--- | :--- | :--- | :--- |
| MOVE | 游戏开始后 1s(玩家还未移动) | 首次移动后 | 任意移动 |
| CAST | 法杖冷却恢复后 2s(玩家未施法) | 首次施法后 | 施法 |
| PICK_SPELL | 首个法术 loot 落地后 3s | 拾取后 | 走到 loot 上 |
| OPEN_SHOP | Wave 1 通关时自动进入商店流程 | 商店打开后 | 自动 |
| EQUIP_CORE | 商店内有 Core 可购买时 | 购买后 | 购买 |
| KILL_10 | 击杀计数达 5 时预显提示 | 计数达 10 | 击杀 |
| WAVE_CLEARED | Wave 1 通关瞬间 | 5s 后 | 点击 |
| RESONANCE_HINT | 首次编辑法杖时检测邻接 | 触发共鸣后 | 触发共鸣 |
### 3.3 提示 UI 节点结构
```
CombatScene
└── UILayer (CanvasLayer z_index=100)
└── TutorialHintContainer # UIManager 管理的专用节点
├── HintBubble (Panel)
│ ├── Icon (TextureRect)
│ └── Label (text = tr(hint.text_key))
└── Arrow (TextureRect, 根据 arrow_dir 旋转)
```
- `TutorialHintContainer``anchor` 节点世界坐标定位(`force_update_transform` 每帧同步)。
- 气泡使用 `AnimationPlayer` 播放 0.2s 淡入动画,消失时 0.15s 淡出。
- 同一时刻最多显示 1 个提示(显示新提示时先隐藏旧提示)。
---
## 四、首 3 分钟玩家流(First 3 Minutes Flow
```
T=0s 游戏载入 → CombatScene 初始化
T=1s [提示] WASD 移动(气泡锚定玩家位置右侧,arrow→左)
T=~5s 玩家完成移动 → MOVE 标记完成 → 提示消失
T=~8s 法杖冷却结束 → [提示] 观察法术已自动施发(施法为自动)
T=~15s 首个敌人被击杀 → loot 掉落 → [提示] 拾取法术卡
T=~30s 玩家拾取法术卡 → PICK_SPELL 标记完成
T=~60s Wave 1 清场 → 自动进入商店
T=商店 [提示] 尝试装备 Core(若商店有 Corearrow→Core 格)
T=~90s 购买/关闭商店 → Wave 2 开始
T=~120s 已掌握基本循环,FTUE 完成度 ~70%
T=Wave 4+ 触发共鸣时 → 最终提示显示
```
---
## 五、关键 EventID 扩展
> 以下 EventID 需追加到 `event_id.gd`(当前最大 ID = 17):
| ID | 常量名 | 负载 | 触发方 | 监听方 |
| :--- | :--- | :--- | :--- | :--- |
| 18 | `TUTORIAL_STEP_COMPLETED` | `step: int` | TutorialManager | UIManager(显示解锁提示)|
| 19 | `SHOP_OPENED` | — | ShopManager | TutorialManager |
| 20 | `RESONANCE_TRIGGERED` | `recipe_id: String` | SpellEvaluator | TutorialManager, VFXManager |
| 21 | `ACHIEVEMENT_UNLOCKED` | `achievement_id: String` | AchievementManager | Steam 插件层 |
> **注意**ID 0 为 `CRASH_DETECTED`CrashReporter 专用,已定义)。
---
## 六、可跳过 & 关闭机制
```gdscript
# settings_manager.gd 新增项
const KEY_TUTORIAL_DISABLED: String = "tutorial_disabled"
# TutorialManager._ready() 中检查
func _ready() -> void:
if ProfileManager.get_bool(SettingsManager.KEY_TUTORIAL_DISABLED, false):
# 将所有步骤标记为已完成,彻底关闭引导
for s in TutorialStep.values():
_completed_steps.append(s)
return
# 正常初始化...
```
设置菜单提供「关闭新手提示」开关,写入 `ProfileManager`(持久化跨 Run)。
**重置入口**`SettingsManager.reset_tutorial()` → 清空 `tutorial_completed` 并重置标志,
下次启动重新触发完整 FTUE 流程(适用于练习账号 / QA 测试)。
---
## 七、文件清单
```
scripts/autoloads/
tutorial_manager.gd # 核心 Autoload(本文档 §2 实现)
resources/tutorial/
hint_move.tres # TutorialHintDataMOVE 步骤)
hint_cast.tres
hint_pick_spell.tres
hint_shop.tres
hint_equip_core.tres
hint_kill10.tres
hint_wave_cleared.tres
hint_resonance.tres
scenes/ui/
tutorial_hint_bubble.tscn # HintBubble + Arrow 节点树
```
**Autoload 注册顺序**`project.godot`):
> `TutorialManager` 在 `EventBus` 和 `ProfileManager` 之后注册,
> 确保 `_ready()` 时可正常读取持久化数据并订阅事件。
---
## 八、与其他系统的接口约定
| 系统 | 接口 | 说明 |
| :--- | :--- | :--- |
| `UIManager` | `show_tutorial_hint(data, anchor, offset)` / `hide_tutorial_hint()` | 渲染和动画委托给 UIManager |
| `ProfileManager` | `get_int_array / set_int_array("tutorial_completed", ...)` | 跨 Run 持久化已完成步骤 |
| `EventBus` | 订阅 ID 1/6/11/18~21 等 | 只读订阅,不 emit 游戏逻辑事件 |
| `SpellEvaluator` | 无直接依赖 | 共鸣触发通过 `RESONANCE_TRIGGERED` 事件解耦 |
| `SettingsManager` | `KEY_TUTORIAL_DISABLED` 常量 | 关闭引导的持久化开关 |