15 KiB
15 KiB
18 · 过场动画系统(Cutscene)
命名空间
BaseGames.Cutscene
所属文档集 ← 返回索引 · 总览
依赖BaseGames.Core.Events·BaseGames.UI(黑屏/字幕)· Unity Timeline · Cinemachine
目录
- 系统总览
- CutsceneManager
- CutsceneTrigger
- 过场动画 Timeline 规范
- Timeline Track 扩展
- 跳过机制
- 过场与对话系统集成
- 输入锁定与 GameState
- 保存/重播机制
- 事件频道
- 编辑器友好设计
1. 系统总览
过场动画系统基于 Unity Timeline,提供序列化、可预览的过场演出。
过场系统职责:
├─ CutsceneManager → 全局过场状态机(播放/暂停/跳过)
├─ CutsceneTrigger → 场景中的触发器(位置/事件触发过场)
├─ Timeline 规范 → 统一的 Track 分层规范
├─ Timeline Tracks → 自定义 Signal Track(发布 SO 事件频道)
├─ 跳过机制 → 长按跳过,带过场进度条
└─ GameState 集成 → 过场期间切换到 Cutscene 状态,锁定输入
零耦合原则:过场通过自定义 SignalEmitterClip 发布 SO 事件频道,过场动画与游戏逻辑互不直接引用。
2. CutsceneManager
CutsceneManager 常驻 Persistent 场景,管理过场播放状态。
namespace BaseGames.Cutscene
{
public class CutsceneManager : MonoBehaviour
{
[Header("组件引用")]
[SerializeField] PlayableDirector _director; // 运行时绑定活跃 Timeline
[Header("事件频道")]
[SerializeField] CutsceneEventChannelSO _onCutsceneStarted; // 发布(传入 CutsceneSO)
[SerializeField] VoidEventChannelSO _onCutsceneEnded; // 发布
[SerializeField] VoidEventChannelSO _onCutsceneSkipped; // 发布
[SerializeField] GameStateEventChannelSO _onGameStateChanged; // 发布
// 运行时状态
CutsceneSO _currentCutscene;
bool _isPlaying;
bool _skipHeld;
float _skipHoldTimer;
const float SKIP_HOLD_DURATION = 1.5f; // 长按 1.5s 跳过
public bool IsPlaying => _isPlaying;
// ─────────────── 公开 API ───────────────
public void Play(CutsceneSO cutscene)
{
if (_isPlaying) return;
_currentCutscene = cutscene;
_isPlaying = true;
// 1. 切换 GameState → Cutscene(锁定玩家输入)
_onGameStateChanged.Raise(GameState.Cutscene);
// 2. 绑定并播放 Timeline
_director.playableAsset = cutscene.timelineAsset;
BindObjects(cutscene);
_director.Play();
// 3. 发布事件(UI/Audio 订阅)
_onCutsceneStarted.Raise(cutscene);
}
public void Skip()
{
if (!_isPlaying || !_currentCutscene.isSkippable) return;
_director.Stop();
FinishCutscene(skipped: true);
}
void Update()
{
if (!_isPlaying) return;
// 长按跳过检测
if (_skipHeld)
{
_skipHoldTimer += Time.unscaledDeltaTime;
_onSkipProgress?.Raise(_skipHoldTimer / SKIP_HOLD_DURATION); // 进度条更新
if (_skipHoldTimer >= SKIP_HOLD_DURATION)
Skip();
}
// Timeline 自然结束
if (_director.state == PlayState.Paused && _director.time >= _director.duration)
FinishCutscene(skipped: false);
}
public void OnSkipButtonDown() { _skipHeld = true; _skipHoldTimer = 0f; }
public void OnSkipButtonUp() { _skipHeld = false; _onSkipProgress?.Raise(0f); }
void FinishCutscene(bool skipped)
{
_isPlaying = false;
if (skipped)
_onCutsceneSkipped.Raise();
else
_onCutsceneEnded.Raise();
// 恢复 GameState
_onGameStateChanged.Raise(GameState.Gameplay);
// 标记为已播放(保存进度)
SaveManager.Instance.MarkCutscenePlayed(_currentCutscene.cutsceneId);
}
/// <summary>
/// 将 CutsceneSO 中声明的对象绑定到 Timeline Bindings(如摄像机、NPC)。
/// </summary>
void BindObjects(CutsceneSO cutscene)
{
foreach (var binding in cutscene.bindings)
{
_director.SetGenericBinding(binding.track, binding.targetObject);
}
}
}
}
CutsceneSO — 过场元数据
[CreateAssetMenu(menuName = "Cutscene/CutsceneData")]
public class CutsceneSO : ScriptableObject
{
[Header("基础信息")]
public string cutsceneId; // 全局唯一 ID,存入 SaveData 已播放列表
public string displayName; // 编辑器显示名称
public TimelineAsset timelineAsset; // 对应的 Timeline 资产
public bool isSkippable; // 是否可跳过(序章/结局通常不可跳过)
public bool playOnlyOnce; // 已看过后不再自动触发(可在菜单重播)
[Header("绑定声明")]
public CutsceneBinding[] bindings; // Track → 场景中具体对象的绑定表
[Header("缩略图")]
public Sprite thumbnail; // 用于"过场回放"菜单显示
}
[Serializable]
public struct CutsceneBinding
{
public PlayableAsset track; // Timeline Track 的 PlayableAsset 引用
public UnityEngine.Object targetObject; // 绑定目标(CinemachineVirtualCamera / Transform 等)
}
3. CutsceneTrigger
CutsceneTrigger 放置在场景中,当满足触发条件时启动过场。
public class CutsceneTrigger : MonoBehaviour
{
[Header("过场配置")]
[SerializeField] CutsceneSO _cutscene;
[SerializeField] TriggerCondition _condition; // OnEnter / OnEvent / OnInteract
[Header("事件触发(OnEvent 模式)")]
[SerializeField] VoidEventChannelSO _listenChannel; // 监听此频道触发过场
[Header("过场管理器")]
[SerializeField] CutsceneManager _cutsceneManager;
bool _hasTriggered;
void OnEnable()
{
if (_condition == TriggerCondition.OnEvent && _listenChannel != null)
_listenChannel.OnEventRaised += TryTrigger;
}
void OnDisable()
{
if (_listenChannel != null)
_listenChannel.OnEventRaised -= TryTrigger;
}
void OnTriggerEnter2D(Collider2D other)
{
if (_condition != TriggerCondition.OnEnter) return;
if (!other.CompareTag("Player")) return;
TryTrigger();
}
void TryTrigger()
{
if (_hasTriggered) return;
if (_cutscene.playOnlyOnce && SaveManager.Instance.IsCutscenePlayed(_cutscene.cutsceneId)) return;
_hasTriggered = _cutscene.playOnlyOnce;
_cutsceneManager.Play(_cutscene);
}
}
public enum TriggerCondition { OnEnter, OnEvent, OnInteract }
4. 过场动画 Timeline 规范
每个过场 Timeline 遵循以下标准化 Track 分层:
CutsceneTimeline.playable
├── [00] Activation Track — Player ← 过场期间隐藏/显示玩家角色
├── [01] Activation Track — HUD ← 隐藏 HUD(可选,由 CutsceneSO.hideHUD 控制)
├── [02] Cinemachine Track — Cam_Cutscene ← 专用过场虚拟相机(不影响游戏相机状态机)
│ └── CinemachineShot Clips(各镜头切换)
├── [03] Animation Track — NPC_A ← NPC 角色动画(Animator)
├── [04] Animation Track — NPC_B
├── [05] Audio Track — BGM ← 背景音乐(可淡入淡出)
├── [06] Audio Track — SFX ← 音效时间点
├── [07] Signal Track — GameEvents ← 在关键帧发布 SO 事件频道(见 §5)
└── [08] Control Track — SubTimelines(可选)← 嵌套子 Timeline
专用过场相机
每个过场使用独立的 CinemachineVirtualCamera(优先级在过场期间最高):
[VC_Cutscene_01]
├── CinemachineVirtualCamera
│ ├── Priority: 100(过场期间高于游戏相机)
│ └── FollowTarget: 空(固定视角)或临时目标
└── (Timeline 结束时,CutsceneManager 恢复游戏相机优先级)
5. Timeline Track 扩展
SignalEmitterClip — SO 事件频道发射器
自定义 Signal Track Clip,允许在 Timeline 的任意帧发布 SO 事件频道:
[Serializable]
public class SignalEmitterClip : PlayableAsset, ITimelineClipAsset
{
public VoidEventChannelSO voidChannel;
public StringEventChannelSO stringChannel;
public string stringPayload;
public ClipCaps clipCaps => ClipCaps.None;
public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
=> ScriptPlayable<SignalEmitterBehaviour>.Create(graph,
new SignalEmitterBehaviour { Clip = this });
}
public class SignalEmitterBehaviour : PlayableBehaviour
{
public SignalEmitterClip Clip;
bool _fired;
public override void OnBehaviourPlay(Playable playable, FrameData info)
{
if (_fired) return;
_fired = true;
Clip.voidChannel?.Raise();
if (!string.IsNullOrEmpty(Clip.stringPayload))
Clip.stringChannel?.Raise(Clip.stringPayload);
}
public override void OnBehaviourPause(Playable playable, FrameData info)
=> _fired = false; // 重置,支持 Timeline 预览反复播放
}
使用示例:
- 在
t=5.0s发布OnBossFightStarted:关闭 Boss 房间门、触发 Boss BGM - 在
t=12.0s发布OnDialogueStart:启动对话系统显示字幕
SubtitleTrack — 字幕轨道
[TrackClipType(typeof(SubtitleClip))]
[TrackBindingType(typeof(SubtitleUI))]
public class SubtitleTrack : TrackAsset { }
[Serializable]
public class SubtitleClip : PlayableAsset
{
[TextArea(2, 4)]
public string text;
public Color textColor = Color.white;
}
6. 跳过机制
跳过 UI
Canvas_Cutscene (Sorting Order: 50,过场期间显示)
└── SkipPanel (右下角)
├── SkipLabel ("长按 X / Space 跳过")
└── SkipFillBar (Image,fillAmount 0→1,随长按进度填充)
跳过流程
玩家长按 Skip 键
→ InputReader 持续发布 OnSkipHeld 频道
→ CutsceneManager.OnSkipButtonDown()
→ 每帧累计 _skipHoldTimer
→ 发布 OnSkipProgress(0~1) → SkipFillBar.fillAmount 更新
→ _skipHoldTimer >= 1.5s → CutsceneManager.Skip()
→ _director.Stop()
→ FinishCutscene(skipped: true)
→ 恢复 GameState.Gameplay
松开 Skip 键(未达到 1.5s)
→ _skipHoldTimer 归零,SkipFillBar 清空
7. 过场与对话系统集成
过场中的对话通过 Signal Track → DialogueManager 触发,与游戏内对话复用同一套系统:
Signal Track t=3.0s: Raise OnDialogueStart("DG_Cutscene_Intro")
│
▼
DialogueManager.StartDialogue("DG_Cutscene_Intro")
│
├─ 暂停 Timeline(_director.Pause())
├─ 显示对话气泡 / 全屏对话 UI(取决于 DialogueGraphSO.displayMode)
└─ 对话结束后 → 发布 OnDialogueEnded → CutsceneManager 恢复播放 (_director.Resume())
8. 输入锁定与 GameState
| 阶段 | GameState | 输入 ActionMap | 说明 |
|---|---|---|---|
| 正常游戏 | Gameplay |
Player |
全功能输入 |
| 过场播放中 | Cutscene |
Cutscene |
仅监听 Skip 键(长按) |
| 对话中(过场内) | Dialogue |
Dialogue |
仅监听 Confirm/Skip |
| 过场结束后 | Gameplay |
Player |
自动恢复 |
InputReader 在 Cutscene ActionMap 下只暴露 OnSkipPressed / OnSkipReleased 事件,玩家无法在过场中移动或攻击。
9. 保存/重播机制
过场播放记录
"cutscenes": {
"playedIds": ["CS_Intro", "CS_Boss01_Entrance", "CS_MidgameReveal"]
}
- 每次
FinishCutscene时,SaveManager.MarkCutscenePlayed(id)追加到playedIds[] CutsceneSO.playOnlyOnce = true的过场,触发前检查是否已在列表中
过场回放(剧情回顾菜单,P2)
- 主菜单 → 剧情回顾 → 列出所有
CutsceneSO.playOnlyOnce = true且已播放的过场 - 点击缩略图:不加载游戏场景,直接在独立场景播放 Timeline(无跳过限制)
10. 事件频道
| 频道资产 | 类型 | 发布方 | 主要订阅方 |
|---|---|---|---|
OnCutsceneStarted.asset |
CutsceneEventChannelSO |
CutsceneManager |
HUD(隐藏)、AudioManager(过场 BGM)、InputReader(切 ActionMap) |
OnCutsceneEnded.asset |
VoidEventChannelSO |
CutsceneManager |
HUD(显示)、AudioManager(恢复 BGM)、InputReader(恢复 ActionMap) |
OnCutsceneSkipped.asset |
VoidEventChannelSO |
CutsceneManager |
同上(快速执行收尾逻辑) |
OnSkipProgress.asset |
FloatEventChannelSO(0~1) |
CutsceneManager |
SkipFillBar(UI 填充) |
OnSubtitleShow.asset |
SubtitleEventChannelSO |
SubtitleTrack |
SubtitleUI(显示字幕) |
OnSubtitleHide.asset |
VoidEventChannelSO |
SubtitleTrack |
SubtitleUI(隐藏字幕) |
11. 编辑器友好设计
CutsceneManager Inspector(Play Mode)
┌─ CutsceneManager ──────────────────────────────────────────┐
│ State: Playing Cutscene: CS_Boss01_Entrance │
│ Progress: ████████░░░░ 8.2s / 15.0s │
│ Skippable: ✓ PlayOnlyOnce: ✓ │
│ ───────────────────────────────────────────────────── │
│ [立即跳过] [重新播放] [暂停/继续] │
└────────────────────────────────────────────────────────────┘
CutsceneSO 自定义 Inspector
- 拖入
timelineAsset后,自动检测 Timeline 中的绑定 Track,并在bindings[]列表中预填槽位 - 缩略图:
[自动截图]按钮在 Play Mode 下截取 Timeline 第 1 帧作为缩略图
CutsceneTrigger Gizmo
- Scene 视图中显示黄色边框 + 触发器尺寸
- 显示文字标注:
[过场名] 触发条件: OnEnter - 若
_cutscene == null,显示红色警告 Gizmo:⚠ 缺少 CutsceneSO