From 7646cb4db31ef8d8c5486ed061ea7d065f43b6e4 Mon Sep 17 00:00:00 2001 From: Ding Yuntian <1491671119@qq.com> Date: Fri, 17 Jul 2026 20:19:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(scene-mgmt):=20=E6=B7=BB=E5=8A=A0=20FrameA?= =?UTF-8?q?nimation=20=E8=A7=92=E8=89=B2=E7=B1=BB=E5=9E=8B=E4=B8=8E?= =?UTF-8?q?=E5=BC=82=E6=AD=A5=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SceneManagement/ActorKit/AbsActor.cs | 106 +++++++++- .../SceneManagement/ActorKit/ActorManager.cs | 81 +++++++- .../ActorKit/FrameAnimationActor.cs | 187 ++++++++++++++++++ .../ActorKit/FrameAnimationActor.cs.meta | 11 ++ 4 files changed, 378 insertions(+), 7 deletions(-) create mode 100644 Assets/Scripts/SceneManagement/ActorKit/FrameAnimationActor.cs create mode 100644 Assets/Scripts/SceneManagement/ActorKit/FrameAnimationActor.cs.meta diff --git a/Assets/Scripts/SceneManagement/ActorKit/AbsActor.cs b/Assets/Scripts/SceneManagement/ActorKit/AbsActor.cs index 7aa83a843..0b2bfe713 100644 --- a/Assets/Scripts/SceneManagement/ActorKit/AbsActor.cs +++ b/Assets/Scripts/SceneManagement/ActorKit/AbsActor.cs @@ -5,6 +5,7 @@ using AibisDream.SaveSystem; using AibisDream.Utility; using DG.Tweening; using UnityEngine; +using UnityEngine.ResourceManagement.AsyncOperations; using Object = UnityEngine.Object; namespace AibisDream @@ -24,6 +25,7 @@ namespace AibisDream public string ActorName => _actorName; public string SlotName => _slotName; public ActorType ActorType => _actorType; + public virtual bool IsReady => true; public void Init(string actorName, ActorSlot slot, ActorType actorType) { @@ -58,6 +60,11 @@ namespace AibisDream spriteRenderer.SetAlpha(0); } + public virtual IEnumerator InitializeAsync() + { + yield break; + } + public abstract ActorEntrySnapshotDto CaptureEntry(); public abstract IEnumerator ApplyEntryAsync(ActorEntrySnapshotDto entry); @@ -176,6 +183,102 @@ namespace AibisDream return actor; } + public IEnumerator CreateActorAsync( + string actorName, + ActorSlot slot, + ActorType actorType, + Action onCompleted) + { + if (actorType != ActorType.FrameAnimation) + { + onCompleted?.Invoke(CreateActor(actorName, slot, actorType)); + yield break; + } + + yield return CreateFrameAnimationActorAsync( + actorName, + slot, + actorType, + null, + onCompleted); + } + + public IEnumerator CreateFromEntryAsync( + ActorEntrySnapshotDto entry, + ActorSlot slot, + Action onCompleted) + { + if (!Enum.TryParse(entry.actorType, out var actorType)) + { + actorType = ActorType.Anima; + } + + if (actorType != ActorType.FrameAnimation) + { + onCompleted?.Invoke(CreateFromEntry(entry, slot)); + yield break; + } + + yield return CreateFrameAnimationActorAsync( + entry.actorName, + slot, + actorType, + entry, + onCompleted); + } + + private IEnumerator CreateFrameAnimationActorAsync( + string actorName, + ActorSlot slot, + ActorType actorType, + ActorEntrySnapshotDto entry, + Action onCompleted) + { + var prefabKey = ConstRef.FrameAnimationActorPrefabName; + var handle = ResourceSystem.LoadPersistentAsync(prefabKey); + yield return handle; + + if (handle.Status != AsyncOperationStatus.Succeeded || handle.Result == null) + { + Debug.LogError( + $"[ActorFactory] 角色 '{actorName}' 的帧动画 Prefab 加载失败,key='{prefabKey}'。"); + onCompleted?.Invoke(null); + yield break; + } + + var instance = Object.Instantiate(handle.Result, _actorRoot); + var actor = instance.GetComponent(); + if (actor == null) + { + Debug.LogError( + $"[ActorFactory] Prefab '{prefabKey}' 缺少 AbsActor 组件,角色='{actorName}'。"); + Object.Destroy(instance); + onCompleted?.Invoke(null); + yield break; + } + + if (entry == null) + { + actor.Init(actorName, slot, actorType); + } + else + { + actor.InitFromEntry(entry, slot); + } + + yield return actor.InitializeAsync(); + if (!actor.IsReady) + { + Debug.LogError( + $"[ActorFactory] 帧动画角色 '{actorName}' 初始化失败,Prefab='{prefabKey}'。"); + Object.Destroy(actor.gameObject); + onCompleted?.Invoke(null); + yield break; + } + + onCompleted?.Invoke(actor); + } + private AbsActor InstantiateByType(ActorType actorType) { return actorType switch @@ -192,6 +295,7 @@ namespace AibisDream { Sprite, Anima, - AnimaEx + AnimaEx, + FrameAnimation = 3 } } diff --git a/Assets/Scripts/SceneManagement/ActorKit/ActorManager.cs b/Assets/Scripts/SceneManagement/ActorKit/ActorManager.cs index 056eae8c7..11a7781f4 100644 --- a/Assets/Scripts/SceneManagement/ActorKit/ActorManager.cs +++ b/Assets/Scripts/SceneManagement/ActorKit/ActorManager.cs @@ -49,7 +49,17 @@ namespace AibisDream { foreach (var slot in _slotDict.Values.Where(slot => slot.hasDefaultActor)) { - InitActor(slot.defaultActorName, slot.slotName, slot.defaultActorType); + if (slot.defaultActorType == ActorType.FrameAnimation) + { + StartCoroutine(InitActorAsync( + slot.defaultActorName, + slot.slotName, + slot.defaultActorType)); + } + else + { + InitActor(slot.defaultActorName, slot.slotName, slot.defaultActorType); + } } } @@ -94,7 +104,11 @@ namespace AibisDream continue; } - var actor = _actorFactory.CreateFromEntry(entry, slot); + AbsActor actor = null; + yield return _actorFactory.CreateFromEntryAsync( + entry, + slot, + created => actor = created); if (actor == null) continue; UpsertActor(entry.actorName, actor); @@ -121,8 +135,26 @@ namespace AibisDream { if (!string.IsNullOrEmpty(slot.defaultActorName)) { - var actor = InitActor(slot.defaultActorName, slot.slotName, slot.defaultActorType); - actor.Show(); + if (slot.defaultActorType == ActorType.FrameAnimation) + { + if (Application.isPlaying) + { + StartCoroutine(InitActorAsync( + slot.defaultActorName, + slot.slotName, + slot.defaultActorType, + actor => actor?.Show())); + } + else + { + Debug.LogWarning("FrameAnimation 默认角色只能在 Play Mode 中异步加载。"); + } + } + else + { + var actor = InitActor(slot.defaultActorName, slot.slotName, slot.defaultActorType); + actor?.Show(); + } } else { @@ -134,6 +166,13 @@ namespace AibisDream public AbsActor InitActor(string actorName, string slotName, ActorType actorType) { + if (actorType == ActorType.FrameAnimation) + { + Debug.LogError( + $"FrameAnimation 角色 '{actorName}' 必须通过 InitActorAsync 初始化。"); + return null; + } + var slot = _slotDict[slotName]; var actor = _actorFactory.CreateActor(actorName, slot, actorType); UpsertActor(actorName, actor); @@ -141,6 +180,36 @@ namespace AibisDream return actor; } + public IEnumerator InitActorAsync( + string actorName, + string slotName, + ActorType actorType, + Action onCompleted = null) + { + if (!_slotDict.TryGetValue(slotName, out var slot)) + { + Debug.LogError( + $"[ActorManager] 初始化角色 '{actorName}' 失败:slot '{slotName}' 不存在。"); + onCompleted?.Invoke(null); + yield break; + } + + AbsActor actor = null; + yield return _actorFactory.CreateActorAsync( + actorName, + slot, + actorType, + created => actor = created); + if (actor == null) + { + onCompleted?.Invoke(null); + yield break; + } + + UpsertActor(actorName, actor); + onCompleted?.Invoke(actor); + } + public bool TryFindActor(string actorOrSlot, out AbsActor actor) { if (_actorDict.TryGetValue(actorOrSlot, out actor)) @@ -165,11 +234,11 @@ namespace AibisDream private const string ClinicSlotName = "clinic"; [YarnCommand("init_actor")] - public static void InitActor(string actorName, string slotName = ClinicSlotName, string actorType = "Anima") + public static IEnumerator InitActor(string actorName, string slotName = ClinicSlotName, string actorType = "Anima") { if (Enum.TryParse(actorType, out var actorTypeEnum)) { - ActorManager.Instance.InitActor(actorName, slotName, actorTypeEnum); + yield return ActorManager.Instance.InitActorAsync(actorName, slotName, actorTypeEnum); } else { diff --git a/Assets/Scripts/SceneManagement/ActorKit/FrameAnimationActor.cs b/Assets/Scripts/SceneManagement/ActorKit/FrameAnimationActor.cs new file mode 100644 index 000000000..f9d737207 --- /dev/null +++ b/Assets/Scripts/SceneManagement/ActorKit/FrameAnimationActor.cs @@ -0,0 +1,187 @@ +using System.Collections; +using System.Linq; +using AibisDream.FrameAnimation; +using AibisDream.Framework; +using AibisDream.SaveSystem; +using AibisDream.Utility; +using UnityEngine; +using UnityEngine.ResourceManagement.AsyncOperations; + +namespace AibisDream +{ + /// + /// 由 FrameAnimationGraph 驱动的 ActorManager 角色实现。 + /// Graph 使用 FrameAnimation/{ActorName} 的场景级 Addressable key。 + /// + public sealed class FrameAnimationActor : AbsActor + { + private FrameAnimationPlayer _player; + private string _currentStateName = string.Empty; + private bool _isReady; + + public override bool IsReady => _isReady; + + protected override void InitInternal() + { + _player = GetComponent(); + base.InitInternal(); + _actorType = ActorType.FrameAnimation; + _isReady = false; + } + + public override IEnumerator InitializeAsync() + { + if (_player == null) + { + Debug.LogError( + $"[FrameAnimationActor] 角色 '{ActorName}' 缺少 FrameAnimationPlayer。", + this); + yield break; + } + + var graphKey = ConstRef.FrameAnimationGraphPrefix + ActorName; + var handle = ResourceSystem.LoadSceneAsync(graphKey); + if (!handle.IsValid()) + { + Debug.LogError( + $"[FrameAnimationActor] 角色 '{ActorName}' 无法开始加载 Graph,key='{graphKey}'。", + this); + yield break; + } + + yield return handle; + if (handle.Status != AsyncOperationStatus.Succeeded || handle.Result == null) + { + Debug.LogError( + $"[FrameAnimationActor] 角色 '{ActorName}' 的 Graph 加载失败,key='{graphKey}'。", + this); + yield break; + } + + var validation = FrameAnimationGraphValidator.Validate(handle.Result); + if (validation.HasErrors) + { + var errors = string.Join( + " | ", + validation.Issues + .Where(issue => issue.Severity == FrameAnimationValidationSeverity.Error) + .Select(issue => $"{issue.Code}: {issue.Message}")); + Debug.LogError( + $"[FrameAnimationActor] 角色 '{ActorName}' 的 Graph 校验失败,key='{graphKey}':{errors}", + this); + yield break; + } + + if (!_player.TryBindGraph(handle.Result, out var bindError)) + { + Debug.LogError( + $"[FrameAnimationActor] 角色 '{ActorName}' 绑定 Graph 失败,key='{graphKey}', error={bindError}", + this); + yield break; + } + + _isReady = true; + } + + public override void ChangeState(string stateName) + { + if (!TryPlay(stateName, false, out var handle)) + { + return; + } + + handle.RegisterCompleted(result => LogFailedResult(stateName, result)); + } + + public override IEnumerator ChangeStateAsync(string stateName) + { + if (!TryPlay(stateName, false, out var handle)) + { + yield break; + } + + yield return handle; + LogFailedResult(stateName, handle.Result); + } + + public override ActorEntrySnapshotDto CaptureEntry() + { + var entry = new ActorEntrySnapshotDto(); + FillBaseEntry(entry); + entry.stateName = _currentStateName; + return entry; + } + + public override IEnumerator ApplyEntryAsync(ActorEntrySnapshotDto entry) + { + ApplyAlphaFromEntry(entry); + if (string.IsNullOrWhiteSpace(entry.stateName)) + { + yield break; + } + + if (!TryPlay(entry.stateName, true, out var handle)) + { + yield break; + } + + // 非循环终态会同步完成;Loop 会继续播放,读档 Barrier 不等待它。 + if (handle.IsCompleted) + { + LogFailedResult(entry.stateName, handle.Result); + } + } + + private bool TryPlay( + string stateName, + bool restoreTerminalState, + out FrameAnimationPlaybackHandle handle) + { + handle = null; + if (!_isReady || _player == null) + { + Debug.LogError( + $"[FrameAnimationActor] 角色 '{ActorName}' 尚未完成初始化,playable='{stateName}'。", + this); + return false; + } + + if (string.IsNullOrWhiteSpace(stateName)) + { + Debug.LogWarning( + $"[FrameAnimationActor] 角色 '{ActorName}' 收到空 playable id。", + this); + return false; + } + + handle = restoreTerminalState + ? _player.RestoreTerminalState(stateName) + : _player.Play(stateName); + if (handle.IsCompleted && + handle.Result.Reason == FrameAnimationCompletionReason.Failed) + { + LogFailedResult(stateName, handle.Result); + return false; + } + + _currentStateName = stateName; + return true; + } + + private void LogFailedResult( + string stateName, + FrameAnimationPlaybackResult result) + { + if (result.Reason != FrameAnimationCompletionReason.Failed) + { + return; + } + + var graphKey = ConstRef.FrameAnimationGraphPrefix + ActorName; + Debug.LogError( + $"[FrameAnimationActor] 角色 '{ActorName}' 播放失败,Graph='{graphKey}', " + + $"playable='{stateName}', error={result.Error}", + this); + } + } +} diff --git a/Assets/Scripts/SceneManagement/ActorKit/FrameAnimationActor.cs.meta b/Assets/Scripts/SceneManagement/ActorKit/FrameAnimationActor.cs.meta new file mode 100644 index 000000000..1730256b2 --- /dev/null +++ b/Assets/Scripts/SceneManagement/ActorKit/FrameAnimationActor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f2b56f5f150d4fc486d9c68615bd57dc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: