123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534 |
- // Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
- #if UNITY_EDITOR && UNITY_IMGUI
- using System;
- using System.Collections.Generic;
- using UnityEditor;
- using UnityEngine;
- using static Animancer.Editor.AnimancerGUI;
- using Object = UnityEngine.Object;
- namespace Animancer.Editor
- {
- /// <summary>[Editor-Only]
- /// A custom Inspector for an <see cref="AnimancerLayer"/> which sorts and exposes some of its internal values.
- /// </summary>
- /// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerLayerDrawer
- ///
- [CustomGUI(typeof(AnimancerLayer))]
- public class AnimancerLayerDrawer : AnimancerNodeDrawer<AnimancerLayer>
- {
- /************************************************************************************************************************/
- /// <summary>The states in the target layer which have non-zero <see cref="AnimancerNode.Weight"/>.</summary>
- public readonly List<AnimancerState> ActiveStates = new();
- /// <summary>The states in the target layer which have zero <see cref="AnimancerNode.Weight"/>.</summary>
- public readonly List<AnimancerState> InactiveStates = new();
- /************************************************************************************************************************/
- #region Gathering
- /************************************************************************************************************************/
- /// <summary>Initializes an editor in the list for each layer in the `graph`.</summary>
- /// <remarks>
- /// The `count` indicates the number of elements actually being used.
- /// Spare elements are kept in the list in case they need to be used again later.
- /// </remarks>
- internal static void GatherLayerEditors(
- AnimancerGraph graph,
- List<AnimancerLayerDrawer> editors,
- out int count)
- {
- count = graph.Layers.Count;
- for (int i = 0; i < count; i++)
- {
- AnimancerLayerDrawer editor;
- if (editors.Count <= i)
- {
- editor = new();
- editors.Add(editor);
- }
- else
- {
- editor = editors[i];
- }
- editor.GatherStates(graph.Layers[i]);
- }
- }
- /************************************************************************************************************************/
- /// <summary>
- /// Sets the target `layer` and sorts its states and their keys into the active/inactive lists.
- /// </summary>
- private void GatherStates(AnimancerLayer layer)
- {
- Value = layer;
- ActiveStates.Clear();
- InactiveStates.Clear();
- foreach (var state in layer)
- {
- if (state.IsActive || !AnimancerGraphDrawer.SeparateActiveFromInactiveStates)
- {
- ActiveStates.Add(state);
- continue;
- }
- if (AnimancerGraphDrawer.ShowInactiveStates)
- InactiveStates.Add(state);
- }
- SortAndGatherKeys(ActiveStates);
- SortAndGatherKeys(InactiveStates);
- }
- /************************************************************************************************************************/
- /// <summary>
- /// Sorts any entries that use another state as their key to come right after that state.
- /// See <see cref="AnimancerLayer.Play(AnimancerState, float, FadeMode)"/>.
- /// </summary>
- private static void SortAndGatherKeys(List<AnimancerState> states)
- {
- var count = states.Count;
- if (count == 0)
- return;
- AnimancerGraphDrawer.ApplySortStatesByName(states);
- // Sort any states that use another state as their key to be right after the key.
- for (int i = 0; i < count; i++)
- {
- var state = states[i];
- var key = state.Key;
- if (key is not AnimancerState keyState)
- continue;
- var keyStateIndex = states.IndexOf(keyState);
- if (keyStateIndex < 0 || keyStateIndex + 1 == i)
- continue;
- states.RemoveAt(i);
- if (keyStateIndex < i)
- keyStateIndex++;
- states.Insert(keyStateIndex, state);
- i--;
- }
- }
- /************************************************************************************************************************/
- #endregion
- /************************************************************************************************************************/
- /// <summary>Draws the layer's name and weight.</summary>
- protected override void DoLabelGUI(Rect area)
- {
- var label = Value.IsAdditive ? "Additive" : "Override";
- if (Value._Mask != null)
- label = $"{label} ({Value._Mask.GetCachedName()})";
- area.xMin += FoldoutIndent;
- DoWeightLabel(ref area, Value.Weight, Value.EffectiveWeight);
- EditorGUIUtility.labelWidth -= FoldoutIndent;
- EditorGUI.LabelField(area, Value.ToString(), label);
- EditorGUIUtility.labelWidth += FoldoutIndent;
- }
- /************************************************************************************************************************/
- /// <summary>The number of pixels of indentation required to fit the foldout arrow.</summary>
- const float FoldoutIndent = 12;
- /// <inheritdoc/>
- protected override void DoFoldoutGUI(Rect area)
- {
- var hierarchyMode = EditorGUIUtility.hierarchyMode;
- EditorGUIUtility.hierarchyMode = true;
- area.xMin += FoldoutIndent;
- IsExpanded = EditorGUI.Foldout(area, IsExpanded, GUIContent.none, true);
- EditorGUIUtility.hierarchyMode = hierarchyMode;
- }
- /************************************************************************************************************************/
- /// <inheritdoc/>
- protected override void DoDetailsGUI()
- {
- EditorGUI.indentLevel++;
- base.DoDetailsGUI();
- if (IsExpanded)
- {
- GUILayout.BeginHorizontal();
- GUILayout.Space(FoldoutIndent);
- GUILayout.BeginVertical();
- DoLayerDetailsGUI();
- DoNodeDetailsGUI();
- GUILayout.EndVertical();
- GUILayout.EndHorizontal();
- }
- EditorGUI.indentLevel--;
- DoStatesGUI();
- }
- /************************************************************************************************************************/
- /// <summary>
- /// Draws controls for <see cref="AnimancerLayer.IsAdditive"/> and <see cref="AnimancerLayer._Mask"/>.
- /// </summary>
- private void DoLayerDetailsGUI()
- {
- var area = LayoutSingleLineRect(SpacingMode.Before);
- area = EditorGUI.IndentedRect(area);
- area.xMin += ExtraLeftPadding;
- var labelWidth = EditorGUIUtility.labelWidth;
- var indentLevel = EditorGUI.indentLevel;
- EditorGUI.indentLevel = 0;
- var additiveLabel = "Is Additive";
- var additiveWidth = GUI.skin.toggle.CalculateWidth(additiveLabel) + StandardSpacing * 2;
- var additiveArea = StealFromLeft(ref area, additiveWidth, StandardSpacing);
- var maskArea = area;
- // Additive.
- EditorGUIUtility.labelWidth = CalculateLabelWidth(additiveLabel);
- EditorGUI.BeginChangeCheck();
- var isAdditive = EditorGUI.Toggle(additiveArea, additiveLabel, Value.IsAdditive);
- if (EditorGUI.EndChangeCheck())
- Value.IsAdditive = isAdditive;
- // Mask.
- using (var label = PooledGUIContent.Acquire("Mask"))
- {
- EditorGUIUtility.labelWidth = CalculateLabelWidth(label.text);
- EditorGUI.BeginChangeCheck();
- var mask = DoObjectFieldGUI(maskArea, label, Value.Mask, false);
- if (EditorGUI.EndChangeCheck())
- Value.Mask = mask;
- }
- EditorGUI.indentLevel = indentLevel;
- EditorGUIUtility.labelWidth = labelWidth;
- }
- /************************************************************************************************************************/
- private void DoStatesGUI()
- {
- if (!AnimancerGraphDrawer.ShowInactiveStates)
- {
- DoStatesGUI("Active States", ActiveStates);
- }
- else if (AnimancerGraphDrawer.SeparateActiveFromInactiveStates)
- {
- DoStatesGUI("Active States", ActiveStates);
- DoStatesGUI("Inactive States", InactiveStates);
- }
- else
- {
- DoStatesGUI("States", ActiveStates);
- }
- if (Value.Weight != 0 &&
- !Value.IsAdditive &&
- !Mathf.Approximately(Value.GetTotalChildWeight(), 1))
- {
- var message =
- "The total Weight of all states in this layer does not equal 1" +
- " which will likely give undesirable results.";
- if (AreAllStatesFadingOut())
- message +=
- " If you no longer want anything playing on a layer," +
- " you should fade out that layer instead of fading out its states.";
- message += " Click here for more information.";
- EditorGUILayout.HelpBox(message, MessageType.Warning);
- if (TryUseClickEventInLastRect())
- EditorUtility.OpenWithDefaultApp(Strings.DocsURLs.Layers);
- }
- }
- /************************************************************************************************************************/
- /// <summary>Are all the target's states fading out to 0?</summary>
- private bool AreAllStatesFadingOut()
- {
- var count = Value.ActiveStates.Count;
- if (count == 0)
- return false;
- for (int i = 0; i < count; i++)
- {
- var state = Value.ActiveStates[i];
- if (state.TargetWeight != 0)
- return false;
- }
- return true;
- }
- /************************************************************************************************************************/
- /// <summary>Draws all `states` in the given list.</summary>
- public void DoStatesGUI(string label, List<AnimancerState> states)
- {
- var area = LayoutSingleLineRect();
- const string Label = "Weight";
- var width = CalculateLabelWidth(Label);
- GUI.Label(StealFromRight(ref area, width), Label);
- EditorGUI.LabelField(area, label, states.Count.ToStringCached());
- EditorGUI.indentLevel++;
- for (int i = 0; i < states.Count; i++)
- {
- DoStateGUI(states[i]);
- }
- EditorGUI.indentLevel--;
- }
- /************************************************************************************************************************/
- /// <summary>Cached Inspectors that have already been created for states.</summary>
- private readonly Dictionary<AnimancerState, ICustomGUI>
- StateInspectors = new();
- /// <summary>Draws the Inspector for the given `state`.</summary>
- private void DoStateGUI(AnimancerState state)
- {
- if (!StateInspectors.TryGetValue(state, out var inspector))
- {
- inspector = CustomGUIFactory.GetOrCreateForObject(state);
- StateInspectors.Add(state, inspector);
- }
- inspector?.DoGUI();
- DoChildStatesGUI(state);
- }
- /************************************************************************************************************************/
- /// <summary>Draws all child states of the `state`.</summary>
- private void DoChildStatesGUI(AnimancerState state)
- {
- if (!state._IsInspectorExpanded)
- return;
- EditorGUI.indentLevel++;
- foreach (var child in state)
- if (child != null)
- DoStateGUI(child);
- EditorGUI.indentLevel--;
- }
- /************************************************************************************************************************/
- /// <inheritdoc/>
- protected override void DoHeaderGUI()
- {
- if (!AnimancerGraphDrawer.ShowSingleLayerHeader &&
- Value.Graph.Layers.Count == 1 &&
- Value.Weight == 1 &&
- Value.TargetWeight == 1 &&
- Value.Speed == 1 &&
- !Value.IsAdditive &&
- Value._Mask == null &&
- Value.Graph.Component != null &&
- Value.Graph.Component.Animator != null &&
- Value.Graph.Component.Animator.runtimeAnimatorController == null)
- return;
- base.DoHeaderGUI();
- }
- /************************************************************************************************************************/
- /// <inheritdoc/>
- public override void DoGUI()
- {
- if (!Value.IsValid())
- return;
- base.DoGUI();
- var area = GUILayoutUtility.GetLastRect();
- HandleDragAndDropToPlay(area, Value);
- }
- /************************************************************************************************************************/
- /// <summary>
- /// If <see cref="AnimationClip"/>s or <see cref="IAnimationClipSource"/>s are dropped inside the `dropArea`,
- /// this method creates a new state in the `target` for each animation.
- /// </summary>
- public static void HandleDragAndDropToPlay(Rect area, object layerOrGraph)
- {
- if (layerOrGraph == null)
- return;
- _DragAndDropPlayTarget = layerOrGraph;
- _DragAndDropPlayHandler ??= HandleDragAndDropToPlay;
- _DragAndDropPlayHandler.Handle(area);
- _DragAndDropPlayTarget = null;
- }
- private static DragAndDropHandler<Object> _DragAndDropPlayHandler;
- private static object _DragAndDropPlayTarget;
- private static AnimancerLayer DragAndDropPlayTargetLayer
- => _DragAndDropPlayTarget as AnimancerLayer
- ?? (_DragAndDropPlayTarget is AnimancerGraph graph ? graph.Layers[0] : null);
- /// <summary>Handles drag and drop events to play animations and transitions.</summary>
- public static bool HandleDragAndDropToPlay(Object obj, bool isDrop)
- {
- if (_DragAndDropPlayTarget == null)
- return false;
- if (obj is AnimationClip clip)
- {
- if (clip.legacy)
- return false;
- if (isDrop)
- DragAndDropPlayTargetLayer.Play(clip);
- return true;
- }
- if (obj is ITransition transition)
- {
- if (isDrop)
- DragAndDropPlayTargetLayer.Play(transition);
- return true;
- }
- var transitionAsset = TryCreateTransitionAttribute.TryCreateTransitionAsset(obj);
- if (transitionAsset != null)
- {
- if (isDrop)
- DragAndDropPlayTargetLayer.Play(transitionAsset);
- if (!EditorUtility.IsPersistent(transitionAsset))
- Object.DestroyImmediate(transitionAsset);
- return true;
- }
- using (ListPool<AnimationClip>.Instance.Acquire(out var clips))
- {
- clips.GatherFromSource(obj);
- var anyValid = false;
- for (int i = 0; i < clips.Count; i++)
- {
- clip = clips[i];
- if (clip.legacy)
- continue;
- if (!isDrop)
- return true;
- anyValid = true;
- DragAndDropPlayTargetLayer.Play(clip);
- }
- if (anyValid)
- return true;
- }
- return false;
- }
- /************************************************************************************************************************/
- #region Context Menu
- /************************************************************************************************************************/
- /// <inheritdoc/>
- protected override void PopulateContextMenu(GenericMenu menu)
- {
- menu.AddDisabledItem(new($"{DetailsPrefix}{nameof(Value.CurrentState)}: {Value.CurrentState}"));
- menu.AddDisabledItem(new($"{DetailsPrefix}{nameof(Value.CommandCount)}: {Value.CommandCount}"));
- menu.AddFunction("Stop",
- HasAnyStates((state) => state.IsPlaying || state.Weight != 0),
- () => Value.Stop());
- AnimancerEditorUtilities.AddFadeFunction(menu, "Fade In",
- Value.Index > 0 && Value.Weight != 1, Value,
- (duration) => Value.StartFade(1, duration));
- AnimancerEditorUtilities.AddFadeFunction(menu, "Fade Out",
- Value.Index > 0 && Value.Weight != 0, Value,
- (duration) => Value.StartFade(0, duration));
- AnimancerNodeBase.AddContextMenuIK(menu, Value);
- menu.AddSeparator("");
- menu.AddFunction("Destroy States",
- ActiveStates.Count > 0 || InactiveStates.Count > 0,
- () => Value.DestroyStates());
- AnimancerGraphDrawer.AddRootFunctions(menu, Value.Graph);
- menu.AddSeparator("");
- AnimancerGraphDrawer.AddDisplayOptions(menu);
- AnimancerEditorUtilities.AddDocumentationLink(menu, "Layer Documentation", Strings.DocsURLs.Layers);
- menu.ShowAsContext();
- }
- /************************************************************************************************************************/
- private bool HasAnyStates(Func<AnimancerState, bool> condition)
- {
- foreach (var state in Value)
- {
- if (condition(state))
- return true;
- }
- return false;
- }
- /************************************************************************************************************************/
- #endregion
- /************************************************************************************************************************/
- }
- }
- #endif
|