123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449 |
- // Animancer // https://kybernetik.com.au/animancer // Copyright 2022 Kybernetik //
- #if UNITY_EDITOR
- using System;
- using UnityEditor;
- using UnityEngine;
- using Object = UnityEngine.Object;
- using static Animancer.Editor.AnimancerPlayableDrawer;
- namespace Animancer.Editor
- {
- /// <summary>[Editor-Only] Draws the Inspector GUI for an <see cref="AnimancerState"/>.</summary>
- /// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerStateDrawer_1
- ///
- public class AnimancerStateDrawer<T> : AnimancerNodeDrawer<T> where T : AnimancerState
- {
- /************************************************************************************************************************/
- /// <summary>
- /// Creates a new <see cref="AnimancerStateDrawer{T}"/> to manage the Inspector GUI for the `target`.
- /// </summary>
- public AnimancerStateDrawer(T target) => Target = target;
- /************************************************************************************************************************/
- /// <summary>The <see cref="GUIStyle"/> used for the area encompassing this drawer is <c>null</c>.</summary>
- protected override GUIStyle RegionStyle => null;
- /************************************************************************************************************************/
- /// <summary>Determines whether the <see cref="AnimancerState.MainObject"/> field can occupy the whole line.</summary>
- private bool IsAssetUsedAsKey =>
- string.IsNullOrEmpty(Target.DebugName) &&
- (Target.Key == null || ReferenceEquals(Target.Key, Target.MainObject));
- /************************************************************************************************************************/
- /// <inheritdoc/>
- protected override bool AutoNormalizeSiblingWeights => AutoNormalizeWeights;
- /************************************************************************************************************************/
- /// <summary>
- /// Draws the state's main label: an <see cref="Object"/> field if it has a
- /// <see cref="AnimancerState.MainObject"/>, otherwise just a simple text label.
- /// <para></para>
- /// Also shows a bar to indicate its progress.
- /// </summary>
- 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);
- }
- /************************************************************************************************************************/
- /// <summary>Draws a progress bar to show the animation time.</summary>
- 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;
- }
- /************************************************************************************************************************/
- /// <summary>Handles Ctrl + Click on the label to CrossFade the animation.</summary>
- 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);
- }
- /************************************************************************************************************************/
- /// <inheritdoc/>
- 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;
- }
- /************************************************************************************************************************/
- /// <summary>
- /// Gets the current <see cref="AnimancerState.Time"/>.
- /// If the state is looping, the value is modulo by the <see cref="AnimancerState.Length"/>.
- /// </summary>
- private float GetWrappedTime(out float length) => GetWrappedTime(Target.Time, length = Target.Length, Target.IsLooping);
- /// <summary>
- /// Gets the current <see cref="AnimancerState.Time"/>.
- /// If the state is looping, the value is modulo by the <see cref="AnimancerState.Length"/>.
- /// </summary>
- 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;
- }
- /************************************************************************************************************************/
- /// <inheritdoc/>
- protected override void DoDetailsGUI()
- {
- if (!IsExpanded)
- return;
- EditorGUI.indentLevel++;
- DoTimeSliderGUI();
- DoNodeDetailsGUI();
- DoOnEndGUI();
- EditorGUI.indentLevel--;
- }
- /************************************************************************************************************************/
- /// <summary>Draws a slider for controlling the current <see cref="AnimancerState.Time"/>.</summary>
- 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<int, string> _LoopCounterCache;
- private void DoLoopCounterGUI(ref Rect area, float length)
- {
- if (_LoopCounterCache == null)
- _LoopCounterCache = new ConversionCache<int, string>((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
- /************************************************************************************************************************/
- /// <inheritdoc/>
- 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);
- }
- /************************************************************************************************************************/
- /// <summary>Adds the details of this state to the `menu`.</summary>
- 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
|