// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2023 Kybernetik // #if UNITY_EDITOR using UnityEditor; using UnityEngine; using static Animancer.Editor.AnimancerPlayableDrawer; using Object = UnityEngine.Object; namespace Animancer.Editor { /// [Editor-Only] Draws the Inspector GUI for an . /// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerStateDrawer_1 /// public class AnimancerStateDrawer : AnimancerNodeDrawer where T : AnimancerState { /************************************************************************************************************************/ /// /// Creates a new to manage the Inspector GUI for the `target`. /// public AnimancerStateDrawer(T target) => Target = target; /************************************************************************************************************************/ /// The used for the area encompassing this drawer is null. protected override GUIStyle RegionStyle => null; /************************************************************************************************************************/ /// Determines whether the field can occupy the whole line. private bool IsAssetUsedAsKey => string.IsNullOrEmpty(Target.DebugName) && (Target.Key == null || ReferenceEquals(Target.Key, Target.MainObject)); /************************************************************************************************************************/ /// protected override bool AutoNormalizeSiblingWeights => AutoNormalizeWeights; /************************************************************************************************************************/ /// /// Draws the state's main label: an field if it has a /// , otherwise just a simple text label. /// /// Also shows a bar to indicate its progress. /// protected override void DoLabelGUI(Rect area) { string label; if (!string.IsNullOrEmpty(Target.DebugName)) { label = Target.DebugName; } else if (IsAssetUsedAsKey) { label = ""; } else { var key = Target.Key; if (key is string str) label = $"\"{str}\""; else label = key.ToString(); } HandleLabelClick(area); AnimancerGUI.DoWeightLabel(ref area, Target.Weight); AnimationBindings.DoBindingMatchGUI(ref area, Target); var mainObject = Target.MainObject; if (!(mainObject is null)) { EditorGUI.BeginChangeCheck(); mainObject = EditorGUI.ObjectField(area, label, mainObject, typeof(Object), false); if (EditorGUI.EndChangeCheck()) Target.MainObject = mainObject; } else if (!string.IsNullOrEmpty(Target.DebugName)) { EditorGUI.LabelField(area, Target.DebugName); } else { EditorGUI.LabelField(area, label, Target.ToString()); } // Highlight a section of the label based on the time like a loading bar. area.width -= 18;// Remove the area for the Object Picker icon to line the bar up with the field. DoTimeHighlightBarGUI(area, Target.IsPlaying, Target.EffectiveWeight, Target.Time, Target.Length, Target.IsLooping); } /************************************************************************************************************************/ /// Draws a progress bar to show the animation time. public static void DoTimeHighlightBarGUI(Rect area, bool isPlaying, float weight, float time, float length, bool isLooping) { var color = GUI.color; if (ScaleTimeBarByWeight) { var height = area.height; area.height = 1 + (area.height - 1) * Mathf.Clamp01(weight); area.y += height - area.height; } // Green = Playing, Yelow = Paused. GUI.color = isPlaying ? new Color(0.15f, 0.7f, 0.15f, 0.35f) : new Color(0.7f, 0.7f, 0.15f, 0.35f); area = EditorGUI.IndentedRect(area); var wrappedTime = GetWrappedTime(time, length, isLooping); if (length > 0) area.width *= Mathf.Clamp01(wrappedTime / length); GUI.DrawTexture(area, Texture2D.whiteTexture); GUI.color = color; } /************************************************************************************************************************/ /// Handles Ctrl + Click on the label to CrossFade the animation. private void HandleLabelClick(Rect area) { var currentEvent = Event.current; if (currentEvent.type != EventType.MouseUp || !currentEvent.control || !area.Contains(currentEvent.mousePosition)) return; currentEvent.Use(); Target.Root.UnpauseGraph(); var fadeDuration = Target.CalculateEditorFadeDuration(AnimancerPlayable.DefaultFadeDuration); Target.Root.Play(Target, fadeDuration); } /************************************************************************************************************************/ /// protected override void DoFoldoutGUI(Rect area) { float foldoutWidth; if (IsAssetUsedAsKey) { foldoutWidth = EditorGUI.indentLevel * AnimancerGUI.IndentSize; } else { foldoutWidth = EditorGUIUtility.labelWidth; } area.xMin -= 2; area.width = foldoutWidth; var hierarchyMode = EditorGUIUtility.hierarchyMode; EditorGUIUtility.hierarchyMode = true; IsExpanded = EditorGUI.Foldout(area, IsExpanded, GUIContent.none, true); EditorGUIUtility.hierarchyMode = hierarchyMode; } /************************************************************************************************************************/ /// /// Gets the current . /// If the state is looping, the value is modulo by the . /// private float GetWrappedTime(out float length) => GetWrappedTime(Target.Time, length = Target.Length, Target.IsLooping); /// /// Gets the current . /// If the state is looping, the value is modulo by the . /// private static float GetWrappedTime(float time, float length, bool isLooping) { var wrappedTime = time; if (isLooping) { wrappedTime = AnimancerUtilities.Wrap(wrappedTime, length); if (wrappedTime == 0 && time != 0) wrappedTime = length; } return wrappedTime; } /************************************************************************************************************************/ /// protected override void DoDetailsGUI() { if (!IsExpanded) return; EditorGUI.indentLevel++; DoTimeSliderGUI(); DoNodeDetailsGUI(); DoOnEndGUI(); EditorGUI.indentLevel--; } /************************************************************************************************************************/ /// Draws a slider for controlling the current . private void DoTimeSliderGUI() { if (Target.Length <= 0) return; var time = GetWrappedTime(out var length); if (length == 0) return; var area = AnimancerGUI.LayoutSingleLineRect(AnimancerGUI.SpacingMode.Before); var normalized = DoNormalizedTimeToggle(ref area); string label; float max; if (normalized) { label = "Normalized Time"; time /= length; max = 1; } else { label = "Time"; max = length; } DoLoopCounterGUI(ref area, length); EditorGUI.BeginChangeCheck(); label = AnimancerGUI.BeginTightLabel(label); time = EditorGUI.Slider(area, label, time, 0, max); AnimancerGUI.EndTightLabel(); if (AnimancerGUI.TryUseClickEvent(area, 2)) time = 0; if (EditorGUI.EndChangeCheck()) { if (normalized) Target.NormalizedTime = time; else Target.Time = time; } } /************************************************************************************************************************/ private bool DoNormalizedTimeToggle(ref Rect area) { using (ObjectPool.Disposable.AcquireContent(out var label, "N")) { var style = AnimancerGUI.MiniButton; var width = style.CalculateWidth(label); var toggleArea = AnimancerGUI.StealFromRight(ref area, width); UseNormalizedTimeSliders.Value = GUI.Toggle(toggleArea, UseNormalizedTimeSliders, label, style); } return UseNormalizedTimeSliders; } /************************************************************************************************************************/ private static ConversionCache _LoopCounterCache; private void DoLoopCounterGUI(ref Rect area, float length) { if (_LoopCounterCache == null) _LoopCounterCache = new ConversionCache((x) => "x" + x); string label; var normalizedTime = Target.Time / length; if (float.IsNaN(normalizedTime)) { label = "NaN"; } else { var loops = Mathf.FloorToInt(Target.Time / length); label = _LoopCounterCache.Convert(loops); } var width = AnimancerGUI.CalculateLabelWidth(label); var labelArea = AnimancerGUI.StealFromRight(ref area, width); GUI.Label(labelArea, label); } /************************************************************************************************************************/ private void DoOnEndGUI() { if (!Target.HasEvents) return; var events = Target.Events; var drawer = EventSequenceDrawer.Get(events); var area = GUILayoutUtility.GetRect(0, drawer.CalculateHeight(events) + AnimancerGUI.StandardSpacing); area.yMin += AnimancerGUI.StandardSpacing; using (ObjectPool.Disposable.AcquireContent(out var label, "Events")) drawer.Draw(ref area, events, label); } /************************************************************************************************************************/ #region Context Menu /************************************************************************************************************************/ /// protected override void PopulateContextMenu(GenericMenu menu) { AddContextMenuFunctions(menu); menu.AddFunction("Play", !Target.IsPlaying || Target.Weight != 1, () => { Target.Root.UnpauseGraph(); Target.Root.Play(Target); }); AnimancerEditorUtilities.AddFadeFunction(menu, "Cross Fade (Ctrl + Click)", Target.Weight != 1, Target, (duration) => { Target.Root.UnpauseGraph(); Target.Root.Play(Target, duration); }); menu.AddSeparator(""); menu.AddItem(new GUIContent("Destroy State"), false, () => Target.Destroy()); menu.AddSeparator(""); AddDisplayOptions(menu); AnimancerEditorUtilities.AddDocumentationLink(menu, "State Documentation", Strings.DocsURLs.States); } /************************************************************************************************************************/ /// Adds the details of this state to the `menu`. protected virtual void AddContextMenuFunctions(GenericMenu menu) { menu.AddDisabledItem(new GUIContent($"{DetailsPrefix}{nameof(AnimancerState.Key)}: {Target.Key}")); var length = Target.Length; if (!float.IsNaN(length)) menu.AddDisabledItem(new GUIContent($"{DetailsPrefix}{nameof(AnimancerState.Length)}: {length}")); menu.AddDisabledItem(new GUIContent($"{DetailsPrefix}Playable Path: {Target.GetPath()}")); var mainAsset = Target.MainObject; if (mainAsset != null) { var assetPath = AssetDatabase.GetAssetPath(mainAsset); if (assetPath != null) menu.AddDisabledItem(new GUIContent($"{DetailsPrefix}Asset Path: {assetPath.Replace("/", "->")}")); } if (Target.HasEvents) { var events = Target.Events; for (int i = 0; i < events.Count; i++) { var index = i; AddEventFunctions(menu, "Event " + index, events[index], () => events.SetCallback(index, AnimancerEvent.DummyCallback), () => events.Remove(index)); } AddEventFunctions(menu, "End Event", events.EndEvent, () => events.EndEvent = new AnimancerEvent(float.NaN, null), null); } } /************************************************************************************************************************/ private void AddEventFunctions(GenericMenu menu, string name, AnimancerEvent animancerEvent, GenericMenu.MenuFunction clearEvent, GenericMenu.MenuFunction removeEvent) { name = $"Events/{name}/"; menu.AddDisabledItem(new GUIContent($"{name}{nameof(AnimancerState.NormalizedTime)}: {animancerEvent.normalizedTime}")); bool canInvoke; if (animancerEvent.callback == null) { menu.AddDisabledItem(new GUIContent(name + "Callback: null")); canInvoke = false; } else if (animancerEvent.callback == AnimancerEvent.DummyCallback) { menu.AddDisabledItem(new GUIContent(name + "Callback: Dummy")); canInvoke = false; } else { var label = name + (animancerEvent.callback.Target != null ? ("Target: " + animancerEvent.callback.Target) : "Target: null"); var targetObject = animancerEvent.callback.Target as Object; menu.AddFunction(label, targetObject != null, () => Selection.activeObject = targetObject); menu.AddDisabledItem(new GUIContent( $"{name}Declaring Type: {animancerEvent.callback.Method.DeclaringType.GetNameCS()}")); menu.AddDisabledItem(new GUIContent( $"{name}Method: {animancerEvent.callback.Method}")); canInvoke = true; } if (clearEvent != null) menu.AddFunction(name + "Clear", canInvoke || !float.IsNaN(animancerEvent.normalizedTime), clearEvent); if (removeEvent != null) menu.AddFunction(name + "Remove", true, removeEvent); menu.AddFunction(name + "Invoke", canInvoke, () => animancerEvent.Invoke(Target)); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } } #endif