// Animancer // Copyright 2020 Kybernetik // #if UNITY_EDITOR #pragma warning disable IDE0041 // Use 'is null' check. using System; using System.Collections.Generic; using UnityEditor; using UnityEngine; using Object = UnityEngine.Object; namespace Animancer.Editor { /// [Editor-Only] Draws the Inspector GUI for an . public class AnimancerStateDrawer : AnimancerNodeDrawer where T : AnimancerState { /************************************************************************************************************************/ /// /// Constructs a new to manage the Inspector GUI for the `target`. /// public AnimancerStateDrawer(T target) { Target = target; } /************************************************************************************************************************/ /// The used for the area encompassing this drawer. Null. protected override GUIStyle RegionStyle { get { return null; } } /************************************************************************************************************************/ /// /// 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) { var key = Target.Key; var mainObject = Target.MainObject; var isAssetUsedAsKey = key == null || ReferenceEquals(key, mainObject); string label; if (isAssetUsedAsKey) { label = ""; } else { if (key is string) label = "\"" + key + "\""; else label = key.ToString(); } HandleLabelClick(area); AnimancerGUI.DoWeightLabel(ref area, Target.Weight); if (!ReferenceEquals(mainObject, null)) { EditorGUI.BeginChangeCheck(); mainObject = EditorGUI.ObjectField(area, label, mainObject, typeof(Object), false); if (EditorGUI.EndChangeCheck()) Target.MainObject = mainObject; } else { EditorGUI.LabelField(area, label, Target.ToString()); } // Highlight a section of the label based on the time like a loading bar. if (Target.IsPlaying || Target.Time != 0) { var color = GUI.color; // Green = Playing, Yelow = Paused. GUI.color = Target.IsPlaying ? new Color(0.15f, 0.7f, 0.15f, 0.35f) : new Color(0.7f, 0.7f, 0.15f, 0.35f); area.xMin += AnimancerGUI.IndentSize; area.width -= 18; float length; var wrappedTime = GetWrappedTime(out length); 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. /// /// If Shift is also held, the effect will be queued until after the previous animation finishes. /// private void HandleLabelClick(Rect area) { var currentEvent = Event.current; if (currentEvent.type != EventType.MouseUp || !currentEvent.control || !area.Contains(currentEvent.mousePosition)) return; currentEvent.Use(); if (currentEvent.shift) { AnimationQueue.CrossFadeQueued(Target); return; } AnimationQueue.ClearQueue(Target.Layer); Target.Root.UnpauseGraph(); var fadeDuration = Target.CalculateEditorFadeDuration(AnimancerPlayable.DefaultFadeDuration); Target.Root.Play(Target, fadeDuration); } /************************************************************************************************************************/ /// Draws a foldout arrow to expand/collapse the state details. protected override void DoFoldoutGUI(Rect area) { var key = Target.Key; var isAssetUsedAsKey = key == null || ReferenceEquals(key, Target.MainObject); 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; } /************************************************************************************************************************/ /// /// Manages the playing of animations in sequence. /// private sealed class AnimationQueue { /************************************************************************************************************************/ private static readonly Dictionary PlayableToQueue = new Dictionary(); private readonly List Queue = new List(); /************************************************************************************************************************/ private AnimationQueue() { } public static void CrossFadeQueued(AnimancerState state) { CleanUp(); var layer = state.Layer; // If the layer has no current state, just play the animation immediately. if (!layer.CurrentState.IsValid() || layer.CurrentState.Weight == 0) { var fadeDuration = state.CalculateEditorFadeDuration(AnimancerPlayable.DefaultFadeDuration); layer.Play(state, fadeDuration); return; } AnimationQueue queue; if (!PlayableToQueue.TryGetValue(layer, out queue)) { queue = new AnimationQueue(); PlayableToQueue.Add(layer, queue); } queue.Queue.Add(state); layer.CurrentState.Events.OnEnd -= queue.PlayNext; layer.CurrentState.Events.OnEnd += queue.PlayNext; } /************************************************************************************************************************/ public static void ClearQueue(AnimancerLayer layer) { PlayableToQueue.Remove(layer); } /************************************************************************************************************************/ private static readonly List OldQueues = new List(); /// /// Clear out any playables that have been destroyed. /// private static void CleanUp() { OldQueues.Clear(); foreach (var layer in PlayableToQueue.Keys) { if (!layer.IsValid) OldQueues.Add(layer); } for (int i = 0; i < OldQueues.Count; i++) { PlayableToQueue.Remove(OldQueues[i]); } } /************************************************************************************************************************/ private void PlayNext() { if (Queue.Count == 0) return; var state = Queue[0]; Queue.RemoveAt(0); if (!state.IsValid()) { PlayNext(); return; } var fadeDuration = state.CalculateEditorFadeDuration(AnimancerPlayable.DefaultFadeDuration); state.Layer.Play(state, fadeDuration); state.Events.OnEnd = PlayNext; } /************************************************************************************************************************/ } /************************************************************************************************************************/ /// /// Gets the current . /// If the state is looping, the value is modulo by the . /// private float GetWrappedTime(out float length) { var time = Target.Time; length = Target.Length; var wrappedTime = time; if (Target.IsLooping) { wrappedTime = Mathf.Repeat(wrappedTime, length); if (wrappedTime == 0 && time != 0) wrappedTime = length; } return wrappedTime; } /************************************************************************************************************************/ /// Draws the details of the target state in the GUI. protected override void DoDetailsGUI(IAnimancerComponent owner) { 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; float length; var time = GetWrappedTime(out 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 static readonly BoolPref UseNormalizedTimeSliders = new BoolPref("Inspector", "UseNormalizedTimeSliders", false); private static readonly GUIElementWidth UseNormalizedTimeSlidersWidth = new GUIElementWidth(); private bool DoNormalizedTimeToggle(ref Rect area) { var content = AnimancerGUI.TempContent("N"); var style = AnimancerGUI.MiniButton; var width = UseNormalizedTimeSlidersWidth.GetWidth(style, content.text); var toggleArea = AnimancerGUI.StealFromRight(ref area, width); UseNormalizedTimeSliders.Value = GUI.Toggle(toggleArea, UseNormalizedTimeSliders, content, 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 = (int)Math.Abs(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.Events.OnEnd == null) return; var area = AnimancerGUI.LayoutSingleLineRect(AnimancerGUI.SpacingMode.Before); EditorGUI.LabelField(area, "OnEnd: " + Target.Events.OnEnd.Method); } /************************************************************************************************************************/ #region Context Menu /************************************************************************************************************************/ /// Adds functions relevant to the . protected override void PopulateContextMenu(GenericMenu menu) { AddContextMenuFunctions(menu); AnimancerEditorUtilities.AddMenuItem(menu, "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); }); AnimancerEditorUtilities.AddFadeFunction(menu, "Cross Fade Queued (Ctrl + Shift + Click)", Target.Weight != 1, Target, (duration) => { AnimationQueue.CrossFadeQueued(Target); }); menu.AddSeparator(""); menu.AddItem(new GUIContent("Destroy State"), false, () => Target.Destroy()); menu.AddSeparator(""); AnimancerEditorUtilities.AddDocumentationLink(menu, "State Documentation", "/docs/manual/states"); } /************************************************************************************************************************/ /// [Editor-Only] /// Adds the details of this state to the menu. /// By default, that means a single item showing the path of the . /// protected virtual void AddContextMenuFunctions(GenericMenu menu) { menu.AddDisabledItem(new GUIContent(DetailsPrefix + "Key: " + Target.Key)); var length = Target.Length; if (!float.IsNaN(length)) menu.AddDisabledItem(new GUIContent(DetailsPrefix + "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("/", "->"))); } const string OnEndPrefix = "End Event/"; { var endEvent = Target.Events.endEvent; menu.AddDisabledItem(new GUIContent(OnEndPrefix + "NormalizedTime: " + endEvent.normalizedTime)); if (endEvent.callback == null) { menu.AddDisabledItem(new GUIContent(OnEndPrefix + "Callback: null")); } else { var label = OnEndPrefix + (endEvent.callback.Target != null ? ("Target: " + endEvent.callback.Target) : "Target: null"); var targetObject = endEvent.callback.Target as Object; AnimancerEditorUtilities.AddMenuItem(menu, label, targetObject != null, () => Selection.activeObject = targetObject); menu.AddDisabledItem(new GUIContent( OnEndPrefix + "Declaring Type: " + endEvent.callback.Method.DeclaringType.FullName)); menu.AddDisabledItem(new GUIContent( OnEndPrefix + "Method: " + endEvent.callback.Method)); } AnimancerEditorUtilities.AddMenuItem(menu, OnEndPrefix + "Clear", !float.IsNaN(endEvent.normalizedTime) || endEvent.callback != null, () => Target.Events.endEvent = new AnimancerEvent(float.NaN, null)); AnimancerEditorUtilities.AddMenuItem(menu, OnEndPrefix + "Invoke", endEvent.callback != null, () => Target.Events.endEvent.Invoke(Target)); } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } } #endif