feat(scene-mgmt): 添加 FrameAnimation 角色类型与异步加载
This commit is contained in:
@@ -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}' 无法开始加载 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f2b56f5f150d4fc486d9c68615bd57dc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user