From 75c71b394d88a6449c32505c0235b8739084ce72 Mon Sep 17 00:00:00 2001 From: Ding Yuntian <1491671119@qq.com> Date: Fri, 10 Apr 2026 13:24:04 +0800 Subject: [PATCH] =?UTF-8?q?feat(scene-mgmt):=20=E6=B7=BB=E5=8A=A0AnimatorK?= =?UTF-8?q?it=E5=8A=A8=E7=94=BB=E7=AE=A1=E7=90=86=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Scripts/SceneManagement/AnimatorKit.meta | 8 + .../AnimatorKit/AnimatorCenter.cs | 161 +++++++ .../AnimatorKit/AnimatorCenter.cs.meta | 11 + .../AnimatorKit/AnimatorHandler.cs | 401 ++++++++++++++++++ .../AnimatorKit/AnimatorHandler.cs.meta | 11 + .../AnimatorKit/AnimatorYarnCommand.cs | 34 ++ .../AnimatorKit/AnimatorYarnCommand.cs.meta | 11 + .../AnimatorKit/AnimatorYarnCommand.md | 31 ++ 8 files changed, 668 insertions(+) create mode 100644 Assets/Scripts/SceneManagement/AnimatorKit.meta create mode 100644 Assets/Scripts/SceneManagement/AnimatorKit/AnimatorCenter.cs create mode 100644 Assets/Scripts/SceneManagement/AnimatorKit/AnimatorCenter.cs.meta create mode 100644 Assets/Scripts/SceneManagement/AnimatorKit/AnimatorHandler.cs create mode 100644 Assets/Scripts/SceneManagement/AnimatorKit/AnimatorHandler.cs.meta create mode 100644 Assets/Scripts/SceneManagement/AnimatorKit/AnimatorYarnCommand.cs create mode 100644 Assets/Scripts/SceneManagement/AnimatorKit/AnimatorYarnCommand.cs.meta create mode 100644 Assets/Scripts/SceneManagement/AnimatorKit/AnimatorYarnCommand.md diff --git a/Assets/Scripts/SceneManagement/AnimatorKit.meta b/Assets/Scripts/SceneManagement/AnimatorKit.meta new file mode 100644 index 000000000..25281c659 --- /dev/null +++ b/Assets/Scripts/SceneManagement/AnimatorKit.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7a05366bfd7167e4b9ec409cc1ac8a55 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SceneManagement/AnimatorKit/AnimatorCenter.cs b/Assets/Scripts/SceneManagement/AnimatorKit/AnimatorCenter.cs new file mode 100644 index 000000000..e4eea2705 --- /dev/null +++ b/Assets/Scripts/SceneManagement/AnimatorKit/AnimatorCenter.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using AibisDream.Kit; + +namespace AibisDream +{ + public class AnimatorCenter : SingletonBase + { + private readonly Dictionary _handlers = new(); + + private AnimatorCenter() { } + + public void RegisterHandler(AnimatorHandler handler) + { + _handlers.Add(handler.animatorName, handler); + } + + public void UnregisterHandler(AnimatorHandler handler) + { + _handlers.Remove(handler.animatorName); + } + + private bool TryGetHandler(string animatorName, out AnimatorHandler handler) + { + if (_handlers.TryGetValue(animatorName, out handler)) + return true; + + Debug.LogError($"AnimatorCenter: 未找到名为 \"{animatorName}\" 的 AnimatorHandler。"); + handler = null; + return false; + } + + public void PlayAnimation(string animatorName, string stateName, int layer = 0, Action onComplete = null) + { + if (!TryGetHandler(animatorName, out AnimatorHandler animatorHandler)) + return; + + animatorHandler.SetAnimatorObjectActive(true); + animatorHandler.Play(stateName, layer, onComplete); + } + + public IEnumerator PlayAnimationAsync(string animatorName, string stateName, int layer = 0) + { + if (!TryGetHandler(animatorName, out AnimatorHandler animatorHandler)) + yield break; + + animatorHandler.SetAnimatorObjectActive(true); + yield return animatorHandler.PlayAsync(stateName, layer); + } + + public void PlayAnimationFromAddressable(string animatorName, string addressableKey, string stateName, + int layer = 0, Action onComplete = null) + { + if (!TryGetHandler(animatorName, out AnimatorHandler animatorHandler)) + return; + + animatorHandler.SetAnimatorObjectActive(true); + animatorHandler.PlayFromAddressable(addressableKey, stateName, layer, onComplete); + } + + public IEnumerator PlayAnimationFromAddressableAsync(string animatorName, string addressableKey, + string stateName, int layer = 0) + { + if (!TryGetHandler(animatorName, out AnimatorHandler animatorHandler)) + yield break; + + animatorHandler.SetAnimatorObjectActive(true); + yield return animatorHandler.PlayFromAddressableAsync(addressableKey, stateName, layer); + } + + public void StopAnimation(string animatorName) + { + if (!TryGetHandler(animatorName, out AnimatorHandler animatorHandler)) + return; + + animatorHandler.Stop(); + } + + public void PauseAnimation(string animatorName) + { + if (!TryGetHandler(animatorName, out AnimatorHandler animatorHandler)) + return; + + animatorHandler.Pause(); + } + + public void ResumeAnimation(string animatorName) + { + if (!TryGetHandler(animatorName, out AnimatorHandler animatorHandler)) + return; + + animatorHandler.Resume(); + } + + public void ResetAnimation(string animatorName) + { + if (!TryGetHandler(animatorName, out AnimatorHandler animatorHandler)) + return; + + animatorHandler.Reset(); + } + + public void HideAnimation(string animatorName) + { + if (!TryGetHandler(animatorName, out AnimatorHandler animatorHandler)) + return; + + animatorHandler.SetAnimatorObjectActive(false); + } + + public float GetPlaybackTime(string animatorName, int layer = 0) + { + return TryGetHandler(animatorName, out AnimatorHandler animatorHandler) + ? animatorHandler.GetPlaybackTime(layer) + : 0f; + } + + public float GetDuration(string animatorName, int layer = 0) + { + return TryGetHandler(animatorName, out AnimatorHandler animatorHandler) + ? animatorHandler.GetDuration(layer) + : 0f; + } + + public void SetPlaybackSpeed(string animatorName, float speed) + { + if (!TryGetHandler(animatorName, out AnimatorHandler animatorHandler)) + return; + + animatorHandler.SetPlaybackSpeed(speed); + } + + public void SeekToTime(string animatorName, double timeSeconds, int layer = 0) + { + if (!TryGetHandler(animatorName, out AnimatorHandler animatorHandler)) + return; + + animatorHandler.SeekToTime(timeSeconds, layer); + } + + public void PauseAll() + { + foreach (AnimatorHandler registeredHandler in _handlers.Values) + registeredHandler.Pause(); + } + + public void StopAll() + { + foreach (AnimatorHandler registeredHandler in _handlers.Values) + registeredHandler.Stop(); + } + + public void ResetAll() + { + foreach (AnimatorHandler registeredHandler in _handlers.Values) + registeredHandler.Reset(); + } + } +} diff --git a/Assets/Scripts/SceneManagement/AnimatorKit/AnimatorCenter.cs.meta b/Assets/Scripts/SceneManagement/AnimatorKit/AnimatorCenter.cs.meta new file mode 100644 index 000000000..6e79056dc --- /dev/null +++ b/Assets/Scripts/SceneManagement/AnimatorKit/AnimatorCenter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2b1fe1047b45a654c926318a50a38357 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SceneManagement/AnimatorKit/AnimatorHandler.cs b/Assets/Scripts/SceneManagement/AnimatorKit/AnimatorHandler.cs new file mode 100644 index 000000000..31afe7372 --- /dev/null +++ b/Assets/Scripts/SceneManagement/AnimatorKit/AnimatorHandler.cs @@ -0,0 +1,401 @@ +using System; +using System.Collections; +using AibisDream.Framework; +using UnityEngine; +using UnityEngine.ResourceManagement.AsyncOperations; + +namespace AibisDream +{ + public class AnimatorHandler : MonoBehaviour + { + private const string AnimationPrefix = "Animation/"; + + public string animatorName; + + [SerializeField] private Animator animator; + + [Tooltip("若当前状态为 Loop:勾选则播完一整轮后触发完成回调;不勾选则循环状态不会结束(异步会一直等待)")] + [SerializeField] + private bool completeAfterOneCycleWhenStateLoops = true; + + private float _speedBeforePause = 1f; + + private AsyncOperationHandle _addressableHandle; + private RuntimeAnimatorController _savedRuntimeAnimatorController; + + private Coroutine _playCompletionCoroutine; + + private Action _onCompleteCallback; + +#if UNITY_EDITOR + private void OnValidate() + { + ValidateSerializedAnimator(); + } +#endif + + private void Start() + { + AnimatorCenter.Instance.RegisterHandler(this); + } + + private void OnDestroy() + { + AnimatorCenter.Instance.UnregisterHandler(this); + + CancelCompletionCoroutine(); + + if (animator != null) + animator.speed = _speedBeforePause > 0f ? _speedBeforePause : 1f; + + ReleaseAddressableHandle(); + } + + private void ValidateSerializedAnimator() + { + if (animator != null && animator.transform == transform) + animator = null; + } + + private bool EnsureAnimator() + { + if (animator != null) + return true; + + Debug.LogError($"AnimatorHandler {animatorName}: 未在子物体上找到 Animator。", this); + return false; + } + + private void CancelCompletionCoroutine() + { + if (_playCompletionCoroutine == null) + return; + StopCoroutine(_playCompletionCoroutine); + _playCompletionCoroutine = null; + } + + private void ReleaseAddressableHandle() + { + if (!_addressableHandle.IsValid()) + return; + + if (animator != null && animator.runtimeAnimatorController == _addressableHandle.Result) + animator.runtimeAnimatorController = _savedRuntimeAnimatorController; + + ResourceKit.Release(_addressableHandle); + _addressableHandle = default; + _savedRuntimeAnimatorController = null; + } + + private void EnsurePlayingSpeedForNewClip() + { + if (animator != null && animator.speed < 0.001f) + animator.speed = _speedBeforePause > 0f ? _speedBeforePause : 1f; + } + + /// + /// 对挂载 调用 , + /// 不是修改 。Handler 根物体可保持激活以便协程与注册。 + /// + public void SetAnimatorObjectActive(bool active) + { + if (animator == null) + return; + + animator.gameObject.SetActive(active); + } + + private void ActivateAnimatorGameObject() + { + SetAnimatorObjectActive(true); + } + + /// + /// 使用当前 Animator 上已绑定的控制器播放指定状态。 + /// + public void Play(string stateName, int layer = 0, Action onComplete = null) + { + if (!EnsureAnimator()) + return; + + ActivateAnimatorGameObject(); + + CancelCompletionCoroutine(); + ReleaseAddressableHandle(); + + _onCompleteCallback = onComplete; + + EnsurePlayingSpeedForNewClip(); + + animator.Play(stateName, layer, 0f); + animator.Update(0f); + + if (onComplete != null) + _playCompletionCoroutine = StartCoroutine(WaitForStatePlaybackEnd(layer, stateName, true)); + } + + public IEnumerator PlayAsync(string stateName, int layer = 0) + { + if (!EnsureAnimator()) + yield break; + + ActivateAnimatorGameObject(); + + CancelCompletionCoroutine(); + ReleaseAddressableHandle(); + + EnsurePlayingSpeedForNewClip(); + + animator.Play(stateName, layer, 0f); + animator.Update(0f); + + yield return WaitForStatePlaybackEnd(layer, stateName, false); + } + + private void OnPlaybackComplete() + { + _playCompletionCoroutine = null; + + var completionCallback = _onCompleteCallback; + _onCompleteCallback = null; + + ReleaseAddressableHandle(); + completionCallback?.Invoke(); + } + + /// 为 true 时在结束时调用 (含 Addressable 释放与 Action);为 false 时仅做 Addressable 释放(用于无回调异步) + private IEnumerator WaitForStatePlaybackEnd(int layer, string stateName, bool invokeCompletionCallback) + { + var timeout = 10f; + while (timeout > 0f) + { + var currentStateInfo = animator.GetCurrentAnimatorStateInfo(layer); + if (currentStateInfo.IsName(stateName) && !animator.IsInTransition(layer)) + break; + timeout -= Time.deltaTime; + yield return null; + } + + while (true) + { + var currentStateInfo = animator.GetCurrentAnimatorStateInfo(layer); + + if (!currentStateInfo.IsName(stateName)) + { + if (invokeCompletionCallback) + OnPlaybackComplete(); + else + ReleaseAddressableHandle(); + yield break; + } + + if (animator.IsInTransition(layer)) + { + yield return null; + continue; + } + + if (currentStateInfo.loop) + { + if (completeAfterOneCycleWhenStateLoops && currentStateInfo.normalizedTime >= 1f) + { + if (invokeCompletionCallback) + OnPlaybackComplete(); + else + ReleaseAddressableHandle(); + yield break; + } + } + else if (currentStateInfo.normalizedTime >= 1f) + { + if (invokeCompletionCallback) + OnPlaybackComplete(); + else + ReleaseAddressableHandle(); + yield break; + } + + yield return null; + } + } + + /// + /// 从 Addressables 加载 RuntimeAnimatorController(键为 Animation/ + addressableKey)并播放状态。 + /// + public void PlayFromAddressable(string addressableKey, string stateName, int layer = 0, Action onComplete = null) + { + StartCoroutine(PlayFromAddressableCoroutine(addressableKey, stateName, layer, onComplete)); + } + + public IEnumerator PlayFromAddressableAsync(string addressableKey, string stateName, int layer = 0) + { + yield return PlayFromAddressableCoroutine(addressableKey, stateName, layer, null); + } + + private IEnumerator PlayFromAddressableCoroutine(string addressableKey, string stateName, int layer, + Action onComplete) + { + if (!EnsureAnimator()) + yield break; + + ActivateAnimatorGameObject(); + + if (string.IsNullOrEmpty(addressableKey)) + { + Debug.LogError($"AnimatorHandler {animatorName}: addressableKey 为空。", this); + yield break; + } + + ReleaseAddressableHandle(); + + var loadKey = AnimationPrefix + addressableKey; + RuntimeAnimatorController loaded = null; + AsyncOperationHandle loadedHandle = default; + var loadDone = false; + + ResourceKit.LoadAssetAsyncWithHandle(loadKey, loadHandle => + { + loadedHandle = loadHandle; + if (loadHandle.Status == AsyncOperationStatus.Succeeded) + loaded = loadHandle.Result; + loadDone = true; + }); + + yield return new WaitUntil(() => loadDone); + + if (loaded == null) + { + Debug.LogError($"AnimatorHandler {animatorName}: 加载失败 '{loadKey}'", this); + yield break; + } + + CancelCompletionCoroutine(); + + _savedRuntimeAnimatorController = animator.runtimeAnimatorController; + animator.runtimeAnimatorController = loaded; + animator.Rebind(); + animator.Update(0f); + + _addressableHandle = loadedHandle; + + EnsurePlayingSpeedForNewClip(); + + _onCompleteCallback = onComplete; + + animator.Play(stateName, layer, 0f); + animator.Update(0f); + + if (onComplete != null) + { + _playCompletionCoroutine = StartCoroutine(WaitForStatePlaybackEnd(layer, stateName, true)); + while (_playCompletionCoroutine != null) + yield return null; + } + else + { + yield return WaitForStatePlaybackEnd(layer, stateName, false); + } + } + + public void Stop() + { + if (!EnsureAnimator()) + return; + + CancelCompletionCoroutine(); + _onCompleteCallback = null; + + animator.speed = 0f; + + ReleaseAddressableHandle(); + } + + public void Pause() + { + if (!EnsureAnimator()) + return; + + if (animator.speed > 0f) + { + _speedBeforePause = animator.speed; + animator.speed = 0f; + } + } + + public void Resume() + { + if (!EnsureAnimator()) + return; + + if (animator.speed == 0f) + animator.speed = _speedBeforePause > 0f ? _speedBeforePause : 1f; + } + + public void Reset() + { + if (!EnsureAnimator()) + return; + + CancelCompletionCoroutine(); + _onCompleteCallback = null; + + animator.speed = 1f; + + animator.Rebind(); + animator.Update(0f); + + ReleaseAddressableHandle(); + } + + /// + /// 当前层当前状态下,已播放时长(秒)。过渡或非目标状态时语义以当前 StateInfo 为准。 + /// + public float GetPlaybackTime(int layer = 0) + { + if (animator == null) + return 0f; + + var currentStateInfo = animator.GetCurrentAnimatorStateInfo(layer); + return currentStateInfo.normalizedTime * currentStateInfo.length; + } + + /// + /// 当前层当前状态片段长度(秒)。 + /// + public float GetDuration(int layer = 0) + { + if (animator == null) + return 0f; + + return animator.GetCurrentAnimatorStateInfo(layer).length; + } + + public void SetPlaybackSpeed(float speed) + { + if (!EnsureAnimator()) + return; + + animator.speed = speed; + if (speed > 0f) + _speedBeforePause = speed; + } + + /// + /// 在当前层下,将播放位置 seek 到指定时间(秒),相对当前状态片段长度归一化后播放。 + /// + public void SeekToTime(double timeSeconds, int layer = 0) + { + if (!EnsureAnimator()) + return; + + var currentStateInfo = animator.GetCurrentAnimatorStateInfo(layer); + var stateLengthSeconds = currentStateInfo.length; + if (stateLengthSeconds <= 0.0001f) + return; + + var normalizedPlaybackTime = Mathf.Clamp01((float)(timeSeconds / stateLengthSeconds)); + animator.Play(currentStateInfo.shortNameHash, layer, normalizedPlaybackTime); + animator.Update(0f); + } + } +} diff --git a/Assets/Scripts/SceneManagement/AnimatorKit/AnimatorHandler.cs.meta b/Assets/Scripts/SceneManagement/AnimatorKit/AnimatorHandler.cs.meta new file mode 100644 index 000000000..9ee9419c8 --- /dev/null +++ b/Assets/Scripts/SceneManagement/AnimatorKit/AnimatorHandler.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: babec43795065b244ac4cef4af7fa225 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SceneManagement/AnimatorKit/AnimatorYarnCommand.cs b/Assets/Scripts/SceneManagement/AnimatorKit/AnimatorYarnCommand.cs new file mode 100644 index 000000000..0a5c03c39 --- /dev/null +++ b/Assets/Scripts/SceneManagement/AnimatorKit/AnimatorYarnCommand.cs @@ -0,0 +1,34 @@ +using System.Collections; +using Yarn.Unity; + +namespace AibisDream +{ + // Yarn 命令说明见同目录 AnimatorYarnCommand.md + public static class AnimatorYarnCommand + { + [YarnCommand("play_animation")] + public static IEnumerator PlayAnimation(string animatorName, string stateName) + { + yield return AnimatorCenter.Instance.PlayAnimationAsync(animatorName, stateName); + } + + [YarnCommand("play_animation_addressable")] + public static IEnumerator PlayAnimationAddressable(string animatorName, string addressableKey, string stateName) + { + yield return AnimatorCenter.Instance.PlayAnimationFromAddressableAsync(animatorName, addressableKey, + stateName); + } + + [YarnCommand("reset_animation")] + public static void ResetAnimation(string animatorName) + { + AnimatorCenter.Instance.ResetAnimation(animatorName); + } + + [YarnCommand("hide_animation")] + public static void HideAnimation(string animatorName) + { + AnimatorCenter.Instance.HideAnimation(animatorName); + } + } +} diff --git a/Assets/Scripts/SceneManagement/AnimatorKit/AnimatorYarnCommand.cs.meta b/Assets/Scripts/SceneManagement/AnimatorKit/AnimatorYarnCommand.cs.meta new file mode 100644 index 000000000..91da4c33d --- /dev/null +++ b/Assets/Scripts/SceneManagement/AnimatorKit/AnimatorYarnCommand.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4c6df6618b817054092bb3443c7bcc7e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SceneManagement/AnimatorKit/AnimatorYarnCommand.md b/Assets/Scripts/SceneManagement/AnimatorKit/AnimatorYarnCommand.md new file mode 100644 index 000000000..9c39e6a9c --- /dev/null +++ b/Assets/Scripts/SceneManagement/AnimatorKit/AnimatorYarnCommand.md @@ -0,0 +1,31 @@ +# AnimatorYarnCommand(Yarn 用法) + +实现类:`AnimatorYarnCommand.cs`。命令经 `AnimatorCenter` 调用场景中已注册的 `AnimatorHandler`。 + +## 参数说明 + +- **animatorName**:与场景里 `AnimatorHandler.animatorName` 一致。 +- **stateName**:Animator Controller 里的状态名。 +- **addressableKey**:Addressables 资源键中 **`Animation/` 之后** 的部分(与 `AnimatorHandler` 加载逻辑一致)。 + +在 `.yarn` 里参数顺序与 C# 方法一致;含空格或中文时按项目 Yarn 版本加双引号。 + +## 命令一览 + +| 命令 | 是否等待结束 | +|------|----------------| +| `<>` | 会。等到 `PlayAnimationAsync` 完成(非 Loop 播完或 Loop 一整轮等,见 Handler 上配置)。 | +| `<>` | 会。加载控制器并播完本次流程。 | +| `<>` | 否。立即 `ResetAnimation`。 | +| `<>` | 否。关掉**挂 Animator 的那层 GameObject**,Handler 仍可留在场景里。 | + +## 示例 + +把名称换成你场景里的注册名与状态名: + +```yarn +<> +<> +<> +<> +```