// Animancer // Copyright 2020 Kybernetik // #if UNITY_EDITOR using System; using System.Collections.Generic; using System.Reflection; using System.Text; using UnityEditor; using UnityEngine; using UnityEngine.Animations; using Object = UnityEngine.Object; namespace Animancer.Editor { /// [Editor-Only] Various utilities used throughout Animancer. public static partial class AnimancerEditorUtilities { /************************************************************************************************************************/ #region Misc /************************************************************************************************************************/ /// /// Tries to find a component on the `gameObject` or its parents or children (in that /// order). /// public static T GetComponentInHierarchy(GameObject gameObject) where T : class { var component = gameObject.GetComponentInParent(); if (component != null) return component; return gameObject.GetComponentInChildren(); } /************************************************************************************************************************/ /// Assets cannot reference scene objects. public static bool ShouldAllowReference(Object obj, Object reference) { return obj == null || reference == null || !EditorUtility.IsPersistent(obj) || EditorUtility.IsPersistent(reference); } /************************************************************************************************************************/ /// Wraps . public static bool GetIsInspectorExpanded(Object obj) { return UnityEditorInternal.InternalEditorUtility.GetIsInspectorExpanded(obj); } /// Wraps . public static void SetIsInspectorExpanded(Object obj, bool isExpanded) { UnityEditorInternal.InternalEditorUtility.SetIsInspectorExpanded(obj, isExpanded); } /// Calls on all `objects`. public static void SetIsInspectorExpanded(Object[] objects, bool isExpanded) { for (int i = 0; i < objects.Length; i++) SetIsInspectorExpanded(objects[i], isExpanded); } /************************************************************************************************************************/ private static Dictionary> _TypeToMethodNameToMethod; /// /// Tries to find a method with the specified name on the `target` object and invoke it. /// public static object Invoke(object target, string methodName) { return Invoke(target.GetType(), target, methodName); } /// /// Tries to find a method with the specified name on the `target` object and invoke it. /// public static object Invoke(Type type, object target, string methodName) { if (_TypeToMethodNameToMethod == null) _TypeToMethodNameToMethod = new Dictionary>(); Dictionary nameToMethod; if (!_TypeToMethodNameToMethod.TryGetValue(type, out nameToMethod)) { nameToMethod = new Dictionary(); _TypeToMethodNameToMethod.Add(type, nameToMethod); } MethodInfo method; if (!nameToMethod.TryGetValue(methodName, out method)) { method = type.GetMethod(methodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); nameToMethod.Add(methodName, method); if (method == null) RegisterNonCriticalMissingMember(type.FullName, methodName); } if (method != null) return method.Invoke(target, null); return null; } /************************************************************************************************************************/ private static List> _NonCriticalIssues; /// /// Registers a delegate that can construct a description of an issue at a later time so that it doesn't waste /// the user's time on unimportant issues. /// public static void RegisterNonCriticalIssue(Action describeIssue) { if (_NonCriticalIssues == null) _NonCriticalIssues = new List>(); _NonCriticalIssues.Add(describeIssue); } /// /// Calls with an issue indicating that a particular type was not /// found by reflection. /// public static void RegisterNonCriticalMissingType(string type) { RegisterNonCriticalIssue((text) => text .Append("[Reflection] Unable to find type '") .Append(type) .Append("'")); } /// /// Calls with an issue indicating that a particular member was not /// found by reflection. /// public static void RegisterNonCriticalMissingMember(string type, string name) { RegisterNonCriticalIssue((text) => text .Append("[Reflection] Unable to find member '") .Append(name) .Append("' in type '") .Append(type) .Append("'")); } /// /// Appends all issues given to to the `text`. /// public static void AppendNonCriticalIssues(StringBuilder text) { if (_NonCriticalIssues == null) return; text.Append("\n\nThe following non-critical issues have also been found" + " (in Animancer generally, not specifically this object):\n\n"); for (int i = 0; i < _NonCriticalIssues.Count; i++) { text.Append(" - "); _NonCriticalIssues[i](text); text.Append("\n\n"); } } /************************************************************************************************************************/ /// Gets the value of the `parameter` in the `animator`. public static object GetParameterValue(Animator animator, AnimatorControllerParameter parameter) { switch (parameter.type) { case AnimatorControllerParameterType.Float: return animator.GetFloat(parameter.nameHash); case AnimatorControllerParameterType.Int: return animator.GetInteger(parameter.nameHash); case AnimatorControllerParameterType.Bool: case AnimatorControllerParameterType.Trigger: return animator.GetBool(parameter.nameHash); default: throw new ArgumentException("Unhandled AnimatorControllerParameterType: " + parameter.type); } } /// Gets the value of the `parameter` in the `playable`. public static object GetParameterValue(AnimatorControllerPlayable playable, AnimatorControllerParameter parameter) { switch (parameter.type) { case AnimatorControllerParameterType.Float: return playable.GetFloat(parameter.nameHash); case AnimatorControllerParameterType.Int: return playable.GetInteger(parameter.nameHash); case AnimatorControllerParameterType.Bool: case AnimatorControllerParameterType.Trigger: return playable.GetBool(parameter.nameHash); default: throw new ArgumentException("Unhandled AnimatorControllerParameterType: " + parameter.type); } } /************************************************************************************************************************/ /// Sets the value of the `parameter` in the `animator`. public static void SetParameterValue(Animator animator, AnimatorControllerParameter parameter, object value) { switch (parameter.type) { case AnimatorControllerParameterType.Float: animator.SetFloat(parameter.nameHash, (float)value); break; case AnimatorControllerParameterType.Int: animator.SetInteger(parameter.nameHash, (int)value); break; case AnimatorControllerParameterType.Bool: animator.SetBool(parameter.nameHash, (bool)value); break; case AnimatorControllerParameterType.Trigger: if ((bool)value) animator.SetTrigger(parameter.nameHash); else animator.ResetTrigger(parameter.nameHash); break; default: throw new ArgumentException("Unhandled AnimatorControllerParameterType: " + parameter.type); } } /// Sets the value of the `parameter` in the `playable`. public static void SetParameterValue(AnimatorControllerPlayable playable, AnimatorControllerParameter parameter, object value) { switch (parameter.type) { case AnimatorControllerParameterType.Float: playable.SetFloat(parameter.nameHash, (float)value); break; case AnimatorControllerParameterType.Int: playable.SetInteger(parameter.nameHash, (int)value); break; case AnimatorControllerParameterType.Bool: playable.SetBool(parameter.nameHash, (bool)value); break; case AnimatorControllerParameterType.Trigger: if ((bool)value) playable.SetTrigger(parameter.nameHash); else playable.ResetTrigger(parameter.nameHash); break; default: throw new ArgumentException("Unhandled AnimatorControllerParameterType: " + parameter.type); } } /************************************************************************************************************************/ /// /// Returns true if the `node` is not null and . /// /// /// Normally a method can't have the same name as a property, but an extension method can. /// public static bool IsValid(this AnimancerNode node) { return node != null && node.IsValid; } /************************************************************************************************************************/ /// /// Waits one frame to call the `method` as long as Unity is currently in Edit Mode. /// public static void EditModeDelayCall(Action method) { // Would be better to check this before the delayCall, but it only works on the main thread. EditorApplication.delayCall += () => { if (!EditorApplication.isPlayingOrWillChangePlaymode) method(); }; } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Context Menus /************************************************************************************************************************/ /// /// Adds a menu function which is disabled if `isEnabled` is false. /// public static void AddMenuItem(GenericMenu menu, string label, bool isEnabled, GenericMenu.MenuFunction func) { if (!isEnabled) { menu.AddDisabledItem(new GUIContent(label)); return; } menu.AddItem(new GUIContent(label), false, func); } /************************************************************************************************************************/ /// /// Adds a menu function which passes the result of into `startFade`. /// public static void AddFadeFunction(GenericMenu menu, string label, bool isEnabled, AnimancerNode node, Action startFade) { // Fade functions need to be delayed twice since the context menu itself causes the next frame delta // time to be unreasonably high (which would skip the start of the fade). AddMenuItem(menu, label, isEnabled, () => EditorApplication.delayCall += () => EditorApplication.delayCall += () => { startFade(node.CalculateEditorFadeDuration()); }); } /// /// Returns the duration of the `node`s current fade (if any), otherwise returns the `defaultDuration`. /// public static float CalculateEditorFadeDuration(this AnimancerNode node, float defaultDuration = 1) { return node.FadeSpeed > 0 ? 1 / node.FadeSpeed : defaultDuration; } /************************************************************************************************************************/ /// /// Adds a menu function to open a web page. If the `linkSuffix` starts with a '/' then it will be relative to /// the . /// public static void AddDocumentationLink(GenericMenu menu, string label, string linkSuffix) { menu.AddItem(new GUIContent(label), false, () => { if (linkSuffix[0] == '/') linkSuffix = Strings.DocumentationURL + linkSuffix; EditorUtility.OpenWithDefaultApp(linkSuffix); }); } /************************************************************************************************************************/ /// /// Toggles the flag between true and false. /// [MenuItem("CONTEXT/AnimationClip/Toggle Looping")] private static void ToggleLooping(MenuCommand command) { var clip = (AnimationClip)command.context; SetLooping(clip, !clip.isLooping); } /// /// Sets the flag. /// public static void SetLooping(AnimationClip clip, bool looping) { var settings = AnimationUtility.GetAnimationClipSettings(clip); settings.loopTime = looping; AnimationUtility.SetAnimationClipSettings(clip, settings); Debug.Log("Set " + clip.name + " to be " + (looping ? "Looping" : "Not Looping") + ". Note that you need to restart Unity for this change to take effect.", clip); // None of these let us avoid the need to restart Unity. //EditorUtility.SetDirty(clip); //AssetDatabase.SaveAssets(); //var path = AssetDatabase.GetAssetPath(clip); //AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate); } /************************************************************************************************************************/ /// Swaps the flag between true and false. [MenuItem("CONTEXT/AnimationClip/Toggle Legacy")] private static void ToggleLegacy(MenuCommand command) { var clip = (AnimationClip)command.context; clip.legacy = !clip.legacy; } /************************************************************************************************************************/ /// Calls . [MenuItem("CONTEXT/Animator/Restore Bind Pose", priority = 110)] private static void RestoreBindPose(MenuCommand command) { var animator = (Animator)command.context; Undo.RegisterFullObjectHierarchyUndo(animator.gameObject, "Restore bind pose"); var type = Type.GetType("UnityEditor.AvatarSetupTool, UnityEditor"); if (type != null) { var method = type.GetMethod("SampleBindPose", BindingFlags.Static | BindingFlags.Public); if (method != null) method.Invoke(null, new object[] { animator.gameObject }); } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Dummy Animancer Component /************************************************************************************************************************/ /// [Editor-Only] /// An that is not actually a . /// public sealed class DummyAnimancerComponent : IAnimancerComponent { /************************************************************************************************************************/ /// Creates a new . public DummyAnimancerComponent(Animator animator, AnimancerPlayable playable) { Animator = animator; Playable = playable; InitialUpdateMode = animator.updateMode; } /************************************************************************************************************************/ /// [] Returns true. public bool enabled { get { return true; } } /// [] Returns the 's . public GameObject gameObject { get { return Animator.gameObject; } } /// [] The target . public Animator Animator { get; set; } /// [] The target . public AnimancerPlayable Playable { get; private set; } /// [] Returns true. public bool IsPlayableInitialised { get { return true; } } /// [] Returns false. public bool ResetOnDisable { get { return false; } } /// [] Does nothing. public AnimatorUpdateMode UpdateMode { get; set; } /************************************************************************************************************************/ /// [] Returns the `clip`. public object GetKey(AnimationClip clip) { return clip; } /************************************************************************************************************************/ /// [] Returns null. public string AnimatorFieldName { get { return null; } } /// [] Returns null. public AnimatorUpdateMode? InitialUpdateMode { get; private set; } /************************************************************************************************************************/ } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } } #endif