mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2025-04-28 15:28:59 +08:00
feat: add the Replace Object component
This commit is contained in:
parent
4b40c5197a
commit
4240a4f4cf
3
Assets/_ModularAvatar/EditModeTests/ReplaceObject.meta
Normal file
3
Assets/_ModularAvatar/EditModeTests/ReplaceObject.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4c1526a4a3c8483696aed2fa0c98e3da
|
||||||
|
timeCreated: 1690621713
|
@ -0,0 +1,169 @@
|
|||||||
|
using System;
|
||||||
|
using nadena.dev.modular_avatar.core;
|
||||||
|
using nadena.dev.modular_avatar.core.editor;
|
||||||
|
using nadena.dev.modular_avatar.editor.ErrorReporting;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using UnityEditor.VersionControl;
|
||||||
|
using UnityEngine;
|
||||||
|
using VRC.SDK3.Avatars.Components;
|
||||||
|
|
||||||
|
namespace modular_avatar_tests.ReplaceObject
|
||||||
|
{
|
||||||
|
public class ReplaceObjectTests : TestBase
|
||||||
|
{
|
||||||
|
void Process(GameObject root)
|
||||||
|
{
|
||||||
|
var avDesc = root.GetComponent<VRCAvatarDescriptor>();
|
||||||
|
new ReplaceObjectPass(new BuildContext(avDesc)).Process();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void smokeTest()
|
||||||
|
{
|
||||||
|
var root = CreateRoot("root");
|
||||||
|
var replacee = CreateChild(root, "replacee");
|
||||||
|
var replacement = CreateChild(root, "replacement");
|
||||||
|
|
||||||
|
replacee.AddComponent<SphereCollider>();
|
||||||
|
replacement.AddComponent<BoxCollider>();
|
||||||
|
|
||||||
|
var replaceObject = root.AddComponent<ModularAvatarReplaceObject>();
|
||||||
|
replaceObject.targetObject.referencePath = RuntimeUtil.AvatarRootPath(replacee);
|
||||||
|
|
||||||
|
Process(root);
|
||||||
|
|
||||||
|
Assert.AreEqual(1, root.transform.childCount);
|
||||||
|
Assert.AreEqual(root.transform.GetChild(0).gameObject, replacement);
|
||||||
|
//Assert.AreEqual(replacement.name, "replacee");
|
||||||
|
Assert.IsTrue(replacee == null);
|
||||||
|
|
||||||
|
Assert.NotNull(replacement.GetComponent<BoxCollider>());
|
||||||
|
Assert.IsNull(root.GetComponentInChildren<SphereCollider>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestComponent : MonoBehaviour
|
||||||
|
{
|
||||||
|
public Transform transformRef;
|
||||||
|
public GameObject gameObjectRef;
|
||||||
|
public BoxCollider memberRef;
|
||||||
|
public BoxCollider secondMemberRef;
|
||||||
|
public SphereCollider lostRef;
|
||||||
|
public Transform rootRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void rewritesReferences()
|
||||||
|
{
|
||||||
|
var root = CreateRoot("root");
|
||||||
|
var replacee = CreateChild(root, "replacee");
|
||||||
|
var replacement = CreateChild(root, "replacement");
|
||||||
|
var reference = CreateChild(root, "reference");
|
||||||
|
|
||||||
|
var testComponent = reference.AddComponent<TestComponent>();
|
||||||
|
testComponent.transformRef = replacee.transform;
|
||||||
|
testComponent.gameObjectRef = replacee;
|
||||||
|
testComponent.memberRef = replacee.AddComponent<BoxCollider>();
|
||||||
|
testComponent.secondMemberRef = replacee.AddComponent<BoxCollider>();
|
||||||
|
testComponent.lostRef = replacee.AddComponent<SphereCollider>();
|
||||||
|
testComponent.rootRef = root.transform;
|
||||||
|
|
||||||
|
var newBoxCollider1 = replacement.AddComponent<BoxCollider>();
|
||||||
|
var newBoxCollider2 = replacement.AddComponent<BoxCollider>();
|
||||||
|
newBoxCollider1.center = Vector3.up; // just to make it easier to observe in the debugger
|
||||||
|
newBoxCollider2.center = Vector3.up * 2;
|
||||||
|
|
||||||
|
var replaceObject = replacement.AddComponent<ModularAvatarReplaceObject>();
|
||||||
|
replaceObject.targetObject.Set(replacee);
|
||||||
|
|
||||||
|
Process(root);
|
||||||
|
|
||||||
|
Assert.AreEqual(2, root.transform.childCount);
|
||||||
|
Assert.AreEqual(root.transform.GetChild(0).gameObject, replacement);
|
||||||
|
Assert.AreEqual(testComponent.transformRef, replacement.transform);
|
||||||
|
Assert.AreEqual(testComponent.gameObjectRef, replacement);
|
||||||
|
Assert.AreEqual(testComponent.memberRef, newBoxCollider1);
|
||||||
|
Assert.AreEqual(testComponent.secondMemberRef, newBoxCollider2);
|
||||||
|
Assert.AreEqual(testComponent.lostRef, null);
|
||||||
|
Assert.AreEqual(testComponent.rootRef, root.transform);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test: disallow replacing parent of replacee
|
||||||
|
[Test]
|
||||||
|
public void disallowReplacingParent()
|
||||||
|
{
|
||||||
|
var root = CreateRoot("root");
|
||||||
|
var replacee = CreateChild(root, "replacee");
|
||||||
|
var replacement = CreateChild(root, "replacement");
|
||||||
|
|
||||||
|
var replaceObject = replacement.AddComponent<ModularAvatarReplaceObject>();
|
||||||
|
replaceObject.targetObject.Set(root);
|
||||||
|
|
||||||
|
BuildReport.Clear();
|
||||||
|
Assert.Throws<Exception>(() =>
|
||||||
|
{
|
||||||
|
using (BuildReport.CurrentReport.ReportingOnAvatar(root.GetComponent<VRCAvatarDescriptor>()))
|
||||||
|
{
|
||||||
|
Process(root);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void abortWhenTargetNull()
|
||||||
|
{
|
||||||
|
var root = CreateRoot("root");
|
||||||
|
var replacee = CreateChild(root, "replacee");
|
||||||
|
var replacement = CreateChild(root, "replacement");
|
||||||
|
|
||||||
|
var replaceObject = replacement.AddComponent<ModularAvatarReplaceObject>();
|
||||||
|
replaceObject.targetObject.Set(null);
|
||||||
|
|
||||||
|
BuildReport.Clear();
|
||||||
|
Assert.Throws<Exception>(() =>
|
||||||
|
{
|
||||||
|
using (BuildReport.CurrentReport.ReportingOnAvatar(root.GetComponent<VRCAvatarDescriptor>()))
|
||||||
|
{
|
||||||
|
Process(root);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test: child object handling
|
||||||
|
[Test]
|
||||||
|
public void preservesExistingChildObjects()
|
||||||
|
{
|
||||||
|
var root = CreateRoot("root");
|
||||||
|
var replacee = CreateChild(root, "replacee");
|
||||||
|
var replacement = CreateChild(root, "replacement");
|
||||||
|
|
||||||
|
var child1 = CreateChild(replacee, "child1");
|
||||||
|
var child2 = CreateChild(replacement, "child2");
|
||||||
|
|
||||||
|
var replaceObject = replacement.AddComponent<ModularAvatarReplaceObject>();
|
||||||
|
replaceObject.targetObject.Set(replacee);
|
||||||
|
|
||||||
|
Process(root);
|
||||||
|
|
||||||
|
Assert.AreEqual(1, root.transform.childCount);
|
||||||
|
Assert.AreEqual(replacement.transform, child1.transform.parent);
|
||||||
|
Assert.AreEqual(replacement.transform, child2.transform.parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test: PathMappings
|
||||||
|
[Test]
|
||||||
|
public void updatesPathMappings()
|
||||||
|
{
|
||||||
|
var root = CreateRoot("root");
|
||||||
|
var replacee = CreateChild(root, "replacee");
|
||||||
|
var replacement = CreateChild(root, "replacement");
|
||||||
|
|
||||||
|
var replaceObject = replacement.AddComponent<ModularAvatarReplaceObject>();
|
||||||
|
replaceObject.targetObject.Set(replacee);
|
||||||
|
|
||||||
|
PathMappings.Init(root);
|
||||||
|
Process(root);
|
||||||
|
|
||||||
|
Assert.AreEqual("replacement", PathMappings.MapPath("replacee"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: bdc6f8e53d1843ec8d22d7f5141aff52
|
||||||
|
timeCreated: 1690621719
|
@ -207,6 +207,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
new BoneProxyProcessor().OnPreprocessAvatar(avatarGameObject);
|
new BoneProxyProcessor().OnPreprocessAvatar(avatarGameObject);
|
||||||
new VisibleHeadAccessoryProcessor(vrcAvatarDescriptor).Process(context);
|
new VisibleHeadAccessoryProcessor(vrcAvatarDescriptor).Process(context);
|
||||||
new MeshSettingsPass(context).OnPreprocessAvatar();
|
new MeshSettingsPass(context).OnPreprocessAvatar();
|
||||||
|
new ReplaceObjectPass(context).Process();
|
||||||
new RemapAnimationPass(vrcAvatarDescriptor).Process(context.AnimationDatabase);
|
new RemapAnimationPass(vrcAvatarDescriptor).Process(context.AnimationDatabase);
|
||||||
new BlendshapeSyncAnimationProcessor().OnPreprocessAvatar(avatarGameObject, context);
|
new BlendshapeSyncAnimationProcessor().OnPreprocessAvatar(avatarGameObject, context);
|
||||||
PhysboneBlockerPass.Process(avatarGameObject);
|
PhysboneBlockerPass.Process(avatarGameObject);
|
||||||
|
@ -114,6 +114,37 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks an object as having been replaced by another object. All references to the old object will be replaced
|
||||||
|
/// by the new object. References originally to the new object will continue to point to the new object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="old"></param>
|
||||||
|
/// <param name="newObject"></param>
|
||||||
|
public static void ReplaceObject(GameObject old, GameObject newObject)
|
||||||
|
{
|
||||||
|
ClearCache();
|
||||||
|
|
||||||
|
if (_objectToOriginalPaths.TryGetValue(old, out var paths))
|
||||||
|
{
|
||||||
|
if (!_objectToOriginalPaths.TryGetValue(newObject, out var newObjectPaths))
|
||||||
|
{
|
||||||
|
newObjectPaths = new List<string>();
|
||||||
|
_objectToOriginalPaths.Add(newObject, newObjectPaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
newObjectPaths.AddRange(paths);
|
||||||
|
|
||||||
|
_objectToOriginalPaths.Remove(old);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (_transformLookthroughObjects.Contains(old))
|
||||||
|
{
|
||||||
|
_transformLookthroughObjects.Remove(old);
|
||||||
|
_transformLookthroughObjects.Add(newObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static ImmutableDictionary<string, string> BuildMapping(ref ImmutableDictionary<string, string> cache,
|
private static ImmutableDictionary<string, string> BuildMapping(ref ImmutableDictionary<string, string> cache,
|
||||||
bool transformLookup)
|
bool transformLookup)
|
||||||
{
|
{
|
||||||
|
219
Packages/nadena.dev.modular-avatar/Editor/ReplaceObjectPass.cs
Normal file
219
Packages/nadena.dev.modular-avatar/Editor/ReplaceObjectPass.cs
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using nadena.dev.modular_avatar.editor.ErrorReporting;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace nadena.dev.modular_avatar.core.editor
|
||||||
|
{
|
||||||
|
using UnityObject = UnityEngine.Object;
|
||||||
|
// ReSharper disable once RedundantUsingDirective
|
||||||
|
using Object = System.Object;
|
||||||
|
|
||||||
|
internal class ReplaceObjectPass
|
||||||
|
{
|
||||||
|
private readonly BuildContext _buildContext;
|
||||||
|
|
||||||
|
public ReplaceObjectPass(BuildContext context)
|
||||||
|
{
|
||||||
|
_buildContext = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Reference
|
||||||
|
{
|
||||||
|
public UnityObject Source;
|
||||||
|
public string PropPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Process()
|
||||||
|
{
|
||||||
|
var avatarDescriptor = _buildContext.AvatarDescriptor;
|
||||||
|
var replacementComponents =
|
||||||
|
avatarDescriptor.GetComponentsInChildren<ModularAvatarReplaceObject>(true);
|
||||||
|
|
||||||
|
if (replacementComponents.Length == 0) return;
|
||||||
|
|
||||||
|
// Build an index of object references within the avatar that we might need to fix
|
||||||
|
Dictionary<UnityObject, List<Reference>> refIndex = BuildReferenceIndex();
|
||||||
|
|
||||||
|
Dictionary<GameObject, (ModularAvatarReplaceObject, GameObject)> replacements
|
||||||
|
= new Dictionary<GameObject, (ModularAvatarReplaceObject, GameObject)>();
|
||||||
|
|
||||||
|
foreach (var component in replacementComponents)
|
||||||
|
{
|
||||||
|
var targetObject = component.targetObject?.Get(_buildContext.AvatarDescriptor);
|
||||||
|
|
||||||
|
if (targetObject == null)
|
||||||
|
{
|
||||||
|
BuildReport.LogFatal("replace_object.null_target", new string[0],
|
||||||
|
component, targetObject);
|
||||||
|
UnityObject.DestroyImmediate(component.gameObject);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.transform.GetComponentsInParent<Transform>().Contains(targetObject.transform))
|
||||||
|
{
|
||||||
|
BuildReport.LogFatal("replace_object.parent_of_target", new string[0],
|
||||||
|
component, targetObject);
|
||||||
|
UnityObject.DestroyImmediate(component.gameObject);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replacements.TryGetValue(targetObject, out var existingReplacement))
|
||||||
|
{
|
||||||
|
BuildReport.LogFatal("replace_object.replacing_replacement", new string[0],
|
||||||
|
component, existingReplacement.Item1);
|
||||||
|
UnityObject.DestroyImmediate(component);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
replacements[targetObject] = (component, component.gameObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute replacement. For now, we reparent children.
|
||||||
|
// TODO: Handle replacing recursively.
|
||||||
|
|
||||||
|
foreach (var kvp in replacements)
|
||||||
|
{
|
||||||
|
var original = kvp.Key;
|
||||||
|
var replacement = kvp.Value.Item2;
|
||||||
|
|
||||||
|
replacement.transform.SetParent(original.transform.parent, true);
|
||||||
|
var siblingIndex = original.transform.GetSiblingIndex();
|
||||||
|
|
||||||
|
// Move children of original parent
|
||||||
|
foreach (Transform child in original.transform)
|
||||||
|
{
|
||||||
|
child.SetParent(replacement.transform, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update property references
|
||||||
|
foreach (var refKey in GetIndexedComponents(original))
|
||||||
|
{
|
||||||
|
var (component, index) = refKey;
|
||||||
|
|
||||||
|
if (!refIndex.TryGetValue(component, out var references))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
UnityObject newValue = null;
|
||||||
|
if (component is GameObject)
|
||||||
|
{
|
||||||
|
newValue = replacement;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var replacementCandidates = replacement.GetComponents(component.GetType());
|
||||||
|
|
||||||
|
if (replacementCandidates.Length > index)
|
||||||
|
{
|
||||||
|
newValue = replacementCandidates[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var reference in references)
|
||||||
|
{
|
||||||
|
SerializedObject so = new SerializedObject(reference.Source);
|
||||||
|
SerializedProperty prop = so.FindProperty(reference.PropPath);
|
||||||
|
prop.objectReferenceValue = newValue;
|
||||||
|
so.ApplyModifiedPropertiesWithoutUndo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PathMappings.ReplaceObject(original, replacement);
|
||||||
|
|
||||||
|
// Destroy original
|
||||||
|
UnityObject.DestroyImmediate(original);
|
||||||
|
replacement.transform.SetSiblingIndex(siblingIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<(UnityObject, int)> GetIndexedComponents(GameObject original)
|
||||||
|
{
|
||||||
|
yield return (original, -1);
|
||||||
|
|
||||||
|
Dictionary<Type, int> componentTypeIndex = new Dictionary<Type, int>();
|
||||||
|
foreach (var component in original.GetComponents<Component>())
|
||||||
|
{
|
||||||
|
if (!componentTypeIndex.TryGetValue(component.GetType(), out int index))
|
||||||
|
{
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentTypeIndex[component.GetType()] = index + 1;
|
||||||
|
|
||||||
|
yield return (component, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<UnityObject, List<Reference>> BuildReferenceIndex()
|
||||||
|
{
|
||||||
|
Dictionary<UnityObject, List<Reference>> refIndex = new Dictionary<UnityObject, List<Reference>>();
|
||||||
|
|
||||||
|
IndexObject(_buildContext.AvatarDescriptor.gameObject);
|
||||||
|
|
||||||
|
return refIndex;
|
||||||
|
|
||||||
|
void IndexObject(GameObject obj)
|
||||||
|
{
|
||||||
|
foreach (Transform child in obj.transform)
|
||||||
|
{
|
||||||
|
IndexObject(child.gameObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary<Type, int> componentIndex = new Dictionary<Type, int>();
|
||||||
|
foreach (Component c in obj.GetComponents(typeof(Component)))
|
||||||
|
{
|
||||||
|
if (c == null) continue;
|
||||||
|
if (c is Transform) continue;
|
||||||
|
|
||||||
|
if (!componentIndex.TryGetValue(c.GetType(), out int index)) index = 0;
|
||||||
|
componentIndex[c.GetType()] = index + 1;
|
||||||
|
|
||||||
|
var so = new SerializedObject(c);
|
||||||
|
var sp = so.GetIterator();
|
||||||
|
bool enterChildren = true;
|
||||||
|
while (sp.Next(enterChildren))
|
||||||
|
{
|
||||||
|
enterChildren = true;
|
||||||
|
|
||||||
|
if (sp.propertyType == SerializedPropertyType.String) enterChildren = false;
|
||||||
|
if (sp.propertyType != SerializedPropertyType.ObjectReference) continue;
|
||||||
|
if (sp.objectReferenceValue == null) continue;
|
||||||
|
|
||||||
|
string path = sp.propertyPath;
|
||||||
|
if (path == "m_GameObject") continue;
|
||||||
|
|
||||||
|
Reference reference;
|
||||||
|
if (sp.objectReferenceValue is GameObject)
|
||||||
|
{
|
||||||
|
// ok
|
||||||
|
}
|
||||||
|
else if (sp.objectReferenceValue is Component)
|
||||||
|
{
|
||||||
|
// ok
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
reference.Source = c;
|
||||||
|
reference.PropPath = sp.propertyPath;
|
||||||
|
|
||||||
|
var refKey = sp.objectReferenceValue;
|
||||||
|
if (!refIndex.TryGetValue(refKey, out var refList))
|
||||||
|
{
|
||||||
|
refList = new List<Reference>();
|
||||||
|
refIndex[refKey] = refList;
|
||||||
|
}
|
||||||
|
|
||||||
|
refList.Add(reference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b119c0a381384ad88e78550d23997aa7
|
||||||
|
timeCreated: 1690004198
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using VRC.SDK3.Avatars.Components;
|
||||||
|
|
||||||
namespace nadena.dev.modular_avatar.core
|
namespace nadena.dev.modular_avatar.core
|
||||||
{
|
{
|
||||||
@ -41,6 +42,24 @@ namespace nadena.dev.modular_avatar.core
|
|||||||
return (_cachedReference = avatar.transform.Find(referencePath)?.gameObject);
|
return (_cachedReference = avatar.transform.Find(referencePath)?.gameObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Set(GameObject target)
|
||||||
|
{
|
||||||
|
if (target == null)
|
||||||
|
{
|
||||||
|
referencePath = "";
|
||||||
|
}
|
||||||
|
else if (target.GetComponent<VRCAvatarDescriptor>() != null)
|
||||||
|
{
|
||||||
|
referencePath = AVATAR_ROOT;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
referencePath = RuntimeUtil.AvatarRootPath(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
_cacheValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
private void InvalidateCache()
|
private void InvalidateCache()
|
||||||
{
|
{
|
||||||
RuntimeUtil.OnHierarchyChanged -= InvalidateCache;
|
RuntimeUtil.OnHierarchyChanged -= InvalidateCache;
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace nadena.dev.modular_avatar.core
|
||||||
|
{
|
||||||
|
[AddComponentMenu("Modular Avatar/MA Replace Object")]
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
public class ModularAvatarReplaceObject : AvatarTagComponent
|
||||||
|
{
|
||||||
|
public AvatarObjectReference targetObject = new AvatarObjectReference();
|
||||||
|
// public bool deleteChildren;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7e949680c0864ee7b441d9b2c93b890b
|
||||||
|
timeCreated: 1690004129
|
Loading…
x
Reference in New Issue
Block a user