refactor(actor-kit): Actor 体系迁移为 DTO 快照与异步恢复

This commit is contained in:
2026-06-24 22:43:08 +08:00
parent 3746f4aeb6
commit 2a0c34fd7a
7 changed files with 279 additions and 219 deletions
@@ -1,10 +1,10 @@
using System.Collections.Generic;
using System.Collections.Generic;
using AibisDream.Kit;
using UnityEngine;
namespace AibisDream.Framework
{
public class BubbleSlotKit : Singleton<BubbleSlotKit>
public class BubbleSlotKit : SingletonBase<BubbleSlotKit>
{
private readonly Dictionary<BubbleSlotEnum, BubbleSlotGroup> _slotDict = new();
@@ -1,3 +1,4 @@
using System.Collections;
using UnityEngine;
namespace AibisDream.SaveSystem
@@ -6,7 +7,7 @@ namespace AibisDream.SaveSystem
/// sections["actor"]:场景中各角色显隐、槽位、动画状态、透明度等(ActorManager)。
/// RestoreOrder=40,依赖 scene 与 env 已就绪。
/// </summary>
public class ActorSnapshotProvider : ISyncSnapshotProvider
public class ActorSnapshotProvider : IAsyncSnapshotProvider
{
public string SaveId => SnapshotProviderIds.Actor;
public int RestoreOrder => 40;
@@ -22,21 +23,21 @@ namespace AibisDream.SaveSystem
return ActorManager.Instance.CaptureSnapshot();
}
public void Restore(object dto, SnapshotRestoreContext context)
public IEnumerator RestoreAsync(object dto, SnapshotRestoreContext context)
{
if (dto is not ActorSnapshotDto actors)
{
context.Warn("[ActorSnapshotProvider] DTO 类型不匹配,跳过还原。");
return;
yield break;
}
if (ActorManager.Instance == null)
{
context.Warn("[ActorSnapshotProvider] ActorManager 未初始化,跳过还原。");
return;
yield break;
}
ActorManager.Instance.RestoreSnapshot(actors);
yield return ActorManager.Instance.RestoreSnapshotAsync(actors);
}
}
}
@@ -1,11 +1,10 @@
using System;
using System.Collections;
using AibisDream.Framework;
using AibisDream.SaveSystem;
using AibisDream.Utility;
using DG.Tweening;
using Newtonsoft.Json;
using UnityEngine;
using UnityEngine.Events;
using Object = UnityEngine.Object;
namespace AibisDream
@@ -16,47 +15,82 @@ namespace AibisDream
/// </summary>
public abstract class AbsActor : MonoBehaviour
{
public ActorData actorData;
public string ActorName => actorData.actorName;
public string SlotName => actorData.slotName;
protected string _actorName;
protected string _slotName;
protected ActorType _actorType;
protected float _alpha;
protected SpriteRenderer spriteRenderer;
public void Init(string actorName, ActorSlot slot)
{
actorData = new ActorData
{
actorName = actorName,
slotName = slot.slotName,
alpha = 0
};
public string ActorName => _actorName;
public string SlotName => _slotName;
public ActorType ActorType => _actorType;
Init();
public void Init(string actorName, ActorSlot slot, ActorType actorType)
{
_actorName = actorName;
_slotName = slot.slotName;
_actorType = actorType;
_alpha = 0;
InitInternal();
SetSlot(slot);
}
public void Init(ActorData data, ActorSlot slot)
public void InitFromEntry(ActorEntrySnapshotDto entry, ActorSlot slot)
{
actorData = data;
Init();
_actorName = entry.actorName;
_slotName = entry.slotName;
_alpha = entry.alpha;
if (!Enum.TryParse(entry.actorType, out _actorType))
{
_actorType = ActorType.Anima;
}
InitInternal();
SetSlot(slot);
}
protected virtual void Init()
protected virtual void InitInternal()
{
gameObject.name = ActorName;
spriteRenderer = GetComponent<SpriteRenderer>();
spriteRenderer.SetAlpha(0);
}
public abstract ActorEntrySnapshotDto CaptureEntry();
public abstract IEnumerator ApplyEntryAsync(ActorEntrySnapshotDto entry);
protected void FillBaseEntry(ActorEntrySnapshotDto entry)
{
entry.actorName = _actorName;
entry.slotName = _slotName;
entry.actorType = _actorType.ToString();
entry.alpha = _alpha;
}
protected void ApplyAlphaFromEntry(ActorEntrySnapshotDto entry)
{
if (entry.alpha <= 0.01f)
{
Hide();
}
else
{
SetAlpha(Mathf.RoundToInt(entry.alpha));
}
}
public void Show()
{
actorData.alpha = 1;
_alpha = 1;
spriteRenderer.SetAlpha(1);
}
public void Hide()
{
actorData.alpha = 0;
_alpha = 0;
spriteRenderer.SetAlpha(0);
}
@@ -66,24 +100,24 @@ namespace AibisDream
spriteRenderer.sortingOrder = slot.sortingOrder;
spriteRenderer.sortingLayerID = slot.sortingLayer;
actorData.slotName = slot.slotName;
_slotName = slot.slotName;
}
public IEnumerator FadeInAsync(float duration)
{
actorData.alpha = 1;
_alpha = 1;
yield return spriteRenderer.DOFade(1, duration).WaitForCompletion();
}
public IEnumerator FadeOutAsync(float duration)
{
actorData.alpha = 0;
_alpha = 0;
yield return spriteRenderer.DOFade(0, duration).WaitForCompletion();
}
public void SetAlpha(int alpha)
{
actorData.alpha = alpha;
_alpha = alpha;
spriteRenderer.SetAlpha(alpha);
}
@@ -100,7 +134,6 @@ namespace AibisDream
public virtual void SetFaceState(string faceName) {}
}
// 工厂类
public class ActorFactory
{
private readonly Transform _actorRoot;
@@ -118,38 +151,40 @@ namespace AibisDream
public AbsActor CreateActor(string actorName, ActorSlot slot, ActorType actorType)
{
var actor = actorType switch
var actor = InstantiateByType(actorType);
if (actor)
{
actor.Init(actorName, slot, actorType);
}
return actor;
}
public AbsActor CreateFromEntry(ActorEntrySnapshotDto entry, ActorSlot slot)
{
if (!Enum.TryParse<ActorType>(entry.actorType, out var actorType))
{
actorType = ActorType.Anima;
}
var actor = InstantiateByType(actorType);
if (actor)
{
actor.InitFromEntry(entry, slot);
}
return actor;
}
private AbsActor InstantiateByType(ActorType actorType)
{
return actorType switch
{
ActorType.Sprite => Object.Instantiate(_spriteActorPrefab, _actorRoot).GetComponent<AbsActor>(),
ActorType.Anima => Object.Instantiate(_actorAnimaPrefab, _actorRoot).GetComponent<AbsActor>(),
ActorType.AnimaEx => Object.Instantiate(_actorAnimaExPrefab, _actorRoot).GetComponent<AbsActor>(),
_ => null
};
if (actor)
{
actor.Init(actorName, slot);
}
return actor;
}
public AbsActor CreateActor(ActorData actorData, ActorSlot slot)
{
var actor = actorData.actorType switch
{
ActorType.Sprite => Object.Instantiate(_spriteActorPrefab, _actorRoot).GetComponent<AbsActor>(),
ActorType.Anima => Object.Instantiate(_actorAnimaPrefab, _actorRoot).GetComponent<AbsActor>(),
ActorType.AnimaEx => Object.Instantiate(_actorAnimaExPrefab, _actorRoot).GetComponent<AbsActor>(),
_ => null
};
if (actor)
{
actor.Init(actorData, slot);
}
return actor;
}
}
@@ -159,17 +194,4 @@ namespace AibisDream
Anima,
AnimaEx
}
[Serializable]
public class ActorData
{
[JsonIgnore] public UnityEvent onSave = new();
public string actorName;
public string slotName;
public ActorType actorType;
public string stateName;
public float alpha;
public float faceParam;
}
}
@@ -1,6 +1,7 @@
using System.Collections;
using System.Collections.Generic;
using AibisDream.Framework;
using AibisDream.SaveSystem;
using UnityEngine;
namespace AibisDream
@@ -10,34 +11,23 @@ namespace AibisDream
private const string HashPrefix = "#hash.";
private Animator _animator;
private Dictionary<int, string> _stateNameDict;
private Dictionary<int, string> _hashToStateName;
protected override void Init()
protected override void InitInternal()
{
_animator = GetComponent<Animator>();
// 设置基本信息
base.Init();
// 获取Controller
actorData.actorType = ActorType.Anima;
base.InitInternal();
_actorType = ActorType.Anima;
var controller = ResourceKit.LoadAssetSync<RuntimeAnimatorController>($"Animation/{ActorName}");
_animator.runtimeAnimatorController = controller;
// 保存事件
actorData.onSave.AddListener(() =>
{
actorData.stateName =
HashPrefix + _animator.GetCurrentAnimatorStateInfo(0).shortNameHash;
});
// 加载初始状态
if (!string.IsNullOrWhiteSpace(actorData.stateName))
{
ChangeState(actorData.stateName);
}
_hashToStateName = AnimatorKit.BuildStateHashToNameMap(controller);
}
public override void ChangeState(string animaName)
{
if (string.IsNullOrEmpty(animaName)) return;
if (animaName.StartsWith(HashPrefix))
{
var hash = int.Parse(animaName[HashPrefix.Length..]);
@@ -51,12 +41,23 @@ namespace AibisDream
public override IEnumerator ChangeStateAsync(string animaName)
{
if (string.IsNullOrEmpty(animaName))
{
yield break;
}
if (animaName.StartsWith(HashPrefix))
{
var hash = int.Parse(animaName[HashPrefix.Length..]);
_animator.Play(hash);
yield return null;
yield break;
}
_animator.Play(animaName);
// 等待一帧确保动画状态切换完成
yield return null;
// 等待直到动画状态名称匹配
while (!_animator.GetCurrentAnimatorStateInfo(0).IsName(animaName))
{
yield return null;
@@ -66,9 +67,48 @@ namespace AibisDream
yield return new WaitForSeconds(info.length);
}
private void OnDestroy()
public override ActorEntrySnapshotDto CaptureEntry()
{
actorData.onSave.RemoveAllListeners();
var entry = new ActorEntrySnapshotDto();
FillBaseEntry(entry);
var info = _animator.GetCurrentAnimatorStateInfo(0);
entry.stateNormalizedTime = info.normalizedTime;
if (_hashToStateName != null
&& _hashToStateName.TryGetValue(info.shortNameHash, out var name))
{
entry.stateName = name;
}
else
{
entry.stateName = HashPrefix + info.shortNameHash;
}
return entry;
}
public override IEnumerator ApplyEntryAsync(ActorEntrySnapshotDto entry)
{
ApplyAlphaFromEntry(entry);
if (string.IsNullOrEmpty(entry.stateName))
{
yield return null;
yield break;
}
if (entry.stateName.StartsWith(HashPrefix))
{
var hash = int.Parse(entry.stateName[HashPrefix.Length..]);
_animator.Play(hash, 0, entry.stateNormalizedTime);
}
else
{
_animator.Play(entry.stateName, 0, entry.stateNormalizedTime);
}
yield return null;
}
}
}
@@ -1,53 +1,41 @@
using AibisDream.Framework;
using System.Collections;
using System.Collections.Generic;
using AibisDream.Framework;
using AibisDream.SaveSystem;
using UnityEngine;
namespace AibisDream
{
public class ActorAnimaEx : AbsActor
{
private const string IsTalkingParam = "Is Talking";
private Animator _animator;
private string _currentStateName;
private string _currentFaceName;
private Dictionary<string, BlendTreeInfo> _blendTreeDict;
protected override void Init()
protected override void InitInternal()
{
_animator = GetComponent<Animator>();
// 设置基本信息
base.Init();
actorData.actorType = ActorType.AnimaEx;
base.InitInternal();
_actorType = ActorType.AnimaEx;
var controller = ResourceKit.LoadAssetSync<RuntimeAnimatorController>($"Animation/{ActorName}");
_animator.runtimeAnimatorController = controller;
ParseBlendTreeInfos();
// 保存事件
actorData.onSave.AddListener(() =>
{
actorData.stateName = _currentStateName;
actorData.faceParam = _animator.GetFloat("Face Param");
});
// 加载初始状态
if (!string.IsNullOrWhiteSpace(actorData.stateName))
{
ChangeState(actorData.stateName);
}
if (actorData.faceParam >= 0)
{
_animator.SetFloat("Face Param", actorData.faceParam);
}
}
private void ParseBlendTreeInfos()
{
_blendTreeDict = new Dictionary<string, BlendTreeInfo>();
// 解析混合树
var configText = ResourceKit.LoadAssetSync<TextAsset>($"AnimationConfig/{ActorName}");
if (configText != null)
{
if (configText == null) return;
var blendTreeConfig = AnimatorKit.ParseBlendTreeConfigFromJson(configText.text);
if (blendTreeConfig?.blendTrees == null) return;
foreach (var info in blendTreeConfig.blendTrees)
{
_blendTreeDict[info.name] = info;
@@ -55,12 +43,7 @@ namespace AibisDream
_currentStateName = blendTreeConfig.defaultStateName;
}
}
/// <summary>
/// 切换站姿
/// </summary>
/// <param name="stateName"></param>
public override void ChangeState(string stateName)
{
StartCoroutine(ChangeStateAsync(stateName));
@@ -68,36 +51,32 @@ namespace AibisDream
public override IEnumerator ChangeStateAsync(string stateName)
{
// 记录当前状态
_currentStateName = stateName;
// 切换站姿
_animator.SetTrigger(stateName);
// 等待动作切换完成
while (!_animator.GetCurrentAnimatorStateInfo(0).IsName(stateName))
{
yield return null;
}
// var info = _animator.GetCurrentAnimatorStateInfo(0);
// yield return new WaitForSeconds(info.length);
_animator.ResetTrigger(stateName);
Debug.Log($"当前状态{_currentStateName}");
}
public override void SetTalkingState(bool isTalking)
{
_animator.SetFloat("Is Talking", isTalking ? 1 : 0);
_animator.SetFloat(IsTalkingParam, isTalking ? 1 : 0);
}
public override void SetFaceState(string faceName)
{
// 先读取当前混合树信息
if (_blendTreeDict.TryGetValue(_currentStateName, out var blendTreeInfo))
_currentFaceName = faceName;
if (!_blendTreeDict.TryGetValue(_currentStateName, out var blendTreeInfo))
{
Debug.Log($"当前状态{_currentStateName}的混合树信息存在");
Debug.LogWarning($"当前状态{_currentStateName}的混合树信息存在");
return;
}
if (blendTreeInfo.type == "1D")
{
var clipInfo = blendTreeInfo.GetClipInfo(faceName);
@@ -112,14 +91,35 @@ namespace AibisDream
if (clipInfo != null)
{
_animator.SetFloat(blendTreeInfo.blendParameter, clipInfo.position.x);
// 不设置Y轴参数
}
}
}
else
public override ActorEntrySnapshotDto CaptureEntry()
{
Debug.LogWarning($"当前状态{_currentStateName}的混合树信息不存在");
var entry = new ActorEntrySnapshotDto();
FillBaseEntry(entry);
entry.stateName = _currentStateName;
entry.faceName = _currentFaceName;
entry.isTalking = _animator.GetFloat(IsTalkingParam) > 0.5f;
return entry;
}
public override IEnumerator ApplyEntryAsync(ActorEntrySnapshotDto entry)
{
ApplyAlphaFromEntry(entry);
if (!string.IsNullOrEmpty(entry.stateName))
{
yield return ChangeStateAsync(entry.stateName);
}
if (!string.IsNullOrEmpty(entry.faceName))
{
SetFaceState(entry.faceName);
}
SetTalkingState(entry.isTalking);
}
}
}
@@ -71,74 +71,45 @@ namespace AibisDream
var dto = new ActorSnapshotDto();
foreach (var actor in _actorDict.Values)
{
var data = actor.actorData;
dto.actors.Add(new ActorEntrySnapshotDto
{
actorName = data.actorName,
slotName = data.slotName,
actorType = data.actorType.ToString(),
stateName = data.stateName,
alpha = data.alpha,
faceParam = data.faceParam
});
dto.actors.Add(actor.CaptureEntry());
}
return dto;
}
public void RestoreSnapshot(ActorSnapshotDto dto)
public IEnumerator RestoreSnapshotAsync(ActorSnapshotDto dto)
{
if (dto?.actors == null) return;
DestroyEditorTemp();
if (dto?.actors == null)
{
yield break;
}
foreach (var entry in dto.actors)
{
if (!Enum.TryParse<ActorType>(entry.actorType, out var actorType))
if (!_slotDict.TryGetValue(entry.slotName, out var slot))
{
actorType = ActorType.Anima;
}
var actorData = new ActorData
{
actorName = entry.actorName,
slotName = entry.slotName,
actorType = actorType,
stateName = entry.stateName,
alpha = entry.alpha,
faceParam = entry.faceParam
};
if (!_slotDict.TryGetValue(actorData.slotName, out var slot))
{
Debug.LogWarning($"slot {actorData.slotName} 不存在");
Debug.LogWarning($"slot {entry.slotName} 不存在");
continue;
}
var actor = _actorFactory.CreateActor(actorData, slot);
var actor = _actorFactory.CreateFromEntry(entry, slot);
if (actor == null) continue;
_actorDict.Add(actorData.actorName, actor);
ApplyActorVisualState(actor, actorData);
UpsertActor(entry.actorName, actor);
yield return actor.ApplyEntryAsync(entry);
}
}
private void UpsertActor(string actorName, AbsActor actor)
{
if (_actorDict.TryGetValue(actorName, out var existing) && existing != null)
{
Destroy(existing.gameObject);
}
private static void ApplyActorVisualState(AbsActor actor, ActorData data)
{
if (data.alpha <= 0.01f)
{
actor.Hide();
}
else
{
actor.SetAlpha(Mathf.RoundToInt(data.alpha));
}
if (!string.IsNullOrEmpty(data.stateName))
{
actor.ChangeState(data.stateName);
}
_actorDict[actorName] = actor;
}
#if UNITY_EDITOR
@@ -165,7 +136,7 @@ namespace AibisDream
{
var slot = _slotDict[slotName];
var actor = _actorFactory.CreateActor(actorName, slot, actorType);
_actorDict.Add(actorName, actor);
UpsertActor(actorName, actor);
return actor;
}
@@ -1,23 +1,49 @@
using AibisDream.Framework;
using AibisDream.Utility;
using System.Collections;
using AibisDream.Framework;
using AibisDream.SaveSystem;
using UnityEngine;
namespace AibisDream
{
public class SpriteActor : AbsActor
{
protected override void Init()
private string _stateName = "Idle";
protected override void InitInternal()
{
base.Init();
actorData.actorType = ActorType.Sprite;
// 赋值
ChangeState(string.IsNullOrWhiteSpace(actorData.stateName) ? "Idle" : actorData.stateName);
base.InitInternal();
_actorType = ActorType.Sprite;
ChangeState("Idle");
}
public override void ChangeState(string stateName)
{
spriteRenderer.sprite = ResourceKit.LoadAssetSync<Sprite>($"Sprite/{ActorName}[{stateName}]");
actorData.stateName = stateName;
_stateName = string.IsNullOrWhiteSpace(stateName) ? "Idle" : stateName;
spriteRenderer.sprite = ResourceKit.LoadAssetSync<Sprite>($"Sprite/{ActorName}[{_stateName}]");
}
public override ActorEntrySnapshotDto CaptureEntry()
{
var entry = new ActorEntrySnapshotDto();
FillBaseEntry(entry);
entry.stateName = _stateName;
return entry;
}
public override IEnumerator ApplyEntryAsync(ActorEntrySnapshotDto entry)
{
ApplyAlphaFromEntry(entry);
if (!string.IsNullOrEmpty(entry.stateName))
{
ChangeState(entry.stateName);
}
else
{
ChangeState("Idle");
}
yield break;
}
}
}