多轮审查和修复

This commit is contained in:
2026-05-12 15:34:08 +08:00
parent f55d2a57c3
commit ebbbb7332e
805 changed files with 838724 additions and 1905 deletions

View File

@@ -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,

View 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();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7b85bd04fd48a5e41b7e675944f6c355
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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("绑定的目标对象;若为 nullCutsceneManager 从场景中按 tag/name 查找")]
public UnityEngine.Object target;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fc1fcf01f619dac408023483275c18da
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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();
// ── IInteractableOnInteract 模式)──────────────────────────────
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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7d7aa12751a7a604db8018b083d8db3b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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 轨道上放置此 ClipClip 播放时向目标 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;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 70594aff0301cac46bb95c619167d06b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: