mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2024-12-29 02:35:06 +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 VisibleHeadAccessoryProcessor(vrcAvatarDescriptor).Process(context);
|
||||
new MeshSettingsPass(context).OnPreprocessAvatar();
|
||||
new ReplaceObjectPass(context).Process();
|
||||
new RemapAnimationPass(vrcAvatarDescriptor).Process(context.AnimationDatabase);
|
||||
new BlendshapeSyncAnimationProcessor().OnPreprocessAvatar(avatarGameObject, context);
|
||||
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,
|
||||
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 UnityEngine;
|
||||
using VRC.SDK3.Avatars.Components;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core
|
||||
{
|
||||
@ -41,6 +42,24 @@ namespace nadena.dev.modular_avatar.core
|
||||
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()
|
||||
{
|
||||
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…
Reference in New Issue
Block a user