488 lines
15 KiB
C#
488 lines
15 KiB
C#
using System;
|
|
using System.Collections;
|
|
using Cinemachine;
|
|
using UnityEngine;
|
|
using UnityEngine.Playables;
|
|
using UnityEngine.ResourceManagement.AsyncOperations;
|
|
using UnityEngine.Timeline;
|
|
using AibisDream.Framework;
|
|
|
|
namespace AibisDream
|
|
{
|
|
public class DirectorHandler : MonoBehaviour
|
|
{
|
|
private const string TimelinePrefix = "Timeline/";
|
|
|
|
public string timelineName;
|
|
[SerializeField] private PlayableDirector director;
|
|
|
|
[Header("Cinemachine 自动绑定")]
|
|
[Tooltip("加载新 Timeline 时自动绑定 CinemachineBrain")]
|
|
[SerializeField] private bool autoBindCinemachineBrain;
|
|
|
|
[Header("播放结束")]
|
|
[Tooltip("勾选:自然播放到结尾(PlayableDirector.stopped)后将时间设回 0;不勾选:停留在最后一帧(默认)")]
|
|
[SerializeField] private bool resetToStartWhenFinished;
|
|
[Tooltip("当 resetToStartWhenFinished 为真时,回到时间 0 前的等待秒数;0 表示立刻回到起点")]
|
|
[Min(0f)]
|
|
[SerializeField] private float resetToStartDelay;
|
|
|
|
private Action _onCompleteCallback;
|
|
private AsyncOperationHandle<PlayableAsset> _addressableHandle;
|
|
private PlayableAsset _savedPlayableAsset;
|
|
private Coroutine _pendingResetToStartCoroutine;
|
|
|
|
#if UNITY_EDITOR
|
|
/// <summary>
|
|
/// 在 Editor 中,当组件创建或 Inspector 变化时自动查找同物体上的 PlayableDirector
|
|
/// </summary>
|
|
private void OnValidate()
|
|
{
|
|
if (director == null)
|
|
{
|
|
director = GetComponent<PlayableDirector>();
|
|
}
|
|
}
|
|
#endif
|
|
|
|
private void Awake()
|
|
{
|
|
if (director == null)
|
|
{
|
|
director = GetComponent<PlayableDirector>();
|
|
}
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
TimelineCenter.Instance.RegisterDirector(this);
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
TimelineCenter.Instance.UnregisterDirector(this);
|
|
|
|
CancelPendingResetToStartCoroutine();
|
|
|
|
if (director != null)
|
|
{
|
|
director.stopped -= OnDirectorStopped;
|
|
}
|
|
|
|
ReleaseAddressableHandle();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 是否需要订阅 PlayableDirector.stopped(完成回调或需要播放结束行为时)。
|
|
/// </summary>
|
|
private bool ShouldSubscribeStopped(Action onComplete)
|
|
{
|
|
return onComplete != null || resetToStartWhenFinished;
|
|
}
|
|
|
|
private void CancelPendingResetToStartCoroutine()
|
|
{
|
|
if (_pendingResetToStartCoroutine == null) return;
|
|
StopCoroutine(_pendingResetToStartCoroutine);
|
|
_pendingResetToStartCoroutine = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 将 Director 时间设到 0 并刷新一帧(不 Stop、不释放资源)。
|
|
/// </summary>
|
|
private void ApplyPlaybackTimeZero()
|
|
{
|
|
if (director == null) return;
|
|
director.time = 0;
|
|
director.Evaluate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 在 Release + 完成回调之后执行:按配置回到起点或延时回到起点。
|
|
/// </summary>
|
|
private void ApplyEndBehaviorAfterStopped()
|
|
{
|
|
if (!resetToStartWhenFinished) return;
|
|
|
|
CancelPendingResetToStartCoroutine();
|
|
|
|
var delay = Mathf.Max(0f, resetToStartDelay);
|
|
if (delay <= 0f)
|
|
{
|
|
ApplyPlaybackTimeZero();
|
|
}
|
|
else
|
|
{
|
|
_pendingResetToStartCoroutine = StartCoroutine(ResetToStartAfterDelayCoroutine(delay));
|
|
}
|
|
}
|
|
|
|
private IEnumerator ResetToStartAfterDelayCoroutine(float delaySeconds)
|
|
{
|
|
yield return new WaitForSeconds(delaySeconds);
|
|
_pendingResetToStartCoroutine = null;
|
|
ApplyPlaybackTimeZero();
|
|
}
|
|
|
|
private void ReleaseAddressableHandle()
|
|
{
|
|
if (!_addressableHandle.IsValid()) return;
|
|
|
|
if (director != null && director.playableAsset == _addressableHandle.Result)
|
|
{
|
|
director.playableAsset = _savedPlayableAsset;
|
|
}
|
|
|
|
ResourceKit.Release(_addressableHandle);
|
|
_addressableHandle = default;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 自动绑定 CinemachineBrain 到 Timeline 中的所有 CinemachineTrack
|
|
/// </summary>
|
|
private void AutoBindCinemachineBrain()
|
|
{
|
|
Debug.Log($"[DirectorHandler] {timelineName}: AutoBindCinemachineBrain 被调用,autoBindCinemachineBrain={autoBindCinemachineBrain}");
|
|
|
|
if (!autoBindCinemachineBrain)
|
|
{
|
|
Debug.Log($"[DirectorHandler] {timelineName}: autoBindCinemachineBrain 为 false,跳过绑定");
|
|
return;
|
|
}
|
|
|
|
if (director == null)
|
|
{
|
|
Debug.LogError($"[DirectorHandler] {timelineName}: director 为 null");
|
|
return;
|
|
}
|
|
|
|
if (director.playableAsset == null)
|
|
{
|
|
Debug.LogError($"[DirectorHandler] {timelineName}: director.playableAsset 为 null");
|
|
return;
|
|
}
|
|
|
|
// 获取目标 CinemachineBrain
|
|
var mainCamera = Camera.main;
|
|
if (mainCamera == null)
|
|
{
|
|
Debug.LogError($"[DirectorHandler] {timelineName}: Camera.main 为 null");
|
|
return;
|
|
}
|
|
|
|
var brain = mainCamera.GetComponent<CinemachineBrain>();
|
|
if (brain == null)
|
|
{
|
|
Debug.LogWarning($"[DirectorHandler] {timelineName}: 主摄像机上找不到 CinemachineBrain,无法自动绑定");
|
|
return;
|
|
}
|
|
|
|
Debug.Log($"[DirectorHandler] {timelineName}: 找到 CinemachineBrain,准备绑定");
|
|
|
|
// 遍历所有输出轨道,找到 CinemachineTrack 并绑定
|
|
var timelineAsset = director.playableAsset as TimelineAsset;
|
|
if (timelineAsset == null)
|
|
{
|
|
Debug.LogError($"[DirectorHandler] {timelineName}: playableAsset 不是 TimelineAsset");
|
|
return;
|
|
}
|
|
|
|
int trackCount = 0;
|
|
int boundCount = 0;
|
|
foreach (var track in timelineAsset.GetOutputTracks())
|
|
{
|
|
trackCount++;
|
|
Debug.Log($"[DirectorHandler] {timelineName}: 检查轨道 {track.name},类型: {track.GetType().Name}");
|
|
|
|
// 直接检查轨道类型
|
|
if (track is CinemachineTrack)
|
|
{
|
|
director.SetGenericBinding(track, brain);
|
|
boundCount++;
|
|
Debug.Log($"[DirectorHandler] {timelineName}: 成功绑定轨道 {track.name}");
|
|
}
|
|
}
|
|
|
|
Debug.Log($"[DirectorHandler] {timelineName}: 共检查 {trackCount} 个轨道,绑定了 {boundCount} 个 CinemachineTrack");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 播放 Timeline
|
|
/// </summary>
|
|
public void Play(Action onComplete = null)
|
|
{
|
|
if (director == null)
|
|
{
|
|
Debug.LogError($"DirectorHandler {timelineName}: PlayableDirector is null");
|
|
return;
|
|
}
|
|
|
|
// 先停止再播放
|
|
CancelPendingResetToStartCoroutine();
|
|
director.stopped -= OnDirectorStopped;
|
|
if (director.state == PlayState.Playing)
|
|
{
|
|
director.Stop();
|
|
}
|
|
|
|
_onCompleteCallback = onComplete;
|
|
|
|
if (ShouldSubscribeStopped(onComplete))
|
|
{
|
|
director.stopped += OnDirectorStopped;
|
|
}
|
|
|
|
// 自动绑定 CinemachineBrain
|
|
AutoBindCinemachineBrain();
|
|
|
|
director.Play();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 异步播放 Timeline(协程版本)
|
|
/// </summary>
|
|
public IEnumerator PlayAsync()
|
|
{
|
|
if (director == null)
|
|
{
|
|
Debug.LogError($"DirectorHandler {timelineName}: PlayableDirector is null");
|
|
yield break;
|
|
}
|
|
|
|
CancelPendingResetToStartCoroutine();
|
|
director.stopped -= OnDirectorStopped;
|
|
if (director.state == PlayState.Playing)
|
|
{
|
|
director.Stop();
|
|
}
|
|
|
|
if (ShouldSubscribeStopped(null))
|
|
{
|
|
director.stopped += OnDirectorStopped;
|
|
}
|
|
|
|
// 自动绑定 CinemachineBrain
|
|
AutoBindCinemachineBrain();
|
|
|
|
director.Play();
|
|
|
|
// 等待播放完成
|
|
while (director.state == PlayState.Playing)
|
|
{
|
|
yield return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 从 Addressables 加载 PlayableAsset 并播放(加载时自动加 "Timeline/" 前缀)
|
|
/// </summary>
|
|
public void PlayFromAddressable(string addressableKey, Action onComplete = null)
|
|
{
|
|
StartCoroutine(PlayFromAddressableCoroutine(addressableKey, onComplete));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 从 Addressables 加载 PlayableAsset 并播放(协程版本,可等待播完)
|
|
/// </summary>
|
|
public IEnumerator PlayFromAddressableAsync(string addressableKey)
|
|
{
|
|
yield return PlayFromAddressableCoroutine(addressableKey, null);
|
|
}
|
|
|
|
private IEnumerator PlayFromAddressableCoroutine(string addressableKey, Action onComplete)
|
|
{
|
|
if (director == null)
|
|
{
|
|
Debug.LogError($"DirectorHandler {timelineName}: PlayableDirector is null");
|
|
yield break;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(addressableKey))
|
|
{
|
|
Debug.LogError($"DirectorHandler {timelineName}: addressableKey is null or empty");
|
|
yield break;
|
|
}
|
|
|
|
// 播放新 Addressable 前,先释放上一次
|
|
ReleaseAddressableHandle();
|
|
|
|
var loadKey = TimelinePrefix + addressableKey;
|
|
PlayableAsset loadedAsset = null;
|
|
AsyncOperationHandle<PlayableAsset> loadedHandle = default;
|
|
var loadDone = false;
|
|
|
|
ResourceKit.LoadAssetAsyncWithHandle<PlayableAsset>(loadKey, handle =>
|
|
{
|
|
loadedHandle = handle;
|
|
if (handle.Status == AsyncOperationStatus.Succeeded)
|
|
loadedAsset = handle.Result;
|
|
loadDone = true;
|
|
});
|
|
|
|
yield return new WaitUntil(() => loadDone);
|
|
|
|
if (loadedAsset == null)
|
|
{
|
|
Debug.LogError($"DirectorHandler {timelineName}: Failed to load PlayableAsset '{loadKey}'");
|
|
yield break;
|
|
}
|
|
|
|
CancelPendingResetToStartCoroutine();
|
|
director.stopped -= OnDirectorStopped;
|
|
if (director.state == PlayState.Playing)
|
|
{
|
|
director.Stop();
|
|
}
|
|
|
|
_savedPlayableAsset = director.playableAsset;
|
|
director.playableAsset = loadedAsset;
|
|
|
|
director.RebuildGraph();
|
|
|
|
// 自动绑定 CinemachineBrain(必须在 RebuildGraph 之后)
|
|
AutoBindCinemachineBrain();
|
|
|
|
_addressableHandle = loadedHandle;
|
|
|
|
_onCompleteCallback = onComplete;
|
|
if (ShouldSubscribeStopped(onComplete))
|
|
{
|
|
director.stopped += OnDirectorStopped;
|
|
}
|
|
|
|
director.Play();
|
|
|
|
// 等待播放完成(与 PlayAsync 行为一致)
|
|
while (director.state == PlayState.Playing)
|
|
{
|
|
yield return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 停止播放
|
|
/// </summary>
|
|
public void Stop()
|
|
{
|
|
if (director == null) return;
|
|
|
|
CancelPendingResetToStartCoroutine();
|
|
|
|
// 先取消事件订阅,避免 Stop() 触发回调
|
|
director.stopped -= OnDirectorStopped;
|
|
|
|
director.Stop();
|
|
_onCompleteCallback = null;
|
|
ReleaseAddressableHandle();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 暂停播放
|
|
/// </summary>
|
|
public void Pause()
|
|
{
|
|
if (director == null) return;
|
|
|
|
if (director.state == PlayState.Playing)
|
|
{
|
|
director.Pause();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 恢复播放
|
|
/// </summary>
|
|
public void Resume()
|
|
{
|
|
if (director == null) return;
|
|
|
|
if (director.state == PlayState.Paused)
|
|
{
|
|
director.Resume();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 重置 Timeline
|
|
/// </summary>
|
|
public void Reset()
|
|
{
|
|
if (director == null) return;
|
|
|
|
CancelPendingResetToStartCoroutine();
|
|
|
|
// 先取消事件订阅,避免 Stop() 触发回调
|
|
director.stopped -= OnDirectorStopped;
|
|
|
|
director.Stop();
|
|
ApplyPlaybackTimeZero();
|
|
|
|
_onCompleteCallback = null;
|
|
ReleaseAddressableHandle();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取当前播放时间
|
|
/// </summary>
|
|
public double GetPlaybackTime()
|
|
{
|
|
if (director == null) return 0;
|
|
return director.time;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取总时长
|
|
/// </summary>
|
|
public double GetDuration()
|
|
{
|
|
if (director == null) return 0;
|
|
return director.duration;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 设置播放速度
|
|
/// </summary>
|
|
public void SetPlaybackSpeed(float speed)
|
|
{
|
|
if (director == null) return;
|
|
|
|
if (director.playableGraph.IsValid())
|
|
{
|
|
director.playableGraph.GetRootPlayable(0).SetSpeed(speed);
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning($"DirectorHandler {timelineName}: PlayableGraph is not valid. Cannot set playback speed.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 跳转到指定时间点
|
|
/// </summary>
|
|
public void SeekToTime(double time)
|
|
{
|
|
if (director == null) return;
|
|
|
|
director.time = Math.Clamp(time, 0, director.duration);
|
|
director.Evaluate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Director 停止事件处理
|
|
/// </summary>
|
|
private void OnDirectorStopped(PlayableDirector stoppedDirector)
|
|
{
|
|
// 立即取消订阅,避免重复触发
|
|
stoppedDirector.stopped -= OnDirectorStopped;
|
|
|
|
var callback = _onCompleteCallback;
|
|
_onCompleteCallback = null;
|
|
|
|
ReleaseAddressableHandle();
|
|
callback?.Invoke();
|
|
|
|
ApplyEndBehaviorAfterStopped();
|
|
}
|
|
}
|
|
}
|