Files
spellforge/docs/design/tutorial_design.md
T
2026-07-20 10:56:52 +08:00

10 KiB
Raw Blame History

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。
# 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 核心接口

# 查询某步骤是否已完成
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 提示数据结构

# 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 旋转)
  • TutorialHintContaineranchor 节点世界坐标定位(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_DETECTEDCrashReporter 专用,已定义)。


六、可跳过 & 关闭机制

# 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):

TutorialManagerEventBusProfileManager 之后注册,
确保 _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 常量 关闭引导的持久化开关