diff --git a/Packages/net.fushizen.modular-avatar/Editor/Inspector.meta b/Packages/net.fushizen.modular-avatar/Editor/Inspector.meta new file mode 100644 index 00000000..a820ab04 --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Editor/Inspector.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f83eeca568d546a3b9a1d313e84b91cf +timeCreated: 1664754610 \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Editor/Inspector/AvatarObjectReferenceDrawer.cs b/Packages/net.fushizen.modular-avatar/Editor/Inspector/AvatarObjectReferenceDrawer.cs new file mode 100644 index 00000000..196258aa --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Editor/Inspector/AvatarObjectReferenceDrawer.cs @@ -0,0 +1,121 @@ +using UnityEditor; +using UnityEngine; + +namespace net.fushizen.modular_avatar.core.editor +{ + [CustomPropertyDrawer(typeof(AvatarObjectReference))] + public class AvatarObjectReferenceDrawer : PropertyDrawer + { + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + if (!CustomGUI(position, property, label)) + { + var xButtonSize = EditorStyles.miniButtonRight.CalcSize(new GUIContent("x")); + var xButtonRect = new Rect(position.xMax - xButtonSize.x, position.y, xButtonSize.x, position.height); + position = new Rect(position.x, position.y, position.width - xButtonSize.x, position.height); + + var isNull = property.FindPropertyRelative(nameof(AvatarObjectReference.isNull)); + property = property.FindPropertyRelative(nameof(AvatarObjectReference.referencePath)); + + position = EditorGUI.PrefixLabel(position, label); + + EditorGUI.LabelField(position, isNull.boolValue ? "(null)" : property.stringValue); + } + } + + private bool CustomGUI(Rect position, SerializedProperty property, GUIContent label) + { + var indentLevel = EditorGUI.indentLevel; + var color = GUI.contentColor; + + var isNull = property.FindPropertyRelative(nameof(AvatarObjectReference.isNull)); + property = property.FindPropertyRelative(nameof(AvatarObjectReference.referencePath)); + + try + { + // Find containing object, and from that the avatar + if (property.serializedObject == null || property.serializedObject.targetObjects.Length != 1) + return false; + + var obj = property.serializedObject.targetObject as Component; + if (obj == null) return false; + + var transform = obj.transform; + var avatar = RuntimeUtil.FindAvatarInParents(transform); + if (avatar == null) return false; + + var target = isNull.boolValue ? null : avatar.transform.Find(property.stringValue); + + var labelRect = position; + position = EditorGUI.PrefixLabel(position, label); + labelRect.width = position.x - labelRect.x; + + var nullContent = GUIContent.none; + + if (target != null || isNull.boolValue) + { + EditorGUI.BeginChangeCheck(); + var newTarget = EditorGUI.ObjectField(position, nullContent, target, typeof(Transform), true); + if (EditorGUI.EndChangeCheck()) + { + if (newTarget == null) + { + property.stringValue = ""; + isNull.boolValue = true; + } + else + { + var relPath = + RuntimeUtil.RelativePath(avatar.gameObject, ((Transform) newTarget).gameObject); + if (relPath == null) return true; + + property.stringValue = relPath; + isNull.boolValue = false; + } + } + } + else + { + // For some reason, this color change retroactively affects the prefix label above, so draw our own + // label as well (we still want the prefix label for highlights, etc). + EditorGUI.LabelField(labelRect, label); + + GUI.contentColor = new Color(0, 0, 0, 0); + EditorGUI.BeginChangeCheck(); + var newTarget = EditorGUI.ObjectField(position, nullContent, target, typeof(Transform), true); + GUI.contentColor = color; + + if (EditorGUI.EndChangeCheck()) + { + if (newTarget == null) + { + property.stringValue = ""; + isNull.boolValue = true; + } + else + { + var relPath = + RuntimeUtil.RelativePath(avatar.gameObject, ((Transform) newTarget).gameObject); + if (relPath == null) return true; + + property.stringValue = relPath; + isNull.boolValue = false; + } + } + else + { + GUI.contentColor = Color.red; + EditorGUI.LabelField(position, property.stringValue); + } + } + + return true; + } + finally + { + GUI.contentColor = color; + EditorGUI.indentLevel = indentLevel; + } + } + } +} \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Editor/Inspector/AvatarObjectReferenceDrawer.cs.meta b/Packages/net.fushizen.modular-avatar/Editor/Inspector/AvatarObjectReferenceDrawer.cs.meta new file mode 100644 index 00000000..0a30ac6d --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Editor/Inspector/AvatarObjectReferenceDrawer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2a52bc46649d4cf6863b147a830124c5 +timeCreated: 1664754621 \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Editor/Util.cs b/Packages/net.fushizen.modular-avatar/Editor/Util.cs index a021a3f8..1ea605c1 100644 --- a/Packages/net.fushizen.modular-avatar/Editor/Util.cs +++ b/Packages/net.fushizen.modular-avatar/Editor/Util.cs @@ -51,6 +51,8 @@ namespace net.fushizen.modular_avatar.core.editor static Util() { RuntimeUtil.delayCall = (cb) => EditorApplication.delayCall += cb.Invoke; + + EditorApplication.hierarchyChanged += () => { RuntimeUtil.InvokeHierarchyChanged(); }; } public static AnimatorController CreateAnimator(AnimatorController toClone = null) diff --git a/Packages/net.fushizen.modular-avatar/Runtime/AvatarObjectReference.cs b/Packages/net.fushizen.modular-avatar/Runtime/AvatarObjectReference.cs new file mode 100644 index 00000000..08beb0b4 --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Runtime/AvatarObjectReference.cs @@ -0,0 +1,44 @@ +using System; +using UnityEngine; + +namespace net.fushizen.modular_avatar.core +{ + [Serializable] + public class AvatarObjectReference + { + public bool isNull = true; + public string referencePath = ""; + + private bool _cacheValid = false; + private string _cachedPath = ""; + private GameObject _cachedReference; + + public GameObject Get(Component container) + { + if (_cacheValid && _cachedPath == referencePath && !isNull) return _cachedReference; + + _cacheValid = true; + _cachedPath = referencePath; + + if (isNull) + { + _cachedReference = null; + return _cachedReference; + } + + RuntimeUtil.OnHierarchyChanged -= InvalidateCache; + RuntimeUtil.OnHierarchyChanged += InvalidateCache; + + var avatar = RuntimeUtil.FindAvatarInParents(container.transform); + if (avatar == null) return (_cachedReference = null); + + return (_cachedReference = avatar.transform.Find(referencePath)?.gameObject); + } + + private void InvalidateCache() + { + RuntimeUtil.OnHierarchyChanged -= InvalidateCache; + _cacheValid = false; + } + } +} \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Runtime/AvatarObjectReference.cs.meta b/Packages/net.fushizen.modular-avatar/Runtime/AvatarObjectReference.cs.meta new file mode 100644 index 00000000..0a7f270f --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Runtime/AvatarObjectReference.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5a8276620b424854bdad151a08ebe5d0 +timeCreated: 1664754280 \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Runtime/RuntimeUtil.cs b/Packages/net.fushizen.modular-avatar/Runtime/RuntimeUtil.cs index ea21ff6e..a9f76878 100644 --- a/Packages/net.fushizen.modular-avatar/Runtime/RuntimeUtil.cs +++ b/Packages/net.fushizen.modular-avatar/Runtime/RuntimeUtil.cs @@ -24,7 +24,6 @@ using System; using System.Collections.Generic; -using System.Reflection; using JetBrains.Annotations; using UnityEngine; using VRC.SDK3.Avatars.Components; @@ -37,6 +36,7 @@ namespace net.fushizen.modular_avatar.core // Initialized in Util public static Action delayCall = (_) => { }; + public static event NullCallback OnHierarchyChanged; public enum OnDemandSource { @@ -52,16 +52,16 @@ namespace net.fushizen.modular_avatar.core public static string RelativePath(GameObject root, GameObject child) { if (root == child) return ""; - + List pathSegments = new List(); while (child != root && child != null) { pathSegments.Add(child.name); - child = child.transform.parent.gameObject; + child = child.transform.parent?.gameObject; } if (child == null) return null; - + pathSegments.Reverse(); return String.Join("/", pathSegments); } @@ -93,34 +93,38 @@ namespace net.fushizen.modular_avatar.core { UnityEditor.PrefabUtility.RecordPrefabInstancePropertyModifications(obj); } + UnityEditor.EditorUtility.SetDirty(obj); - #endif +#endif } #if UNITY_EDITOR private static UnityEngine.Object cachedAnimationWindowState; - private static readonly Type animationWindowStateType + + private static readonly Type animationWindowStateType = typeof(UnityEditor.Editor).Assembly.GetType("UnityEditorInternal.AnimationWindowState"); + private static readonly PropertyInfo recordingProp = animationWindowStateType.GetProperty( "recording", BindingFlags.Instance | BindingFlags.Public ); + private static readonly PropertyInfo previewingProp = animationWindowStateType.GetProperty( "previewing", BindingFlags.Instance | BindingFlags.Public ); + private static readonly PropertyInfo playingProp = animationWindowStateType.GetProperty( "playing", BindingFlags.Instance | BindingFlags.Public ); #endif - + public static bool IsAnimationEditMode() { #if !UNITY_EDITOR return false; #else - if (cachedAnimationWindowState == null) { foreach (var obj in Resources.FindObjectsOfTypeAll(animationWindowStateType)) @@ -132,15 +136,19 @@ namespace net.fushizen.modular_avatar.core if (cachedAnimationWindowState == null) return false; return (bool) recordingProp.GetValue(cachedAnimationWindowState, null) - || (bool) previewingProp.GetValue(cachedAnimationWindowState, null) - || (bool) playingProp.GetValue(cachedAnimationWindowState, null); + || (bool) previewingProp.GetValue(cachedAnimationWindowState, null) + || (bool) playingProp.GetValue(cachedAnimationWindowState, null); #endif } - + #if UNITY_EDITOR public static bool isPlaying => UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode; #else public static bool isPlaying => true; #endif + public static void InvokeHierarchyChanged() + { + OnHierarchyChanged?.Invoke(); + } } } \ No newline at end of file