/************************************************************************************************************************/ /// <summary>Draws the details and controls for the target <see cref="State"/> in the inspector.</summary> public virtual void DoGUI(IAnimancerComponent owner) { GUILayout.BeginVertical(); { var position = AnimancerEditorUtilities.GetRect(true); string label; DoFoldoutGUI(position, out label); DoLabelGUI(ref position, label); if (_IsExpanded) { DoDetailsGUI(owner); } } GUILayout.EndVertical(); CheckContextMenu(GUILayoutUtility.GetLastRect()); }
private static void LogDescriptionOfStates(MenuCommand command) { var animancer = command.context as AnimancerComponent; var message = new StringBuilder(); message.Append(animancer.ToString()); if (animancer.IsPlayableInitialised) { message.Append(":\n"); animancer.Playable.AppendDescription(message); } else { message.Append(": Playable is not initialised."); } AnimancerEditorUtilities.AppendNonCriticalIssues(message); Debug.Log(message, animancer); }
/// <summary>Returns a cached <see cref="BindingData"/> representing the specified `gameObject`.</summary> /// <remarks>Note that the cache is cleared by <see cref="EditorApplication.hierarchyChanged"/>.</remarks> public static BindingData GetBindings(GameObject gameObject, bool forceGather = true) { if (AnimancerEditorUtilities.InitialiseCleanDictionary(ref _ObjectToBindings)) { EditorApplication.hierarchyChanged += _ObjectToBindings.Clear; } if (!_ObjectToBindings.TryGetValue(gameObject, out var bindings)) { if (!forceGather && !CanGatherBindings()) { return(null); } bindings = new BindingData(gameObject); _ObjectToBindings.Add(gameObject, bindings); } return(bindings); }
/************************************************************************************************************************/ /// <summary>Refreshes the <see cref="Names"/>.</summary> private void UpdateNames() { if (!_NamesAreDirty) { return; } _NamesAreDirty = false; var sprites = Sprites; AnimancerEditorUtilities.SetCount(Names, sprites.Count); if (string.IsNullOrEmpty(_NewName)) { for (int i = 0; i < sprites.Count; i++) { Names[i] = sprites[i].name; } } else { var digits = Mathf.FloorToInt(Mathf.Log10(Names.Count)) + 1; if (digits < _MinimumDigits) { digits = _MinimumDigits; } var formatCharacters = new char[digits]; for (int i = 0; i < digits; i++) { formatCharacters[i] = '0'; } var format = new string(formatCharacters); for (int i = 0; i < Names.Count; i++) { Names[i] = _NewName + (i + 1).ToString(format); } } }
/// <summary> /// Draws the animator reference field followed by its fields that are relevant to Animancer. /// </summary> public void DoInspectorGUI() { _OnEndGUI = null; DoAnimatorGUI(); GatherAnimatorProperties(); if (_SerializedAnimator == null) { return; } _SerializedAnimator.Update(); AnimancerEditorUtilities.BeginVerticalBox(EditorStyles.helpBox); { if (!_IsAnimatorOnSameObject) { EditorGUILayout.HelpBox("It is recommended that you keep this component on the same GameObject" + " as its target Animator so that they get enabled and disabled at the same time.", MessageType.Info); } DoControllerGUI(); EditorGUILayout.PropertyField(_Avatar, AnimancerEditorUtilities.TempContent("Avatar", "The Avatar used by the Animator")); DoRootMotionGUI(); DoUpdateModeGUI(true); DoCullingModeGUI(); DoStopOnDisableGUI(_KeepStateOnDisable, false); } AnimancerEditorUtilities.EndVerticalBox(EditorStyles.helpBox); _SerializedAnimator.ApplyModifiedProperties(); if (_OnEndGUI != null) { _OnEndGUI(); _OnEndGUI = null; } }
/************************************************************************************************************************/ private void DoAnimatorGUI() { var hasAnimator = AnimatorProperty.objectReferenceValue != null; var color = GUI.color; if (!hasAnimator) { GUI.color = AnimancerGUI.WarningFieldColor; } EditorGUILayout.PropertyField(AnimatorProperty); if (!hasAnimator) { GUI.color = color; EditorGUILayout.HelpBox("An Animator is required in order to play animations." + " Click here to search for one nearby.", MessageType.Warning); if (AnimancerGUI.TryUseClickEventInLastRect()) { Serialization.ForEachTarget(AnimatorProperty, (property) => { var target = (IAnimancerComponent)property.serializedObject.targetObject; var animator = AnimancerEditorUtilities.GetComponentInHierarchy <Animator>(target.gameObject); if (animator == null) { Debug.Log("No Animator found on '" + target + "' or any of its parents or children." + " You must assign one manually.", target.gameObject); return; } property.objectReferenceValue = animator; }); } } }
/************************************************************************************************************************/ /// <summary>[Editor-Only] /// Adds the details of this state to the menu. /// By default, that means a single item showing the path of the <see cref="AnimancerState.MainObject"/>. /// </summary> protected virtual void AddContextMenuFunctions(GenericMenu menu) { if (State.HasLength) { menu.AddDisabledItem(new GUIContent(DetailsPrefix + "Length: " + State.Length)); } menu.AddDisabledItem(new GUIContent(DetailsPrefix + "Playable Path: " + State.GetPath())); var mainAsset = State.MainObject; if (mainAsset != null) { var assetPath = AssetDatabase.GetAssetPath(mainAsset); if (assetPath != null) { menu.AddDisabledItem(new GUIContent(DetailsPrefix + "Asset Path: " + assetPath.Replace("/", "->"))); } } if (State.OnEnd != null) { const string OnEndPrefix = "On End/"; var label = OnEndPrefix + (State.OnEnd.Target != null ? ("Target: " + State.OnEnd.Target) : "Target: null"); var targetObject = State.OnEnd.Target as Object; AnimancerEditorUtilities.AddMenuItem(menu, label, targetObject != null, () => Selection.activeObject = targetObject); menu.AddDisabledItem(new GUIContent(OnEndPrefix + "Declaring Type: " + State.OnEnd.Method.DeclaringType.FullName)); menu.AddDisabledItem(new GUIContent(OnEndPrefix + "Method: " + State.OnEnd.Method)); menu.AddItem(new GUIContent(OnEndPrefix + "Clear"), false, () => State.OnEnd = null); menu.AddItem(new GUIContent(OnEndPrefix + "Invoke"), false, () => State.OnEnd()); } }
/// <summary> /// Checks if the current event is a context menu click within the 'clickArea' and opens a context menu with various /// functions for the <see cref="State"/>. /// </summary> protected void CheckContextMenu(Rect clickArea) { if (!AnimancerEditorUtilities.TryUseContextClick(clickArea)) { return; } var menu = new GenericMenu(); menu.AddDisabledItem(new GUIContent(DetailsPrefix + "State: " + State.ToString())); var key = State.Key; if (key != null) { menu.AddDisabledItem(new GUIContent(DetailsPrefix + "Key: " + key)); } AnimancerEditorUtilities.AddMenuItem(menu, "Play", !State.IsPlaying || State.Weight != 1, () => State.Root.Play(State)); AnimancerEditorUtilities.AddFadeFunction(menu, "Cross Fade (Ctrl + Click)", State.Weight != 1, State, (duration) => State.Root.CrossFade(State, duration)); AddContextMenuFunctions(menu); menu.AddItem(new GUIContent(DetailsPrefix + "Log Details"), false, () => Debug.Log("AnimancerState: " + State.GetDescription(true))); menu.AddSeparator(""); menu.AddItem(new GUIContent("Destroy State"), false, () => State.Dispose()); menu.AddSeparator(""); AnimancerEditorUtilities.AddDocumentationLink(menu, "State Documentation", "/docs/manual/animancer-states"); menu.ShowAsContext(); }
private static void OnControllerChanged() { if (_OnControllerChanged == null) { const string TypeName = "UnityEditorInternal.AnimationWindowUtility"; const string MethodName = "ControllerChanged"; try { var type = typeof(UnityEditorInternal.InternalEditorUtility); type = type.Assembly.GetType(TypeName); var method = type.GetMethod(MethodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); _OnControllerChanged = (Action)Delegate.CreateDelegate(typeof(Action), method); } catch { AnimancerEditorUtilities.RegisterNonCriticalMissingMember(TypeName, MethodName); _OnControllerChanged = () => { }; } } _OnControllerChanged(); }
/************************************************************************************************************************/ /// <summary> /// Draws the <see cref="Animator.keepAnimatorControllerStateOnDisable"/> field. /// </summary> public static void DoStopOnDisableGUI(SerializedProperty keepStateOnDisable, bool updateAndApply) { #if UNITY_2018_1_OR_NEWER var area = AnimancerEditorUtilities.GetRect(); var label = AnimancerEditorUtilities.TempContent("Stop On Disable", "If true, disabling this object will stop and rewind all animations." + " Otherwise they will simply be paused and will resume from their current states when it is re-enabled."); if (keepStateOnDisable != null) { if (updateAndApply) { keepStateOnDisable.serializedObject.Update(); } label = EditorGUI.BeginProperty(area, label, keepStateOnDisable); keepStateOnDisable.boolValue = !EditorGUI.Toggle(area, label, !keepStateOnDisable.boolValue); EditorGUI.EndProperty(); if (updateAndApply) { keepStateOnDisable.serializedObject.ApplyModifiedProperties(); } } else { var enabled = GUI.enabled; GUI.enabled = false; EditorGUI.Toggle(area, label, false); GUI.enabled = enabled; } #endif }
private void DoAnimationsField(SerializedProperty property, GUIContent label) { GUILayout.Space(EditorGUIUtility.standardVerticalSpacing - 1); if (_Animations == null) { _Animations = new ReorderableList(property.serializedObject, property.Copy()) { drawHeaderCallback = DrawAnimationsHeader, drawElementCallback = DrawAnimationElement, elementHeight = EditorGUIUtility.singleLineHeight, onRemoveCallback = RemoveSelectedElement, }; } _RemoveAnimationIndex = -1; GUILayout.BeginVertical(); _Animations.DoLayoutList(); GUILayout.EndVertical(); if (_RemoveAnimationIndex >= 0) { property.DeleteArrayElementAtIndex(_RemoveAnimationIndex); } AnimancerEditorUtilities.HandleDragAndDropAnimations(GUILayoutUtility.GetLastRect(), (clip) => { var index = property.arraySize; property.arraySize = index + 1; var element = property.GetArrayElementAtIndex(index); element.objectReferenceValue = clip; }); GUILayout.Space(EditorGUIUtility.standardVerticalSpacing); }
/************************************************************************************************************************/ private void DoPlayableNotInitializedGUI(IAnimancerComponent target) { if (!EditorApplication.isPlaying || target.Animator == null || EditorUtility.IsPersistent(target.Animator)) { return; } EditorGUILayout.HelpBox("Playable is not initialized." + " It will be initialized automatically when something needs it, such as playing an animation.", MessageType.Info); if (AnimancerGUI.TryUseClickEventInLastRect(1)) { var menu = new GenericMenu(); menu.AddItem(new GUIContent("Initialize"), false, () => target.Playable.Evaluate()); AnimancerEditorUtilities.AddDocumentationLink(menu, "Layer Documentation", Strings.DocsURLs.Layers); menu.ShowAsContext(); } }
/************************************************************************************************************************/ /// <summary> /// Draws the <see cref="Animator.updateMode"/> field with any appropriate warnings. /// </summary> private void DoUpdateModeGUI(bool showWithoutWarning) { if (_UpdateMode == null) { return; } var label = AnimancerGUI.TempContent("Update Mode", "Controls when and how often the animations are updated"); var initialUpdateMode = Targets[0].InitialUpdateMode; var updateMode = (AnimatorUpdateMode)_UpdateMode.intValue; EditorGUI.BeginChangeCheck(); if (!EditorApplication.isPlaying || !AnimancerPlayable.HasChangedToOrFromAnimatePhysics(initialUpdateMode, updateMode)) { if (showWithoutWarning) { EditorGUILayout.PropertyField(_UpdateMode, label); } } else { GUILayout.BeginHorizontal(); var color = GUI.color; GUI.color = AnimancerGUI.WarningFieldColor; EditorGUILayout.PropertyField(_UpdateMode, label); GUI.color = color; label = AnimancerGUI.TempContent("Revert", "Revert to initial mode"); if (GUILayout.Button(label, EditorStyles.miniButton, AnimancerGUI.DontExpandWidth)) { _UpdateMode.intValue = (int)initialUpdateMode.Value; } GUILayout.EndHorizontal(); EditorGUILayout.HelpBox( "Changing to or from AnimatePhysics mode at runtime has no effect when using the" + " Playables API. It will continue using the original mode it had on startup.", MessageType.Warning); if (AnimancerGUI.TryUseClickEventInLastRect()) { EditorUtility.OpenWithDefaultApp(Strings.DocsURLs.UpdateModes); } } if (EditorGUI.EndChangeCheck()) { _OnEndGUI += () => { for (int i = 0; i < _Animators.Length; i++) { var animator = _Animators[i]; if (animator != null) { AnimancerEditorUtilities.Invoke(animator, "OnUpdateModeChanged"); } } }; } }
/************************************************************************************************************************/ #region Context Menu /************************************************************************************************************************/ /// <summary> /// Checks if the current event is a context menu click within the 'clickArea' and opens a context menu with various /// functions for the <see cref="Layer"/>. /// </summary> private void CheckContextMenu(Rect clickArea) { if (!AnimancerEditorUtilities.TryUseContextClick(clickArea)) { return; } var menu = new GenericMenu(); menu.AddDisabledItem(new GUIContent(Layer.ToString())); AnimancerEditorUtilities.AddMenuItem(menu, "Stop", HasAnyStates((state) => state.IsPlaying || state.Weight != 0), () => Layer.Stop()); AnimancerEditorUtilities.AddFadeFunction(menu, "Fade In", Layer.PortIndex > 0 && Layer.Weight != 1, Layer, (duration) => Layer.StartFade(1, duration)); AnimancerEditorUtilities.AddFadeFunction(menu, "Fade Out", Layer.PortIndex > 0 && Layer.Weight != 0, Layer, (duration) => Layer.StartFade(0, duration)); menu.AddItem(new GUIContent("Inverse Kinematics/Apply Animator IK"), Layer.ApplyAnimatorIK, () => Layer.ApplyAnimatorIK = !Layer.ApplyAnimatorIK); menu.AddItem(new GUIContent("Inverse Kinematics/Default Apply Animator IK"), Layer.DefaultApplyAnimatorIK, () => Layer.DefaultApplyAnimatorIK = !Layer.DefaultApplyAnimatorIK); menu.AddItem(new GUIContent("Inverse Kinematics/Apply Foot IK"), Layer.ApplyFootIK, () => Layer.ApplyFootIK = !Layer.ApplyFootIK); menu.AddItem(new GUIContent("Inverse Kinematics/Default Apply Foot IK"), Layer.DefaultApplyFootIK, () => Layer.DefaultApplyFootIK = !Layer.DefaultApplyFootIK); menu.AddSeparator(""); AnimancerEditorUtilities.AddMenuItem(menu, "Destroy States", ActiveStates.Count > 0 || InactiveStates.Count > 0, () => Layer.DestroyStates()); AnimancerEditorUtilities.AddMenuItem(menu, "Add Layer", Layer.Root.LayerCount < AnimancerPlayable.maxLayerCount, () => Layer.Root.LayerCount++); AnimancerEditorUtilities.AddMenuItem(menu, "Remove Layer", Layer.Root.LayerCount > 0, () => Layer.Root.LayerCount--); menu.AddSeparator(""); menu.AddItem(new GUIContent("Keep Weightless Playables Connected"), Layer.Root.KeepPlayablesConnected, () => Layer.Root.KeepPlayablesConnected = !Layer.Root.KeepPlayablesConnected); AddPrefFunctions(menu); menu.AddSeparator(""); AnimancerEditorUtilities.AddDocumentationLink(menu, "Layer Documentation", "/docs/manual/animation-layers"); AddPlayableGraphVisualizerFunction(menu); menu.ShowAsContext(); }
/// <summary> /// Determines the <see cref="MatchType"/> representing the properties animated by the `clip` in /// comparison to the properties that actually exist on the target <see cref="GameObject"/> and its /// children. /// <para></para> /// Also compiles a `message` explaining the differences if that paraneter is not null. /// </summary> public MatchType GetMatchType(AnimationClip clip, StringBuilder message, Dictionary <EditorCurveBinding, bool> bindingsInMessage, ref int existingBindings) { AnimancerEditorUtilities.InitialiseCleanDictionary(ref _BindingMatches); if (_BindingMatches.TryGetValue(clip, out var match) && bindingsInMessage == null) { return(match); } var objectType = ObjectType; var clipType = GetAnimationType(clip); if (clipType != objectType) { if (message != null) { message.AppendLine() .Append($"{LinePrefix}The {nameof(AnimationType)} of the '") .Append(clip.name) .Append("' animation is ") .Append(clipType) .Append(" while the '") .Append(GameObject.name) .Append("' Rig is ") .Append(objectType) .Append("."); } switch (clipType) { default: case AnimationType.None: case AnimationType.Humanoid: match = MatchType.Error; if (message == null) { goto SetMatch; } else { break; } case AnimationType.Generic: case AnimationType.Sprite: match = MatchType.Warning; break; } } var bindingMatch = GetMatchType(GetBindings(clip), bindingsInMessage, ref existingBindings); if (match < bindingMatch) { match = bindingMatch; } SetMatch: _BindingMatches[clip] = match; return(match); }
/************************************************************************************************************************/ private void DoAnimatorGUI(SerializedProperty property, GUIContent label) { var hasAnimator = property.objectReferenceValue != null; var color = GUI.color; if (!hasAnimator) { GUI.color = AnimancerGUI.WarningFieldColor; } EditorGUILayout.PropertyField(property, label); if (!hasAnimator) { GUI.color = color; EditorGUILayout.HelpBox($"An {nameof(Animator)} is required in order to play animations." + " Click here to search for one nearby.", MessageType.Warning); if (AnimancerGUI.TryUseClickEventInLastRect()) { Serialization.ForEachTarget(property, (targetProperty) => { var target = (IAnimancerComponent)targetProperty.serializedObject.targetObject; var animator = AnimancerEditorUtilities.GetComponentInHierarchy <Animator>(target.gameObject); if (animator == null) { Debug.Log($"No {nameof(Animator)} found on '{target.gameObject.name}' or any of its parents or children." + " You must assign one manually.", target.gameObject); return; } targetProperty.objectReferenceValue = animator; }); } } else if (property.objectReferenceValue is Animator animator) { if (animator.gameObject != Targets[0].gameObject) { EditorGUILayout.HelpBox( $"It is recommended that you keep this component on the same {nameof(GameObject)}" + $" as its target {nameof(Animator)} so that they get enabled and disabled at the same time.", MessageType.Info); } var initialUpdateMode = Targets[0].InitialUpdateMode; var updateMode = animator.updateMode; if (AnimancerPlayable.HasChangedToOrFromAnimatePhysics(initialUpdateMode, updateMode)) { EditorGUILayout.HelpBox( $"Changing to or from {nameof(AnimatorUpdateMode.AnimatePhysics)} mode at runtime has no effect" + $" when using the Playables API. It will continue using the original mode it had on startup.", MessageType.Warning); if (AnimancerGUI.TryUseClickEventInLastRect()) { EditorUtility.OpenWithDefaultApp(Strings.DocsURLs.UpdateModes); } } } }
/************************************************************************************************************************/ /// <summary> /// Draws the <see cref="Animator.runtimeAnimatorController"/> field with a warning if a controller is /// assigned. /// </summary> private void DoControllerGUI() { if (_Controller == null) { return; } var controller = _Animators[0].runtimeAnimatorController; var showMixedValue = EditorGUI.showMixedValue; for (int i = 1; i < _Animators.Length; i++) { if (_Animators[i].runtimeAnimatorController != controller) { EditorGUI.showMixedValue = true; break; } } if (controller == null && !EditorGUI.showMixedValue) { return; } var label = AnimancerEditorUtilities.TempContent("Controller"); EditorGUI.BeginChangeCheck(); var area = EditorGUILayout.BeginHorizontal(); label = EditorGUI.BeginProperty(area, label, _Controller); var color = GUI.color; GUI.color = AnimancerEditorUtilities.WarningFieldColor; controller = (RuntimeAnimatorController)EditorGUILayout.ObjectField(label, controller, typeof(RuntimeAnimatorController), false); GUI.color = color; if (GUILayout.Button("Remove", EditorStyles.miniButton, AnimancerEditorUtilities.DontExpandWidth)) { controller = null; } GUILayout.EndHorizontal(); if (EditorGUI.EndChangeCheck()) { Undo.RecordObjects(_Animators, "Changed AnimatorController"); for (int i = 0; i < _Animators.Length; i++) { _Animators[i].runtimeAnimatorController = controller; } OnControllerChanged(); } EditorGUI.showMixedValue = showMixedValue; EditorGUILayout.HelpBox( "The AnimatorController will not affect the model while Animancer is active," + " however Unity will still execute it's state machine in the background," + " which may be a waste of processing time if you aren't using it intentionally." + " Click here for more information.", MessageType.Warning); if (AnimancerEditorUtilities.TryUseClickInLastRect()) { EditorUtility.OpenWithDefaultApp(AnimancerPlayable.APIDocumentationURL + "/docs/manual/animator-controller-states"); } }
/// <summary> /// Determines the <see cref="MatchType"/> representing the properties animated by the `clip` in /// comparison to the properties that actually exist on the target <see cref="GameObject"/> and its /// children. /// <para></para> /// Also compiles a `message` explaining the differences if that paraneter is not null. /// </summary> public MatchType GetMatchType(AnimationClip clip, StringBuilder message, Dictionary <EditorCurveBinding, bool> bindingsInMessage, ref int existingBindings, bool forceGather = true) { AnimancerEditorUtilities.InitialiseCleanDictionary(ref _BindingMatches); if (_BindingMatches.TryGetValue(clip, out var match)) { if (bindingsInMessage == null) { return(match); } } else if (!forceGather && !CanGatherBindings()) { return(MatchType.Unknown); } var objectType = ObjectType; var clipType = GetAnimationType(clip); if (clipType != objectType) { if (message != null) { message.AppendLine() .Append($"{LinePrefix}This message does not necessarily mean anything is wrong," + $" but if something is wrong then this might help you identify the problem."); message.AppendLine() .Append($"{LinePrefix}The {nameof(AnimationType)} of the '") .Append(clip.name) .Append("' animation is ") .Append(clipType) .Append(" while the '") .Append(GameObject.name) .Append("' Rig is ") .Append(objectType) .Append(". See the documentation for more information about Animation Types:" + $" {Strings.DocsURLs.Inspector}#animation-types"); } switch (clipType) { default: case AnimationType.None: case AnimationType.Humanoid: match = MatchType.Error; if (message == null) { goto SetMatch; } else { break; } case AnimationType.Generic: case AnimationType.Sprite: match = MatchType.Warning; break; } } var bindingMatch = GetMatchType(GetBindings(clip), bindingsInMessage, ref existingBindings); if (match < bindingMatch) { match = bindingMatch; } SetMatch: _BindingMatches[clip] = match; return(match); }
/************************************************************************************************************************/ /// <summary> Draws the details of the target state in the GUI.</summary> protected override void DoDetailsGUI(IAnimancerComponent owner) { base.DoDetailsGUI(owner); var animator = owner.Animator; if (animator == null) { return; } var count = ParameterCount; if (count <= 0) { return; } var labelWidth = EditorGUIUtility.labelWidth; EditorGUIUtility.labelWidth -= AnimancerEditorUtilities.IndentSize; var area = AnimancerEditorUtilities.GetRect(true); area = EditorGUI.IndentedRect(area); EditorGUI.LabelField(area, "Parameters", count.ToString()); for (int i = 0; i < count; i++) { var type = GetParameterType(i); if (type == 0) { continue; } var name = GetParameterName(i); var value = GetParameterValue(i); EditorGUI.BeginChangeCheck(); area = AnimancerEditorUtilities.GetRect(true); area = EditorGUI.IndentedRect(area); switch (type) { case AnimatorControllerParameterType.Float: value = EditorGUI.FloatField(area, name, (float)value); break; case AnimatorControllerParameterType.Int: value = EditorGUI.IntField(area, name, (int)value); break; case AnimatorControllerParameterType.Bool: value = EditorGUI.Toggle(area, name, (bool)value); break; case AnimatorControllerParameterType.Trigger: value = EditorGUI.Toggle(area, name, (bool)value, EditorStyles.radioButton); break; default: EditorGUI.LabelField(area, name, "Unhandled Type: " + type); break; } if (EditorGUI.EndChangeCheck()) { SetParameterValue(i, value); } } EditorGUIUtility.labelWidth = labelWidth; }