多轮审查和修复
This commit is contained in:
@@ -9,7 +9,11 @@
|
||||
"rootNamespace": "BaseGames.Cutscene",
|
||||
"references": [
|
||||
"BaseGames.Core.Events",
|
||||
"BaseGames.Dialogue"
|
||||
"BaseGames.Input",
|
||||
"BaseGames.Dialogue",
|
||||
"BaseGames.World",
|
||||
"BaseGames.Camera",
|
||||
"Unity.Timeline"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"overrideReferences": false,
|
||||
|
||||
107
Assets/Scripts/Cutscene/CutsceneManager.cs
Normal file
107
Assets/Scripts/Cutscene/CutsceneManager.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Input;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Playables;
|
||||
using UnityEngine.Timeline;
|
||||
|
||||
namespace BaseGames.Cutscene
|
||||
{
|
||||
/// <summary>
|
||||
/// Unity Timeline 过场动画封装(架构 14_NarrativeModule §11)。
|
||||
/// 负责播放/停止 CutsceneSO;切换 Action Map;广播过场开始/结束事件。
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(PlayableDirector))]
|
||||
public class CutsceneManager : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private InputReaderSO _inputReader;
|
||||
|
||||
[Header("事件频道")]
|
||||
[SerializeField] private VoidEventChannelSO _onCutsceneStarted;
|
||||
[SerializeField] private VoidEventChannelSO _onCutsceneEnded;
|
||||
[SerializeField] private StringEventChannelSO _onPlayCutsceneById; // 由 PlayCutsceneAction 触发
|
||||
|
||||
[Header("过场素材库(PlayById 查找用)")]
|
||||
[SerializeField] private CutsceneSO[] _registeredCutscenes;
|
||||
|
||||
private PlayableDirector _director;
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
/// <summary>是否正在播放过场。</summary>
|
||||
public bool IsPlaying => _director != null && _director.state == PlayState.Playing;
|
||||
|
||||
// ── 生命周期 ──────────────────────────────────────────────────────
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_director = GetComponent<PlayableDirector>();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_onPlayCutsceneById?.Subscribe(PlayById).AddTo(_subs);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
_subs.Clear();
|
||||
}
|
||||
|
||||
// ── 公开 API ──────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>通过 cutsceneId 查找并播放(供 PlayCutsceneAction 通过事件触发)。</summary>
|
||||
public void PlayById(string cutsceneId)
|
||||
{
|
||||
if (_registeredCutscenes == null) return;
|
||||
foreach (var cs in _registeredCutscenes)
|
||||
if (cs != null && cs.cutsceneId == cutsceneId) { PlayCutscene(cs); return; }
|
||||
|
||||
Debug.LogWarning($"[CutsceneManager] 找不到 cutsceneId='{cutsceneId}'");
|
||||
}
|
||||
|
||||
/// <summary>播放指定过场 SO。</summary>
|
||||
public void PlayCutscene(CutsceneSO cutscene)
|
||||
{
|
||||
if (cutscene == null || IsPlaying) return;
|
||||
_director.playableAsset = cutscene.Timeline;
|
||||
|
||||
// 应用 Track → GameObject 绑定
|
||||
if (cutscene.Bindings != null && cutscene.Timeline != null)
|
||||
{
|
||||
var outputs = cutscene.Timeline.outputs;
|
||||
int idx = 0;
|
||||
foreach (var output in outputs)
|
||||
{
|
||||
if (idx >= cutscene.Bindings.Length) break;
|
||||
var binding = cutscene.Bindings[idx];
|
||||
if (!string.IsNullOrEmpty(binding.trackName) && output.streamName == binding.trackName
|
||||
&& binding.target != null)
|
||||
{
|
||||
_director.SetGenericBinding(output.sourceObject, binding.target);
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
|
||||
_director.stopped += OnCutsceneStopped;
|
||||
_director.Play();
|
||||
|
||||
_inputReader?.EnableUIInput();
|
||||
_onCutsceneStarted?.Raise();
|
||||
}
|
||||
|
||||
/// <summary>立即停止当前过场。</summary>
|
||||
public void StopCutscene()
|
||||
{
|
||||
if (_director != null) _director.Stop();
|
||||
}
|
||||
|
||||
// ── 内部 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void OnCutsceneStopped(PlayableDirector director)
|
||||
{
|
||||
_director.stopped -= OnCutsceneStopped;
|
||||
_inputReader?.EnableGameplayInput();
|
||||
_onCutsceneEnded?.Raise();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Cutscene/CutsceneManager.cs.meta
Normal file
11
Assets/Scripts/Cutscene/CutsceneManager.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7b85bd04fd48a5e41b7e675944f6c355
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
52
Assets/Scripts/Cutscene/CutsceneSO.cs
Normal file
52
Assets/Scripts/Cutscene/CutsceneSO.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using BaseGames.Camera;
|
||||
using BaseGames.Dialogue;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Timeline;
|
||||
|
||||
namespace BaseGames.Cutscene
|
||||
{
|
||||
/// <summary>
|
||||
/// 过场动画数据资产(架构 14_NarrativeModule §11.5)。
|
||||
/// 定义一段完整的过场内容:Timeline 资产、Track 绑定、摄像机混合、可叠加对话层。
|
||||
/// 资产路径:Assets/ScriptableObjects/Cutscene/CS_{SceneId}_{ContextId}.asset
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Cutscene/Cutscene")]
|
||||
public class CutsceneSO : ScriptableObject
|
||||
{
|
||||
[Header("Identity")]
|
||||
public string cutsceneId; // 全局唯一,用于存档去重和 CutsceneManager.PlayById 查找
|
||||
public string displayName;
|
||||
public bool playOnlyOnce; // true → 仅首次播放(后续触发跳过)
|
||||
public bool isSkippable = true; // 是否允许玩家跳过
|
||||
public Sprite thumbnail; // 过场预览图(剧情重放 UI 用)
|
||||
|
||||
[Header("Timeline")]
|
||||
public TimelineAsset Timeline; // Unity Timeline 资产
|
||||
|
||||
[Header("Timeline Bindings")]
|
||||
// Track 与场景 GameObject 的绑定关系(避免 PlayableDirector 硬引用场景对象)
|
||||
public CutsceneBinding[] Bindings;
|
||||
|
||||
[Header("Camera Blend")]
|
||||
[Tooltip("进入过场时的摄像机混合配置(可空 = 默认瞬切)")]
|
||||
public CameraBlendProfileSO BlendIn;
|
||||
[Tooltip("退出过场时的摄像机混合配置(可空 = 默认瞬切)")]
|
||||
public CameraBlendProfileSO BlendOut;
|
||||
|
||||
[Header("Optional Dialogue Overlay")]
|
||||
[Tooltip("过场中可叠加播放的对话序列(由 Timeline Marker 或 SignalEmitterClip 触发)")]
|
||||
public DialogueSequenceSO[] DialogueLayers;
|
||||
}
|
||||
|
||||
/// <summary>将一条 Timeline Track(通过名称索引)绑定到运行时场景对象。</summary>
|
||||
[Serializable]
|
||||
public struct CutsceneBinding
|
||||
{
|
||||
[Tooltip("Timeline Track 的名称(需与 PlayableDirector 内轨道名一致)")]
|
||||
public string trackName;
|
||||
|
||||
[Tooltip("绑定的目标对象;若为 null,CutsceneManager 从场景中按 tag/name 查找")]
|
||||
public UnityEngine.Object target;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Cutscene/CutsceneSO.cs.meta
Normal file
11
Assets/Scripts/Cutscene/CutsceneSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fc1fcf01f619dac408023483275c18da
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
81
Assets/Scripts/Cutscene/CutsceneTrigger.cs
Normal file
81
Assets/Scripts/Cutscene/CutsceneTrigger.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.World;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Cutscene
|
||||
{
|
||||
/// <summary>
|
||||
/// 过场动画触发器(架构 14_NarrativeModule §11.5)。
|
||||
/// 支持四种触发模式:进入区域、玩家交互、场景加载、事件频道。
|
||||
/// 实现 IInteractable 以支持 OnInteract 模式。
|
||||
/// </summary>
|
||||
public class CutsceneTrigger : MonoBehaviour, IInteractable
|
||||
{
|
||||
public enum TriggerMode
|
||||
{
|
||||
OnEnter, // 进入 Trigger 碰撞区域
|
||||
OnInteract, // 玩家主动交互(IInteractable)
|
||||
OnSceneLoad, // 场景加载完毕(Start)
|
||||
OnEvent, // 订阅事件频道触发
|
||||
}
|
||||
|
||||
[SerializeField] private CutsceneSO _cutscene;
|
||||
[SerializeField] private TriggerMode _mode = TriggerMode.OnEnter;
|
||||
[SerializeField] private CutsceneManager _cutsceneManager;
|
||||
[SerializeField] private VoidEventChannelSO _triggerEventChannel; // OnEvent 模式使用
|
||||
[SerializeField] private WorldStateRegistry _worldState; // SO 注入,记录/查询播放状态
|
||||
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
// ── IInteractable(OnInteract 模式)──────────────────────────────
|
||||
public bool CanInteract => _mode == TriggerMode.OnInteract;
|
||||
public string InteractPrompt => "查看";
|
||||
|
||||
public void Interact(Transform player) => TriggerCutscene();
|
||||
public void OnPlayerEnterRange(Transform player) { }
|
||||
public void OnPlayerExitRange() { }
|
||||
|
||||
// ── 生命周期 ──────────────────────────────────────────────────────
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_mode == TriggerMode.OnEvent)
|
||||
_triggerEventChannel?.Subscribe(TriggerCutscene).AddTo(_subs);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
_subs.Clear();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (_mode == TriggerMode.OnSceneLoad) TriggerCutscene();
|
||||
}
|
||||
|
||||
private void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
if (_mode != TriggerMode.OnEnter) return;
|
||||
if (!other.CompareTag("Player")) return;
|
||||
TriggerCutscene();
|
||||
}
|
||||
|
||||
// ── 触发逻辑 ──────────────────────────────────────────────────────
|
||||
|
||||
private void TriggerCutscene()
|
||||
{
|
||||
if (_cutscene == null || _cutsceneManager == null) return;
|
||||
|
||||
// 已播放且仅播一次 → 跳过
|
||||
if (_cutscene.playOnlyOnce && _worldState != null
|
||||
&& _worldState.HasFlag($"cutscene_played_{_cutscene.cutsceneId}"))
|
||||
return;
|
||||
|
||||
_cutsceneManager.PlayCutscene(_cutscene);
|
||||
_worldState?.SetFlag($"cutscene_played_{_cutscene.cutsceneId}");
|
||||
|
||||
// 区域触发后禁用自身,防止重入
|
||||
if (_mode == TriggerMode.OnEnter) enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Cutscene/CutsceneTrigger.cs.meta
Normal file
11
Assets/Scripts/Cutscene/CutsceneTrigger.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7d7aa12751a7a604db8018b083d8db3b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
63
Assets/Scripts/Cutscene/SignalEmitterClip.cs
Normal file
63
Assets/Scripts/Cutscene/SignalEmitterClip.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using BaseGames.Core.Events;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Playables;
|
||||
using UnityEngine.Timeline;
|
||||
|
||||
namespace BaseGames.Cutscene
|
||||
{
|
||||
// =====================================================================
|
||||
// SignalEmitterClip —— Timeline 零耦合事件桥接(架构 14 §11.6)
|
||||
// =====================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 在 Timeline 轨道上放置此 Clip,Clip 播放时向目标 Void 事件频道 Raise 一次事件。
|
||||
/// 用途:Timeline 动画与游戏逻辑保持零耦合(不直接引用场景对象)。
|
||||
/// 使用场景示例:过场第 3 秒 → 触发 EVT_BossPhase2 → BossOrchestrator 切换阶段。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "BaseGames/Cutscene/SignalEmitterClip")]
|
||||
public class SignalEmitterClip : PlayableAsset, ITimelineClipAsset
|
||||
{
|
||||
[Tooltip("Clip 播放时发射的目标 Void 事件频道 SO")]
|
||||
[SerializeField] private VoidEventChannelSO _targetChannel;
|
||||
|
||||
/// <summary>Timeline 系统查询 Clip 能力(无额外能力)。</summary>
|
||||
public ClipCaps clipCaps => ClipCaps.None;
|
||||
|
||||
public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
|
||||
{
|
||||
var playable = ScriptPlayable<SignalEmitterBehaviour>.Create(graph);
|
||||
playable.GetBehaviour().Clip = this;
|
||||
return playable;
|
||||
}
|
||||
|
||||
/// <summary>供 SignalEmitterBehaviour 内部调用,发射事件。</summary>
|
||||
internal void Fire() => _targetChannel?.Raise();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// SignalEmitterClip 对应的 PlayableBehaviour,处理 ProcessFrame 时机。
|
||||
/// </summary>
|
||||
public class SignalEmitterBehaviour : PlayableBehaviour
|
||||
{
|
||||
/// <summary>由 CreatePlayable 注入,内部访问 Fire() 发射事件。</summary>
|
||||
public SignalEmitterClip Clip;
|
||||
|
||||
private bool _fired;
|
||||
|
||||
public override void OnBehaviourPlay(Playable playable, FrameData info)
|
||||
{
|
||||
_fired = false; // 支持 Timeline 循环/重播时重置
|
||||
}
|
||||
|
||||
public override void ProcessFrame(Playable playable, FrameData info, object playerData)
|
||||
{
|
||||
if (!_fired && Clip != null)
|
||||
{
|
||||
Clip.Fire();
|
||||
_fired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Cutscene/SignalEmitterClip.cs.meta
Normal file
11
Assets/Scripts/Cutscene/SignalEmitterClip.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 70594aff0301cac46bb95c619167d06b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user