Files
ft/Client/Assets/Scripts/EventBossFight/Panel/MainVideo/EventBossFightTimelineCellVideo.cs
2026-06-29 21:18:33 +08:00

220 lines
8.6 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Playables;
namespace EventBossFight
{
/// <summary>
/// 视频 Boss待机与段落切换必须由 Timeline 驱动,不提供 Spine idle 名兜底。
/// Leviathan 轨请绑定<strong>仅用于本 Timeline</strong>的解码器。视频片段 Inspector 中「Timeline 多线程驱动」建议 <c>DecodeThreadCatchUp</c>(默认),行为接近 Match 待机多线程;若异常可改回 <c>ProcessFrameLockstep</c>。
/// </summary>
public class EventBossFightTimelineCellVideo : MonoBehaviour
{
private CancellationTokenSource _playIdleDeferredCts;
[Header("Timeline 驱动(每分包 Cell 独立,与面板 enter 根 Director 解耦)")]
[Tooltip("未赋值则在 Awake 时 GetComponent<PlayableDirector>()")]
[SerializeField]
protected PlayableDirector cellPlayableDirector;
protected GameObject idle;
protected GameObject death;
protected GameObject show;
[Tooltip("必须配置:待机循环由 idle Timeline 播放")]
public PlayableAsset idleTimeline;
public PlayableAsset deathTimeline;
/// <summary>本分包 Boss 专用 PlayableDirector播放入场 show / idle / 攻击等轨。</summary>
public PlayableDirector CellPlayableDirector =>
cellPlayableDirector ? cellPlayableDirector : GetComponent<PlayableDirector>();
/// <summary>
/// 先走 <see cref="GameObject.FindChildGameObject"/>(与工程其它 UI 一致),找不到再在子层级中按名字深度查找(含未激活节点)。
/// </summary>
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<Transform>(true))
{
if (t == root)
continue;
if (t.name == layerName)
return t.gameObject;
}
return null;
}
protected virtual void Awake()
{
if (!cellPlayableDirector)
cellPlayableDirector = GetComponent<PlayableDirector>();
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);
}
/// <summary>新一段 PlayIdle 时取消上一段;销毁时 <see cref="OnDestroy"/> 会 Cancel。</summary>
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();
}
/// <summary>
/// 先播 idle Timeline等待若干帧后再关 show减轻共用材质未就绪时的白屏。
/// </summary>
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);
}
}
/// <summary>
/// 进入待机 Director 或段落 Timeline 播完切回 idle 循环时调用:子类可隐藏攻击/受击等视频节点。
/// </summary>
protected virtual void OnReturnedToIdleVisuals()
{
if (idle)
idle.SetActive(true);
}
/// <summary>
/// Director.Play 后过一帧再调:覆盖 Timeline Activation Track 等对显隐的改写。
/// </summary>
protected virtual void OnAfterDirectorPlayedFirstFrame(PlayableDirector director, PlayableAsset timeline)
{
}
/// <summary>播放一段 Timeline 至结束(不切 idle用于入场 show 等与 <see cref="PlayIdle"/> 串联。</summary>
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();
}
}
/// <summary>
/// 等到当前 Director 上这一段 Hold 播完(用 time/duration/state避免只靠 PlayableAsset.duration
/// </summary>
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);
}
}
}