535 lines
19 KiB
C#
535 lines
19 KiB
C#
// 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
|
|
|