using System; using System.Threading; using System.Threading.Tasks; using UnityEngine; using UnityEngine.Playables; namespace EventBossFight { /// /// 视频 Boss:待机与段落切换必须由 Timeline 驱动,不提供 Spine idle 名兜底。 /// Leviathan 轨请绑定仅用于本 Timeline的解码器。视频片段 Inspector 中「Timeline 多线程驱动」建议 DecodeThreadCatchUp(默认),行为接近 Match 待机多线程;若异常可改回 ProcessFrameLockstep。 /// public class EventBossFightTimelineCellVideo : MonoBehaviour { private CancellationTokenSource _playIdleDeferredCts; [Header("Timeline 驱动(每分包 Cell 独立,与面板 enter 根 Director 解耦)")] [Tooltip("未赋值则在 Awake 时 GetComponent()")] [SerializeField] protected PlayableDirector cellPlayableDirector; protected GameObject idle; protected GameObject death; protected GameObject show; [Tooltip("必须配置:待机循环由 idle Timeline 播放")] public PlayableAsset idleTimeline; public PlayableAsset deathTimeline; /// 本分包 Boss 专用 PlayableDirector,播放入场 show / idle / 攻击等轨。 public PlayableDirector CellPlayableDirector => cellPlayableDirector ? cellPlayableDirector : GetComponent(); /// /// 先走 (与工程其它 UI 一致),找不到再在子层级中按名字深度查找(含未激活节点)。 /// protected static GameObject ResolveVideoLayer(Transform root, string layerName) { if (!root) return null; var go = root.gameObject; var found = go.FindChildGameObject(layerName); if (found) return found; foreach (var t in root.GetComponentsInChildren(true)) { if (t == root) continue; if (t.name == layerName) return t.gameObject; } return null; } protected virtual void Awake() { if (!cellPlayableDirector) cellPlayableDirector = GetComponent(); idle ??= ResolveVideoLayer(transform, "idle"); death ??= ResolveVideoLayer(transform, "die"); show ??= ResolveVideoLayer(transform, "show"); } public virtual void PlayIdle(PlayableDirector director) { if (!director) return; if (!idleTimeline) { Debug.LogError($"{name}: 视频 Boss 必须配置 idleTimeline", this); return; } var ct = StartPlayIdleDeferredCancellation(); _ = PlayIdleDeferredAsync(director, ct); } /// 新一段 PlayIdle 时取消上一段;销毁时 会 Cancel。 protected CancellationToken StartPlayIdleDeferredCancellation() { _playIdleDeferredCts?.Cancel(); _playIdleDeferredCts = new CancellationTokenSource(); return _playIdleDeferredCts.Token; } protected void OnDestroy() { _playIdleDeferredCts?.Cancel(); _playIdleDeferredCts = null; } protected static async Task WaitPlayIdleFrameAsync(CancellationToken cancellationToken) { await Awaiters.NextFrame; cancellationToken.ThrowIfCancellationRequested(); } /// /// 先播 idle Timeline,等待若干帧后再关 show,减轻共用材质未就绪时的白屏。 /// private async Task PlayIdleDeferredAsync(PlayableDirector director, CancellationToken cancellationToken) { try { OnReturnedToIdleVisuals(); director.Play(idleTimeline, DirectorWrapMode.Loop); await WaitPlayIdleFrameAsync(cancellationToken); await WaitPlayIdleFrameAsync(cancellationToken); cancellationToken.ThrowIfCancellationRequested(); if (show) show.SetActive(false); await WaitPlayIdleFrameAsync(cancellationToken); OnReturnedToIdleVisuals(); } catch (OperationCanceledException) { // 面板关闭或再次 PlayIdle 时取消,属正常情况 } catch (Exception e) { Debug.LogException(e); } } /// /// 进入待机 Director 或段落 Timeline 播完切回 idle 循环时调用:子类可隐藏攻击/受击等视频节点。 /// protected virtual void OnReturnedToIdleVisuals() { if (idle) idle.SetActive(true); } /// /// Director.Play 后过一帧再调:覆盖 Timeline Activation Track 等对显隐的改写。 /// protected virtual void OnAfterDirectorPlayedFirstFrame(PlayableDirector director, PlayableAsset timeline) { } /// 播放一段 Timeline 至结束(不切 idle);用于入场 show 等与 串联。 protected async Task PlayTimelineSegmentOnceAsync(PlayableDirector director, PlayableAsset timeline) { if (!director || !timeline) return; director.extrapolationMode = DirectorWrapMode.None; director.Play(timeline, DirectorWrapMode.Hold); await Awaiters.NextFrame; OnAfterDirectorPlayedFirstFrame(director, timeline); await WaitDirectorSegmentFinished(director, timeline); } protected async Task PlayTimeline(PlayableDirector director, PlayableAsset timeline) { if (!director || !timeline) return; director.Play(timeline, DirectorWrapMode.Hold); await Awaiters.NextFrame; OnAfterDirectorPlayedFirstFrame(director, timeline); // PlayableAsset.duration 常与 Timeline 里 Clip 实际长度不一致,不能只用 Seconds 等 await WaitDirectorSegmentFinished(director, timeline); if (timeline != deathTimeline) { if (!idleTimeline) { Debug.LogError($"{name}: 段落结束后回到待机需要 idleTimeline", this); return; } director.Play(idleTimeline, DirectorWrapMode.Loop); await Awaiters.NextFrame; OnReturnedToIdleVisuals(); await Awaiters.NextFrame; OnReturnedToIdleVisuals(); } } /// /// 等到当前 Director 上这一段 Hold 播完(用 time/duration/state,避免只靠 PlayableAsset.duration)。 /// private static async Task WaitDirectorSegmentFinished(PlayableDirector director, PlayableAsset timelineFallback) { await Awaiters.NextFrame; float safety = Mathf.Max((float)(timelineFallback != null ? timelineFallback.duration : 1.0), 0.1f) + 8f; float elapsed = 0f; while (elapsed < safety) { if (director == null) break; // Play() 后图可能延迟一两帧才 IsValid;若此处直接 break 会把 show 等段落当成「已播完」并立刻进 idle。 if (director.playableGraph.IsValid() && IsDirectorSegmentFinished(director)) break; await Awaiters.NextFrame; elapsed += Time.deltaTime; } } private static bool IsDirectorSegmentFinished(PlayableDirector dir) { if (dir == null) return true; if (dir.state != PlayState.Playing) return true; var dur = dir.duration; if (dur > 0.001 && dir.time >= dur - 0.03) return true; return false; } public virtual async Task PlayTimelineDeath(PlayableDirector director) { if (idle) idle.SetActive(false); if (death) death.SetActive(true); await PlayTimeline(director, deathTimeline); } } }