feat: add actor behavior client presentation
This commit is contained in:
@@ -0,0 +1,129 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using XGame.ActorBehavior;
|
||||||
|
using XWorld.Framework.ActorBehavior;
|
||||||
|
|
||||||
|
public sealed class ActorBehaviorPresentationRouterTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void Dispatch_TimelineAnimation_PlaysAnimatorState()
|
||||||
|
{
|
||||||
|
var sink = new RecordingSink();
|
||||||
|
var router = new ActorBehaviorPresentationRouter();
|
||||||
|
|
||||||
|
router.Dispatch(new[]
|
||||||
|
{
|
||||||
|
new BehaviorEvent
|
||||||
|
{
|
||||||
|
Kind = BehaviorEventKind.TimelineAnimation,
|
||||||
|
ActorId = 7,
|
||||||
|
Animation = "140_skill01a_qt"
|
||||||
|
}
|
||||||
|
}, sink);
|
||||||
|
|
||||||
|
Assert.That(sink.Animations, Is.EqualTo(new[] { "7:140_skill01a_qt" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Dispatch_TimelineEffect_PlaysPrefabAtEventPosition()
|
||||||
|
{
|
||||||
|
var sink = new RecordingSink();
|
||||||
|
var router = new ActorBehaviorPresentationRouter();
|
||||||
|
|
||||||
|
router.Dispatch(new[]
|
||||||
|
{
|
||||||
|
new BehaviorEvent
|
||||||
|
{
|
||||||
|
Kind = BehaviorEventKind.TimelineEffect,
|
||||||
|
ActorId = 7,
|
||||||
|
Prefab = "Assets/Game/Art/Effect/Prefab/skill/9110004_zhujue_quantao_skill01a.prefab",
|
||||||
|
Position = new BehaviorVector3(1f, 2f, 3f)
|
||||||
|
}
|
||||||
|
}, sink);
|
||||||
|
|
||||||
|
Assert.That(sink.Effects.Select(e => e.ActorId), Is.EqualTo(new[] { 7 }));
|
||||||
|
Assert.That(sink.Effects.Select(e => e.PrefabPath), Is.EqualTo(new[] { "Assets/Game/Art/Effect/Prefab/skill/9110004_zhujue_quantao_skill01a.prefab" }));
|
||||||
|
Assert.That(sink.Effects[0].Position.X, Is.EqualTo(1f));
|
||||||
|
Assert.That(sink.Effects[0].Position.Y, Is.EqualTo(2f));
|
||||||
|
Assert.That(sink.Effects[0].Position.Z, Is.EqualTo(3f));
|
||||||
|
Assert.That(sink.Effects[0].HasExplicitPosition, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Dispatch_ProjectileTimelineEffect_TreatsZeroPositionAsExplicitWorldPosition()
|
||||||
|
{
|
||||||
|
var sink = new RecordingSink();
|
||||||
|
var router = new ActorBehaviorPresentationRouter();
|
||||||
|
|
||||||
|
router.Dispatch(new[]
|
||||||
|
{
|
||||||
|
new BehaviorEvent
|
||||||
|
{
|
||||||
|
Kind = BehaviorEventKind.TimelineEffect,
|
||||||
|
ActorId = 7,
|
||||||
|
FlyObjectId = "arrow01",
|
||||||
|
Prefab = "Assets/Game/Art/Effect/Prefab/skill/hit.prefab",
|
||||||
|
Position = BehaviorVector3.Zero
|
||||||
|
}
|
||||||
|
}, sink);
|
||||||
|
|
||||||
|
Assert.That(sink.Effects[0].HasExplicitPosition, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Dispatch_BehaviorStopped_StopsActorPresentation()
|
||||||
|
{
|
||||||
|
var sink = new RecordingSink();
|
||||||
|
var router = new ActorBehaviorPresentationRouter();
|
||||||
|
|
||||||
|
router.Dispatch(new[]
|
||||||
|
{
|
||||||
|
new BehaviorEvent
|
||||||
|
{
|
||||||
|
Kind = BehaviorEventKind.BehaviorStopped,
|
||||||
|
ActorId = 7
|
||||||
|
}
|
||||||
|
}, sink);
|
||||||
|
|
||||||
|
Assert.That(sink.StoppedActors, Is.EqualTo(new[] { 7 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RecordingSink : IActorBehaviorPresentationSink
|
||||||
|
{
|
||||||
|
public readonly List<string> Animations = new List<string>();
|
||||||
|
public readonly List<EffectRecord> Effects = new List<EffectRecord>();
|
||||||
|
public readonly List<int> StoppedActors = new List<int>();
|
||||||
|
|
||||||
|
public void PlayAnimation(int actorId, string stateName)
|
||||||
|
{
|
||||||
|
Animations.Add(actorId + ":" + stateName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PlayEffect(int actorId, string prefabPath, BehaviorVector3 position, bool hasExplicitPosition)
|
||||||
|
{
|
||||||
|
Effects.Add(new EffectRecord(actorId, prefabPath, position, hasExplicitPosition));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopActorPresentation(int actorId)
|
||||||
|
{
|
||||||
|
StoppedActors.Add(actorId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly struct EffectRecord
|
||||||
|
{
|
||||||
|
public readonly int ActorId;
|
||||||
|
public readonly string PrefabPath;
|
||||||
|
public readonly BehaviorVector3 Position;
|
||||||
|
public readonly bool HasExplicitPosition;
|
||||||
|
|
||||||
|
public EffectRecord(int actorId, string prefabPath, BehaviorVector3 position, bool hasExplicitPosition)
|
||||||
|
{
|
||||||
|
ActorId = actorId;
|
||||||
|
PrefabPath = prefabPath;
|
||||||
|
Position = position;
|
||||||
|
HasExplicitPosition = hasExplicitPosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 28bdddb483244b36967eea08c1fe7f58
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -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