feat(scene-mgmt): 添加 FrameAnimation 角色类型与异步加载

This commit is contained in:
2026-07-17 20:19:14 +08:00
parent 7c04f507db
commit 7646cb4db3
4 changed files with 378 additions and 7 deletions
@@ -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<AbsActor> 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<AbsActor> onCompleted)
{
if (!Enum.TryParse<ActorType>(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<AbsActor> onCompleted)
{
var prefabKey = ConstRef.FrameAnimationActorPrefabName;
var handle = ResourceSystem.LoadPersistentAsync<GameObject>(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<AbsActor>();
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
}
}
@@ -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<AbsActor> 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>(actorType, out var actorTypeEnum))
{
ActorManager.Instance.InitActor(actorName, slotName, actorTypeEnum);
yield return ActorManager.Instance.InitActorAsync(actorName, slotName, actorTypeEnum);
}
else
{
@@ -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
{
/// <summary>
/// 由 FrameAnimationGraph 驱动的 ActorManager 角色实现。
/// Graph 使用 FrameAnimation/{ActorName} 的场景级 Addressable key。
/// </summary>
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<FrameAnimationPlayer>();
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<FrameAnimationGraph>(graphKey);
if (!handle.IsValid())
{
Debug.LogError(
$"[FrameAnimationActor] 角色 '{ActorName}' 无法开始加载 Graphkey='{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);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f2b56f5f150d4fc486d9c68615bd57dc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: