220 lines
8.6 KiB
C#
220 lines
8.6 KiB
C#
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);
|
||
}
|
||
}
|
||
}
|