feat: add actor behavior client presentation
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e5be8c7f074c4db3a92a03772f9844a3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,284 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using XWorld.Framework.ActorBehavior;
|
||||
using UObject = UnityEngine.Object;
|
||||
|
||||
namespace XGame.ActorBehavior
|
||||
{
|
||||
public sealed class ActorBehaviorPresentation : MonoBehaviour, IActorBehaviorPresentationSink
|
||||
{
|
||||
[SerializeField] private float crossFadeSeconds = 0.1f;
|
||||
[SerializeField] private bool validateAnimatorState = true;
|
||||
[SerializeField] private bool attachActorEffects = true;
|
||||
[SerializeField] private float defaultEffectLifetime;
|
||||
|
||||
private readonly ActorBehaviorPresentationRouter router = new ActorBehaviorPresentationRouter();
|
||||
private readonly Dictionary<int, ActorPresentationTarget> actors = new Dictionary<int, ActorPresentationTarget>();
|
||||
private readonly Dictionary<int, int> actorVersions = new Dictionary<int, int>();
|
||||
|
||||
public float CrossFadeSeconds
|
||||
{
|
||||
get => crossFadeSeconds;
|
||||
set => crossFadeSeconds = Mathf.Max(0f, value);
|
||||
}
|
||||
|
||||
public bool ValidateAnimatorState
|
||||
{
|
||||
get => validateAnimatorState;
|
||||
set => validateAnimatorState = value;
|
||||
}
|
||||
|
||||
public bool AttachActorEffects
|
||||
{
|
||||
get => attachActorEffects;
|
||||
set => attachActorEffects = value;
|
||||
}
|
||||
|
||||
public float DefaultEffectLifetime
|
||||
{
|
||||
get => defaultEffectLifetime;
|
||||
set => defaultEffectLifetime = Mathf.Max(0f, value);
|
||||
}
|
||||
|
||||
public void RegisterActor(int actorId, GameObject actorObject)
|
||||
{
|
||||
if (actorObject == null)
|
||||
{
|
||||
UnregisterActor(actorId, true);
|
||||
return;
|
||||
}
|
||||
|
||||
RegisterActor(actorId, actorObject.GetComponentInChildren<Animator>(), actorObject.transform, null);
|
||||
}
|
||||
|
||||
public void RegisterActor(int actorId, Animator animator, Transform effectRoot = null)
|
||||
{
|
||||
Transform actorTransform = animator != null ? animator.transform : effectRoot;
|
||||
RegisterActor(actorId, animator, actorTransform, effectRoot);
|
||||
}
|
||||
|
||||
public void RegisterActor(int actorId, Animator animator, Transform actorTransform, Transform effectRoot)
|
||||
{
|
||||
if (actorId <= 0)
|
||||
{
|
||||
Debug.LogWarning("[ActorBehaviorPresentation] ignore actor registration with invalid actorId=" + actorId);
|
||||
return;
|
||||
}
|
||||
|
||||
StopActorPresentation(actorId);
|
||||
actors[actorId] = new ActorPresentationTarget(animator, actorTransform, effectRoot, NextActorVersion(actorId));
|
||||
}
|
||||
|
||||
public void UnregisterActor(int actorId, bool destroySpawnedEffects = true)
|
||||
{
|
||||
if (destroySpawnedEffects)
|
||||
{
|
||||
StopActorPresentation(actorId);
|
||||
}
|
||||
|
||||
actors.Remove(actorId);
|
||||
}
|
||||
|
||||
public void PlayEvents(IEnumerable<BehaviorEvent> events)
|
||||
{
|
||||
router.Dispatch(events, this);
|
||||
}
|
||||
|
||||
public void PlayAnimation(int actorId, string stateName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(stateName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!actors.TryGetValue(actorId, out ActorPresentationTarget target) || target.Animator == null)
|
||||
{
|
||||
Debug.LogWarning("[ActorBehaviorPresentation] missing animator for actorId=" + actorId + ", state=" + stateName);
|
||||
return;
|
||||
}
|
||||
|
||||
int stateHash = Animator.StringToHash(stateName);
|
||||
if (validateAnimatorState && target.Animator.runtimeAnimatorController != null && !target.Animator.HasState(0, stateHash))
|
||||
{
|
||||
Debug.LogWarning("[ActorBehaviorPresentation] animator state not found: actorId=" + actorId + ", state=" + stateName);
|
||||
return;
|
||||
}
|
||||
|
||||
target.Animator.CrossFade(stateHash, crossFadeSeconds, 0, 0f);
|
||||
}
|
||||
|
||||
public void PlayEffect(int actorId, string prefabPath, BehaviorVector3 position)
|
||||
{
|
||||
PlayEffect(actorId, prefabPath, position, HasExplicitPosition(position));
|
||||
}
|
||||
|
||||
public void PlayEffect(int actorId, string prefabPath, BehaviorVector3 position, bool hasExplicitPosition)
|
||||
{
|
||||
if (string.IsNullOrEmpty(prefabPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StartCoroutine(LoadAndPlayEffect(actorId, prefabPath, position, hasExplicitPosition));
|
||||
}
|
||||
|
||||
public void StopActorPresentation(int actorId)
|
||||
{
|
||||
if (!actors.TryGetValue(actorId, out ActorPresentationTarget target))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
target.Version = NextActorVersion(actorId);
|
||||
for (int i = target.SpawnedEffects.Count - 1; i >= 0; i--)
|
||||
{
|
||||
DestroyUnityObject(target.SpawnedEffects[i]);
|
||||
}
|
||||
target.SpawnedEffects.Clear();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
StopAllCoroutines();
|
||||
var actorIds = new List<int>(actors.Keys);
|
||||
for (int i = 0; i < actorIds.Count; i++)
|
||||
{
|
||||
StopActorPresentation(actorIds[i]);
|
||||
}
|
||||
actors.Clear();
|
||||
}
|
||||
|
||||
private IEnumerator LoadAndPlayEffect(int actorId, string prefabPath, BehaviorVector3 position, bool hasExplicitPosition)
|
||||
{
|
||||
ActorPresentationTarget startTarget = null;
|
||||
bool actorBoundEffect = !hasExplicitPosition && actors.TryGetValue(actorId, out startTarget);
|
||||
int startVersion = actorBoundEffect && startTarget != null ? startTarget.Version : 0;
|
||||
UObject loaded = null;
|
||||
yield return XResLoader.coLoadRes(prefabPath, typeof(GameObject), obj => loaded = obj);
|
||||
|
||||
GameObject prefab = loaded as GameObject;
|
||||
if (prefab == null)
|
||||
{
|
||||
Debug.LogWarning("[ActorBehaviorPresentation] effect prefab load failed: " + prefabPath);
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (actorBoundEffect && (!actors.TryGetValue(actorId, out ActorPresentationTarget currentTarget) || currentTarget.Version != startVersion))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
GameObject instance = InstantiateEffect(actorId, prefab, position, hasExplicitPosition);
|
||||
if (instance == null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (actors.TryGetValue(actorId, out ActorPresentationTarget target))
|
||||
{
|
||||
target.SpawnedEffects.Add(instance);
|
||||
if (defaultEffectLifetime > 0f)
|
||||
{
|
||||
StartCoroutine(DestroyEffectAfterLifetime(actorId, target.Version, instance, defaultEffectLifetime));
|
||||
}
|
||||
}
|
||||
else if (defaultEffectLifetime > 0f)
|
||||
{
|
||||
Destroy(instance, defaultEffectLifetime);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator DestroyEffectAfterLifetime(int actorId, int version, GameObject instance, float lifetime)
|
||||
{
|
||||
yield return new WaitForSeconds(lifetime);
|
||||
if (actors.TryGetValue(actorId, out ActorPresentationTarget target) && target.Version == version)
|
||||
{
|
||||
target.SpawnedEffects.Remove(instance);
|
||||
}
|
||||
DestroyUnityObject(instance);
|
||||
}
|
||||
|
||||
private GameObject InstantiateEffect(int actorId, GameObject prefab, BehaviorVector3 position, bool hasExplicitPosition)
|
||||
{
|
||||
actors.TryGetValue(actorId, out ActorPresentationTarget target);
|
||||
|
||||
if (target != null && !hasExplicitPosition && attachActorEffects)
|
||||
{
|
||||
Transform parent = target.EffectRoot != null ? target.EffectRoot : target.ActorTransform;
|
||||
GameObject instance = Instantiate(prefab, parent);
|
||||
instance.transform.localPosition = Vector3.zero;
|
||||
instance.transform.localRotation = Quaternion.identity;
|
||||
return instance;
|
||||
}
|
||||
|
||||
Vector3 worldPosition = hasExplicitPosition ? ToUnity(position) : GetActorPosition(target);
|
||||
Quaternion worldRotation = target != null && target.ActorTransform != null
|
||||
? target.ActorTransform.rotation
|
||||
: Quaternion.identity;
|
||||
return Instantiate(prefab, worldPosition, worldRotation);
|
||||
}
|
||||
|
||||
private int NextActorVersion(int actorId)
|
||||
{
|
||||
int version;
|
||||
actorVersions.TryGetValue(actorId, out version);
|
||||
version++;
|
||||
actorVersions[actorId] = version;
|
||||
return version;
|
||||
}
|
||||
|
||||
private static Vector3 GetActorPosition(ActorPresentationTarget target)
|
||||
{
|
||||
return target != null && target.ActorTransform != null
|
||||
? target.ActorTransform.position
|
||||
: Vector3.zero;
|
||||
}
|
||||
|
||||
private static bool HasExplicitPosition(BehaviorVector3 position)
|
||||
{
|
||||
return Mathf.Abs(position.X) > 0.0001f
|
||||
|| Mathf.Abs(position.Y) > 0.0001f
|
||||
|| Mathf.Abs(position.Z) > 0.0001f;
|
||||
}
|
||||
|
||||
private static Vector3 ToUnity(BehaviorVector3 position)
|
||||
{
|
||||
return new Vector3(position.X, position.Y, position.Z);
|
||||
}
|
||||
|
||||
private static void DestroyUnityObject(UObject obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
Destroy(obj);
|
||||
}
|
||||
else
|
||||
{
|
||||
DestroyImmediate(obj);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ActorPresentationTarget
|
||||
{
|
||||
public readonly Animator Animator;
|
||||
public readonly Transform ActorTransform;
|
||||
public readonly Transform EffectRoot;
|
||||
public readonly List<GameObject> SpawnedEffects = new List<GameObject>();
|
||||
public int Version;
|
||||
|
||||
public ActorPresentationTarget(Animator animator, Transform actorTransform, Transform effectRoot, int version)
|
||||
{
|
||||
Animator = animator;
|
||||
ActorTransform = actorTransform;
|
||||
EffectRoot = effectRoot;
|
||||
Version = version;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 38bded11880342a5a5bc0f3b7d95fab5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using XWorld.Framework.ActorBehavior;
|
||||
|
||||
namespace XGame.ActorBehavior
|
||||
{
|
||||
public interface IActorBehaviorPresentationSink
|
||||
{
|
||||
void PlayAnimation(int actorId, string stateName);
|
||||
void PlayEffect(int actorId, string prefabPath, BehaviorVector3 position, bool hasExplicitPosition);
|
||||
void StopActorPresentation(int actorId);
|
||||
}
|
||||
|
||||
public sealed class ActorBehaviorPresentationRouter
|
||||
{
|
||||
public void Dispatch(IEnumerable<BehaviorEvent> events, IActorBehaviorPresentationSink sink)
|
||||
{
|
||||
if (sink == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(sink));
|
||||
}
|
||||
|
||||
if (events == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (BehaviorEvent evt in events)
|
||||
{
|
||||
if (evt == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (evt.Kind)
|
||||
{
|
||||
case BehaviorEventKind.TimelineAnimation:
|
||||
if (!string.IsNullOrEmpty(evt.Animation))
|
||||
{
|
||||
sink.PlayAnimation(evt.ActorId, evt.Animation);
|
||||
}
|
||||
break;
|
||||
case BehaviorEventKind.TimelineEffect:
|
||||
if (!string.IsNullOrEmpty(evt.Prefab))
|
||||
{
|
||||
sink.PlayEffect(evt.ActorId, evt.Prefab, evt.Position, HasExplicitPosition(evt));
|
||||
}
|
||||
break;
|
||||
case BehaviorEventKind.BehaviorStopped:
|
||||
sink.StopActorPresentation(evt.ActorId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasExplicitPosition(BehaviorEvent evt)
|
||||
{
|
||||
return !string.IsNullOrEmpty(evt.FlyObjectId)
|
||||
|| evt.Position.X != 0f
|
||||
|| evt.Position.Y != 0f
|
||||
|| evt.Position.Z != 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2bae7bcb6ca34e47b506cba5660d5b62
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user