20 KiB
45 · 教程与情境提示系统
命名空间
BaseGames.Tutorial
所属文档集 ← 返回索引 · 总览
依赖BaseGames.Core.Events·BaseGames.Player(能力查询)·BaseGames.UI(提示面板)·BaseGames.World(SaveSystem)
关联 01_InputSystem(按键提示)· 03_PlayerSystem(能力解锁)· 14_ProgressionSystem(能力门)· 31_SaveDataSchema(首次触发记录)
目录
- 系统总览
- 设计哲学:沉默式优先
- ContextualHintTrigger — 情境提示触发器
- AbilityTutorialSequence — 能力获取教学演出
- InGameHintPanel — 控制器参考面板
- TooltipPopup — 单行悬浮提示
- TutorialManager — 统一管理器
- SaveData 集成
- 多语言支持
- 编辑器友好设计
1. 系统总览
教程系统解决两个核心问题:
- 首次遭遇新机制 — 在玩家第一次接触特定机制时,以最小侵入方式给予提示
- 随时可查的操作参考 — 玩家按下专用按键可随时查看操作说明
教程系统职责:
├─ ContextualHintTrigger ← 区域/事件触发型提示(仅首次显示)
├─ AbilityTutorialSequence ← 能力解锁后的专项教学演出
├─ InGameHintPanel ← 玩家主动打开的完整操作说明面板
├─ TooltipPopup ← 非侵入式单行浮现提示(无需关闭)
└─ TutorialManager ← 统一状态管理、SaveData 读写
设计原则:
- 不使用强制暂停式教学("按 X 继续…"),除了第一次能力获取演出
- 提示持续时间短(3–5 秒自动消失),不阻碍游戏进行
- 所有提示的"已见过"状态记录在 SaveData,不重复显示
- 提示内容随当前输入设备(键盘/手柄)自动切换图标
2. 设计哲学:沉默式优先
2.1 教学优先级
第 1 选择(最优):通过关卡设计本身教会玩家(无任何文字/UI)
示例:在安全环境放置单独的弱敌,玩家自然尝试攻击
第 2 选择:TooltipPopup 非侵入单行提示(3秒自动消失)
示例:"↑ + 跳跃 穿越平台"(玩家首次接近单向平台时显示)
第 3 选择:ContextualHintTrigger(3–5秒,图文结合)
示例:图解弹反时机(玩家首次接触弹反训练区)
第 4 选择(最后手段):AbilityTutorialSequence(暂停游戏,专项演出)
仅用于:获得全新能力时,玩家必须完全理解才能继续游戏
2.2 三类教学区域
| 类型 | 位置 | 设计方式 |
|---|---|---|
| 关卡式教学 | 游戏开始的 Forest 区域 | 纯关卡设计,无 UI 提示 |
| 机制教学区 | 每个新机制首次出现的房间 | ContextualHintTrigger + 关卡辅助 |
| 危险预警 | 新 HazardType 首次出现 | TooltipPopup("这里危险,避免触碰") |
3. ContextualHintTrigger — 情境提示触发器
3.1 组件定义
namespace BaseGames.Tutorial
{
/// <summary>
/// 区域触发型提示:玩家进入触发体区域时首次显示图文提示。
/// 已显示过的提示不再重复(通过 TutorialManager 记录状态)。
/// </summary>
public class ContextualHintTrigger : MonoBehaviour
{
[SerializeField] ContextualHintSO _hint; // 提示内容 SO
[SerializeField] TriggerMode _triggerMode = TriggerMode.OnPlayerEnter;
[SerializeField] bool _showOnlyOnce = true; // false = 每次进入都显示
public enum TriggerMode
{
OnPlayerEnter, // 玩家进入触发体
OnPlayerInRange, // 玩家持续在范围内(首次进入触发)
OnEventRaised, // 收到指定事件频道信号
}
// TriggerMode.OnEventRaised 时使用
[SerializeField] VoidEventChannelSO _triggerChannel;
void OnTriggerEnter2D(Collider2D other)
{
if (_triggerMode != TriggerMode.OnPlayerEnter) return;
if (!other.CompareTag("Player")) return;
TryShowHint();
}
void TryShowHint()
{
if (_showOnlyOnce && TutorialManager.Instance.HasSeen(_hint.hintId)) return;
TutorialManager.Instance.ShowContextualHint(_hint);
}
}
}
3.2 ContextualHintSO — 提示内容数据
[CreateAssetMenu(menuName = "Tutorial/ContextualHint")]
public class ContextualHintSO : ScriptableObject
{
[Header("标识")]
public string hintId; // 唯一 ID,如 "Hint_Parry_FirstTime"(存 SaveData)
[Header("显示内容")]
public string localizationKey; // 本地化键(见 22_LocalizationSystem)
public Sprite illustrationSprite; // 图示(可选,null = 纯文字)
[Header("输入图标替换")]
public string[] actionNames; // InputActionAsset 中的 Action 名(自动替换 {Jump} 等占位符)
[Header("显示参数")]
public float displayDuration = 4f; // 0 = 手动关闭
public HintPosition position = HintPosition.BottomCenter;
public enum HintPosition { TopCenter, BottomCenter, TopLeft, Center }
}
文本格式(本地化字符串中):
使用 {ActionName} 占位符,运行时替换为当前设备的按键图标:
"按下 {Parry} 在正确时机弹反敌人攻击" → 键盘显示 [A],手柄显示 [LB]
3.3 ContextualHint UI 样式
╔══════════════════════════════════════╗
║ [插图区域 48×48px] 弹反机制 ║
║ ║
║ 在敌人攻击即将命中的瞬间 ║
║ 按下 [LB] 弹反攻击 ║
║ ║
║ ○ 淡出消失 ║
╚══════════════════════════════════════╝
- 出现动画:从底部滑入 + 淡入(0.3s)
- 消失动画:淡出(0.5s)
- 背景:半透明黑色圆角框
- 不阻挡玩家操作(HUD Canvas Overlay 层,不捕获输入)
4. AbilityTutorialSequence — 能力获取教学演出
4.1 触发时机
每当玩家通过 AbilityUnlock 获取新能力时,自动播放专项教学演出:
AbilityUnlock.OnTriggerEnter2D
→ PlayerStats.UnlockAbility(abilityType)
→ SaveManager.MarkCollected(unlockId)
→ 播放 acquisitionFeedback
→ TutorialManager.PlayAbilityTutorial(abilityType) ← 新增
4.2 教学演出流程
AbilityTutorialSequence 播放流程:
1. 暂停游戏(Time.timeScale = 0,但 UIAnimator 不受影响)
2. 全屏黑色遮罩淡入(0.4s)
3. 画面中央显示能力图标 + 动画(Scale 放大 + 粒子效果,1.2s)
4. 显示能力名称(大标题,从下向上飞入,0.6s)
5. 显示能力说明(简短一行,0.4s 延迟)
6. 显示操作说明(包含输入图标,0.4s 延迟)
7. 显示"按任意键继续"提示(0.6s 延迟后出现)
8. 玩家输入 → 遮罩淡出 → 恢复 Time.timeScale
4.3 AbilityTutorialSO — 能力教学配置
[CreateAssetMenu(menuName = "Tutorial/AbilityTutorial")]
public class AbilityTutorialSO : ScriptableObject
{
public AbilityType abilityType; // 对应能力类型
[Header("演出资产")]
public Sprite abilityIcon; // 能力大图标(128×128px)
public string abilityNameLocKey; // 本地化键:"ABILITY_DOUBLE_JUMP"
public string descriptionLocKey; // 简短说明(1–2 行)
public string[] usageInstructionLocKeys; // 操作说明(1–3 条,含 {ActionName} 占位)
public MMF_Player acquisitionFeedback; // 播放音效和粒子的 Feel Player
}
资产存放:Assets/ScriptableObjects/Tutorial/AbilityTutorials/
4.4 能力教学 SO 对照表
| 能力 | abilityType | 说明示例 |
|---|---|---|
| 双跳 | DoubleJump |
"在空中再次跳跃,到达更高的地方" |
| 冲刺 | AerialDash |
"快速冲向一个方向,躲避攻击或穿越间隙" |
| WallGrab | WallGrab |
"按住方向键贴向墙壁,抓住墙面停下" |
| 游泳 | Swim |
"进入液体后自动激活,按跳跃键上浮" |
| 天魂形态 | SkyForm |
"切换到天魂形态,解锁天魂技能" |
| 地魂形态 | EarthForm |
"切换到地魂形态,解锁地魂技能" |
| 命魂形态 | DeathForm |
"切换到命魂形态,解锁命魂技能" |
5. InGameHintPanel — 控制器参考面板
5.1 功能概述
玩家随时按下 Help 按键(默认 F1 / Select 按钮)打开完整操作说明面板,不暂停游戏:
╔════════════════════════════════════════════════════╗
║ 🎮 操作说明 [ESC关闭] ║
║────────────────────────────────────────────────────║
║ 基础操作 战斗 ║
║ ←/→ 移动 [Z/LB] 攻击 ║
║ [X/A] 跳跃 [X/A] 弹反 ║
║ 双击 [X/A] 双跳 ★ ←↓→ + [A] 方向攻击 ║
║ [C/RT] 冲刺 ★ ║
║ 形态切换 ║
║ 进阶移动 [1/D-↑] 天魂形态 ★ ║
║ 贴墙+方向 WallGrab ★ [2/D-↓] 地魂形态 ★ ║
║ WallGrab中 [X/A] 墙跳 [3/D-→] 命魂形态 ★ ║
║ ║
║ ★ = 需解锁后可用 [翻页 ▶] ║
╚════════════════════════════════════════════════════╝
灰显规则:已解锁的能力正常显示;未解锁的能力文字灰色+问号替代图标(保留神秘感但告知存在)。
5.2 InGameHintPanel 组件
public class InGameHintPanel : MonoBehaviour
{
// 由 CanvasGroup 控制显示/隐藏,不暂停 Time.timeScale
[SerializeField] CanvasGroup _panelGroup;
[SerializeField] InputReaderSO _input;
[SerializeField] PlayerStats _playerStats; // 查询已解锁能力
void OnEnable() => _input.HelpEvent += Toggle;
void OnDisable() => _input.HelpEvent -= Toggle;
void Toggle()
{
bool show = _panelGroup.alpha < 0.5f;
_panelGroup.DOFade(show ? 1f : 0f, 0.2f).SetAutoKill(true);
_panelGroup.interactable = _panelGroup.blocksRaycasts = show;
RefreshAbilityHighlights();
}
void RefreshAbilityHighlights()
{
// 根据 _playerStats.HasAbility() 动态更新图标颜色/灰显
foreach (var entry in _abilityEntries)
entry.SetUnlocked(_playerStats.HasAbility(entry.abilityType));
}
}
6. TooltipPopup — 单行悬浮提示
6.1 用途
最轻量的提示方式,不含图片,不阻塞操作:
示例显示效果(屏幕底部):
┌─────────────────────────────────────┐
│ ↓ + [X] 可以穿过此平台 │
└─────────────────────────────────────┘
(3秒后自动消失)
6.2 使用场景
| 触发时机 | 提示内容 |
|---|---|
| 玩家首次站上单向平台 | "↓ + {Jump} 可以穿过此平台" |
| 玩家首次接近 SavePoint | "按 {Interact} 存档并恢复 HP" |
| 玩家首次进入暗区 | "此区域能见度降低" |
| 玩家首次遇到可弹反弹射物 | "可弹反的弹射物会发出白光" |
| Geo 首次掉落在死亡处 | "取回死亡时掉落的 Geo" |
6.3 代码接口
// 任意系统调用(通过事件频道,零耦合):
_tooltipChannel.Raise(new TooltipData
{
localizationKey = "HINT_ONEWAY_PLATFORM",
duration = 3f,
showOnlyOnce = true,
hintId = "OWPlatform_FirstTime"
});
7. TutorialManager — 统一管理器
namespace BaseGames.Tutorial
{
/// <summary>
/// 单例管理器:记录已展示提示的状态,派发提示请求到 UI 层。
/// 位于 Persistent 场景,通过事件频道通信,不持有具体 UI 引用。
/// </summary>
[DefaultExecutionOrder(-50)]
public class TutorialManager : MonoBehaviour
{
public static TutorialManager Instance { get; private set; }
[SerializeField] TooltipEventChannelSO _tooltipChannel; // 订阅:发送单行提示
[SerializeField] HintEventChannelSO _hintChannel; // 订阅:发送图文提示
[SerializeField] AbilityTutorialSO[] _abilityTutorials;// 所有能力教学 SO 数组
HashSet<string> _seenHints = new(); // 运行时缓存,启动时从 SaveData 加载
void Awake()
{
Instance = this;
LoadSeenHints();
}
public bool HasSeen(string hintId) => _seenHints.Contains(hintId);
public void ShowContextualHint(ContextualHintSO hint)
{
_seenHints.Add(hint.hintId);
SaveSeenHints();
_hintChannel.Raise(hint);
}
public void PlayAbilityTutorial(AbilityType type)
{
var tutorial = System.Array.Find(_abilityTutorials, t => t.abilityType == type);
if (tutorial == null) return;
StartCoroutine(PlayTutorialSequence(tutorial));
}
IEnumerator PlayTutorialSequence(AbilityTutorialSO tutorial)
{
Time.timeScale = 0f;
_hintChannel.Raise(tutorial); // 专用全屏演出
// 等待玩家任意键确认(UnscaledTime)
yield return new WaitUntil(() => Input.anyKeyDown);
Time.timeScale = 1f;
}
void LoadSeenHints()
{
var data = SaveManager.Instance?.CurrentSave?.tutorial?.seenHints;
if (data != null) _seenHints = new HashSet<string>(data);
}
void SaveSeenHints()
{
if (SaveManager.Instance?.CurrentSave?.tutorial == null) return;
SaveManager.Instance.CurrentSave.tutorial.seenHints = new List<string>(_seenHints);
SaveManager.Instance.MarkDirty(); // 不立刻写盘,等下次存档点触发
}
}
}
8. SaveData 集成
8.1 SaveData 字段扩展
在 31_SaveDataSchema_Unified.md 的 C# 结构中添加 tutorial 节点:
// SaveData.cs 中新增字段
public class TutorialSaveData
{
public List<string> seenHints = new(); // 已展示过的提示 ID 列表
public List<string> completedTutorials = new(); // 已完成的能力教学 ID 列表
}
// 主 SaveData 结构
public class SaveData
{
// ...(原有字段)
public TutorialSaveData tutorial = new();
}
8.2 JSON Schema 扩展
{
"tutorial": {
"seenHints": ["OWPlatform_FirstTime", "Parry_FirstTime", "SavePoint_FirstTime"],
"completedTutorials": ["DoubleJump", "AerialDash"]
}
}
8.3 新游戏 / 存档重置行为
- 新游戏:
tutorial节点为空,所有提示将依序展示 - 读取存档:从
seenHints列表恢复已见过的提示,不重复展示 - 存档删除:整个
tutorial节点重置为空
9. 多语言支持
所有提示文本通过 localizationKey 引用 22_LocalizationSystem 的字符串表,不在 SO 中硬编码文字:
// 使用方式示例
string text = LanguageManagerSO.Instance.GetString(hint.localizationKey);
// 输入图标替换(替换 {ActionName} 占位符)
text = InputIconReplacer.Replace(text, hint.actionNames);
// 输出示例(手柄):"按下 [图标:LB] 在弹反窗口内反击"
本地化键命名规范:HINT_{机制简称}_{情境}_{语言变体}
示例:
HINT_ONEWAY_FIRST→ "↓ + {Jump} 可以穿过此平台"HINT_PARRY_TIMING→ "在攻击命中瞬间按 {Parry}"HINT_SHADE_COLLECT→ "拾取遗骸可取回死亡时掉落的 Geo"
10. 编辑器友好设计
10.1 TutorialDebugOverlay
运行时叠加层(仅 UNITY_EDITOR || DEVELOPMENT_BUILD):
TutorialDebugOverlay(按 F2 切换显示):
┌─ Tutorial Debug ─────────────────────────────────┐
│ 已见提示数: 12 / 28 │
│ 当前队列: [空] │
│ ─────────────────────────────────────────────── │
│ [重置所有提示] [标记所有已见] [导出未见列表] │
└──────────────────────────────────────────────────┘
10.2 ContextualHintTrigger 自定义 Inspector
ContextualHintTrigger Inspector:
┌─ Hint SO ──────────────────────────────────────┐
│ Hint: [OWPlatform_Hint] │
│ hintId: "OWPlatform_FirstTime" │
│ Duration: 3.0s │
│ ───────────────────────────────────────────── │
│ [▶ 预览提示效果(Editor Play Mode 中)] │
│ [已见状态: 未见 ▶ 点击模拟"已见"] │
└────────────────────────────────────────────────┘
10.3 HintTrigger Gizmos
ContextualHintTrigger的触发区域在 Scene 视图显示为浅蓝色边框虚线框- 框内显示 hintId 字符串(白色小字)
- 鼠标悬停时显示提示文本预览(Tool Tip)
附录:推荐提示 ID 规范
所有 hintId 字符串集中在静态类中定义(类似 GameFlags):
// Assets/Scripts/Tutorial/TutorialHintIds.cs
public static class TutorialHintIds
{
// 移动
public const string OWPlatformDropDown = "OWPlatform_DropDown";
public const string WallGrabIntro = "WallGrab_Intro";
public const string DoubleJumpIntro = "DoubleJump_Intro";
public const string DashIntro = "Dash_Intro";
// 战斗
public const string ParryTimingIntro = "Parry_Timing_Intro";
public const string ParryableProjectile = "Parry_Projectile";
public const string BossPhaseChange = "Boss_PhaseChange";
// 世界
public const string SavePointFirst = "SavePoint_First";
public const string ShadeCollect = "Shade_Collect";
public const string BenchFastTravel = "Bench_FastTravel";
public const string ShopFirst = "Shop_First";
// 形态系统
public const string FormSwitchIntro = "Form_Switch_Intro";
public const string SkyFormSkillIntro = "Form_Sky_Skill";
// 危险
public const string PoisonZoneFirst = "Hazard_Poison_First";
public const string LavaZoneFirst = "Hazard_Lava_First";
public const string InstantKillFirst = "Hazard_InstantKill_First";
}
文档版本 1.0 · 2026