You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
470 lines
19 KiB
C#
470 lines
19 KiB
C#
3 months ago
|
// Animancer // Copyright 2020 Kybernetik //
|
||
|
|
||
|
#if UNITY_EDITOR
|
||
|
|
||
|
using System;
|
||
|
using System.Collections.Generic;
|
||
|
using System.Reflection;
|
||
|
using UnityEditor;
|
||
|
using UnityEngine;
|
||
|
|
||
|
namespace Animancer.Editor
|
||
|
{
|
||
|
/// <summary>[Internal]
|
||
|
/// A custom Inspector for an <see cref="AnimancerLayer"/> which sorts and exposes some of its internal values.
|
||
|
/// </summary>
|
||
|
public sealed 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 List<AnimancerState>();
|
||
|
|
||
|
/// <summary>The states in the target layer which have zero <see cref="AnimancerNode.Weight"/>.</summary>
|
||
|
public readonly List<AnimancerState> InactiveStates = new List<AnimancerState>();
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>The <see cref="GUIStyle"/> used for the area encompassing this drawer. <see cref="GUISkin.box"/>.</summary>
|
||
|
protected override GUIStyle RegionStyle { get { return GUI.skin.box; } }
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
#region Gathering
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>
|
||
|
/// Initialises an editor in the list for each layer in the `animancer`.
|
||
|
/// <para></para>
|
||
|
/// 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.
|
||
|
/// </summary>
|
||
|
internal static void GatherLayerEditors(AnimancerPlayable animancer, List<AnimancerLayerDrawer> editors, out int count)
|
||
|
{
|
||
|
count = animancer.Layers.Count;
|
||
|
for (int i = 0; i < count; i++)
|
||
|
{
|
||
|
AnimancerLayerDrawer editor;
|
||
|
if (editors.Count <= i)
|
||
|
{
|
||
|
editor = new AnimancerLayerDrawer();
|
||
|
editors.Add(editor);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
editor = editors[i];
|
||
|
}
|
||
|
|
||
|
editor.GatherStates(animancer.Layers._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)
|
||
|
{
|
||
|
Target = layer;
|
||
|
|
||
|
ActiveStates.Clear();
|
||
|
InactiveStates.Clear();
|
||
|
|
||
|
foreach (var state in layer)
|
||
|
{
|
||
|
if (HideInactiveStates && state.Weight == 0)
|
||
|
continue;
|
||
|
|
||
|
if (!SeparateActiveFromInactiveStates || state.Weight != 0)
|
||
|
{
|
||
|
ActiveStates.Add(state);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
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="AnimancerPlayable.Play(AnimancerState, float, FadeMode)"/>.
|
||
|
/// </summary>
|
||
|
private static void SortAndGatherKeys(List<AnimancerState> states)
|
||
|
{
|
||
|
var count = states.Count;
|
||
|
if (count == 0)
|
||
|
return;
|
||
|
|
||
|
if (SortStatesByName)
|
||
|
{
|
||
|
states.Sort((x, y) =>
|
||
|
{
|
||
|
if (x.MainObject == null)
|
||
|
return y.MainObject == null ? 0 : 1;
|
||
|
else if (y.MainObject == null)
|
||
|
return -1;
|
||
|
|
||
|
return x.MainObject.name.CompareTo(y.MainObject.name);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// Sort any states that use another state as their key to be right after the key.
|
||
|
for (int i = 0; i < states.Count; i++)
|
||
|
{
|
||
|
var state = states[i];
|
||
|
var key = state.Key;
|
||
|
|
||
|
var keyState = key as AnimancerState;
|
||
|
if (keyState == null)
|
||
|
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 = Target.IsAdditive ? "Additive" : "Override";
|
||
|
if (Target._Mask != null)
|
||
|
label = string.Concat(label, " (", Target._Mask.name, ")");
|
||
|
|
||
|
area.xMin += FoldoutIndent;
|
||
|
|
||
|
AnimancerGUI.DoWeightLabel(ref area, Target.Weight);
|
||
|
|
||
|
EditorGUIUtility.labelWidth -= FoldoutIndent;
|
||
|
EditorGUI.LabelField(area, Target.ToString(), label);
|
||
|
EditorGUIUtility.labelWidth += FoldoutIndent;
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>The number of pixels of indentation required to fit the foldout arrow.</summary>
|
||
|
const float FoldoutIndent = 12;
|
||
|
|
||
|
/// <summary>Draws a foldout arrow to expand/collapse the state details.</summary>
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary> Draws the details of the target state in the GUI.</summary>
|
||
|
protected override void DoDetailsGUI(IAnimancerComponent owner)
|
||
|
{
|
||
|
if (IsExpanded)
|
||
|
{
|
||
|
EditorGUI.indentLevel++;
|
||
|
GUILayout.BeginHorizontal();
|
||
|
GUILayout.Space(FoldoutIndent);
|
||
|
GUILayout.BeginVertical();
|
||
|
|
||
|
DoLayerDetailsGUI();
|
||
|
DoNodeDetailsGUI();
|
||
|
|
||
|
GUILayout.EndVertical();
|
||
|
GUILayout.EndHorizontal();
|
||
|
EditorGUI.indentLevel--;
|
||
|
}
|
||
|
|
||
|
DoStatesGUI(owner);
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
private static readonly GUIElementWidth AdditiveToggleWidth = new GUIElementWidth();
|
||
|
private static readonly GUIElementWidth AdditiveLabelWidth = new GUIElementWidth();
|
||
|
|
||
|
/// <summary>
|
||
|
/// Draws controls for <see cref="AnimancerLayer.IsAdditive"/> and <see cref="AnimancerLayer._Mask"/>.
|
||
|
/// </summary>
|
||
|
private void DoLayerDetailsGUI()
|
||
|
{
|
||
|
var area = AnimancerGUI.LayoutSingleLineRect(AnimancerGUI.SpacingMode.Before);
|
||
|
area = EditorGUI.IndentedRect(area);
|
||
|
|
||
|
var labelWidth = EditorGUIUtility.labelWidth;
|
||
|
var indentLevel = EditorGUI.indentLevel;
|
||
|
EditorGUI.indentLevel = 0;
|
||
|
|
||
|
var additiveLabel = AnimancerGUI.GetNarrowText("Is Additive");
|
||
|
|
||
|
var additiveWidth = AdditiveToggleWidth.GetWidth(GUI.skin.toggle, additiveLabel);
|
||
|
var maskRect = AnimancerGUI.StealFromRight(ref area, area.width - additiveWidth);
|
||
|
|
||
|
// Additive.
|
||
|
EditorGUIUtility.labelWidth = AdditiveLabelWidth.GetWidth(GUI.skin.label, additiveLabel);
|
||
|
Target.IsAdditive = EditorGUI.Toggle(area, additiveLabel, Target.IsAdditive);
|
||
|
|
||
|
// Mask.
|
||
|
var maskLabel = AnimancerGUI.TempContent("Mask");
|
||
|
EditorGUIUtility.labelWidth = GUI.skin.label.CalculateWidth(maskLabel);
|
||
|
EditorGUI.BeginChangeCheck();
|
||
|
Target._Mask = (AvatarMask)EditorGUI.ObjectField(maskRect, maskLabel, Target._Mask, typeof(AvatarMask), false);
|
||
|
if (EditorGUI.EndChangeCheck())
|
||
|
Target.SetMask(Target._Mask);
|
||
|
|
||
|
EditorGUI.indentLevel = indentLevel;
|
||
|
EditorGUIUtility.labelWidth = labelWidth;
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
private void DoStatesGUI(IAnimancerComponent owner)
|
||
|
{
|
||
|
if (HideInactiveStates)
|
||
|
{
|
||
|
DoStatesGUI("Active States", ActiveStates, owner);
|
||
|
}
|
||
|
else if (SeparateActiveFromInactiveStates)
|
||
|
{
|
||
|
DoStatesGUI("Active States", ActiveStates, owner);
|
||
|
DoStatesGUI("Inactive States", InactiveStates, owner);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
DoStatesGUI("States", ActiveStates, owner);
|
||
|
}
|
||
|
|
||
|
if (Target.Index == 0 &&
|
||
|
Target.Weight != 0 &&
|
||
|
!Target.IsAdditive &&
|
||
|
!Mathf.Approximately(Target.GetTotalWeight(), 1))
|
||
|
{
|
||
|
EditorGUILayout.HelpBox(
|
||
|
"The total Weight of all states in this layer does not equal 1, which will likely give undesirable results." +
|
||
|
" Click here for more information.",
|
||
|
MessageType.Warning);
|
||
|
|
||
|
if (AnimancerGUI.TryUseClickEventInLastRect())
|
||
|
EditorUtility.OpenWithDefaultApp(Strings.DocsURLs.Fading);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>Draws all `states` in the given list.</summary>
|
||
|
private void DoStatesGUI(string label, List<AnimancerState> states, IAnimancerComponent owner)
|
||
|
{
|
||
|
var area = AnimancerGUI.LayoutSingleLineRect();
|
||
|
|
||
|
var width = AnimancerGUI.CalculateLabelWidth("Weight");
|
||
|
GUI.Label(AnimancerGUI.StealFromRight(ref area, width), "Weight");
|
||
|
|
||
|
EditorGUI.LabelField(area, label, states.Count.ToString());
|
||
|
|
||
|
EditorGUI.indentLevel++;
|
||
|
for (int i = 0; i < states.Count; i++)
|
||
|
{
|
||
|
DoStateGUI(states[i], owner);
|
||
|
}
|
||
|
EditorGUI.indentLevel--;
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>Cached Inspectors that have already been created for states.</summary>
|
||
|
private readonly Dictionary<AnimancerState, IAnimancerNodeDrawer>
|
||
|
StateInspectors = new Dictionary<AnimancerState, IAnimancerNodeDrawer>();
|
||
|
|
||
|
/// <summary>Draws the Inspector for the given `state`.</summary>
|
||
|
private void DoStateGUI(AnimancerState state, IAnimancerComponent owner)
|
||
|
{
|
||
|
IAnimancerNodeDrawer Inspector;
|
||
|
if (!StateInspectors.TryGetValue(state, out Inspector))
|
||
|
{
|
||
|
Inspector = state.GetDrawer();
|
||
|
StateInspectors.Add(state, Inspector);
|
||
|
}
|
||
|
|
||
|
Inspector.DoGUI(owner);
|
||
|
DoChildStatesGUI(state, owner);
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>Draws all child states of the `state`.</summary>
|
||
|
private void DoChildStatesGUI(AnimancerState state, IAnimancerComponent owner)
|
||
|
{
|
||
|
EditorGUI.indentLevel++;
|
||
|
|
||
|
foreach (var child in state)
|
||
|
{
|
||
|
if (child == null)
|
||
|
continue;
|
||
|
|
||
|
DoStateGUI(child, owner);
|
||
|
}
|
||
|
|
||
|
EditorGUI.indentLevel--;
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>
|
||
|
/// Draws the details and controls for the target <see cref="AnimancerNodeDrawer{T}.Target"/> in the Inspector.
|
||
|
/// </summary>
|
||
|
public override void DoGUI(IAnimancerComponent owner)
|
||
|
{
|
||
|
if (!Target.IsValid)
|
||
|
return;
|
||
|
|
||
|
base.DoGUI(owner);
|
||
|
|
||
|
var area = GUILayoutUtility.GetLastRect();
|
||
|
HandleDragAndDropAnimations(area, owner, Target.Index);
|
||
|
}
|
||
|
|
||
|
/// <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 HandleDragAndDropAnimations(Rect dropArea, IAnimancerComponent target, int layerIndex)
|
||
|
{
|
||
|
AnimancerGUI.HandleDragAndDropAnimations(dropArea, (clip) =>
|
||
|
{
|
||
|
target.Playable.Layers[layerIndex].GetOrCreateState(clip);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
#region Context Menu
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>Adds functions relevant to the <see cref="AnimancerNodeDrawer{T}.Target"/>.</summary>
|
||
|
protected override void PopulateContextMenu(GenericMenu menu)
|
||
|
{
|
||
|
menu.AddDisabledItem(new GUIContent(DetailsPrefix + "CurrentState: " + Target.CurrentState));
|
||
|
menu.AddDisabledItem(new GUIContent(DetailsPrefix + "CommandCount: " + Target.CommandCount));
|
||
|
|
||
|
AnimancerEditorUtilities.AddMenuItem(menu, "Stop",
|
||
|
HasAnyStates((state) => state.IsPlaying || state.Weight != 0),
|
||
|
() => Target.Stop());
|
||
|
|
||
|
AnimancerEditorUtilities.AddFadeFunction(menu, "Fade In",
|
||
|
Target.Index > 0 && Target.Weight != 1, Target,
|
||
|
(duration) => Target.StartFade(1, duration));
|
||
|
AnimancerEditorUtilities.AddFadeFunction(menu, "Fade Out",
|
||
|
Target.Index > 0 && Target.Weight != 0, Target,
|
||
|
(duration) => Target.StartFade(0, duration));
|
||
|
|
||
|
menu.AddItem(new GUIContent("Inverse Kinematics/Apply Animator IK"),
|
||
|
Target.ApplyAnimatorIK,
|
||
|
() => Target.ApplyAnimatorIK = !Target.ApplyAnimatorIK);
|
||
|
menu.AddItem(new GUIContent("Inverse Kinematics/Default Apply Animator IK"),
|
||
|
Target.DefaultApplyAnimatorIK,
|
||
|
() => Target.DefaultApplyAnimatorIK = !Target.DefaultApplyAnimatorIK);
|
||
|
menu.AddItem(new GUIContent("Inverse Kinematics/Apply Foot IK"),
|
||
|
Target.ApplyFootIK,
|
||
|
() => Target.ApplyFootIK = !Target.ApplyFootIK);
|
||
|
menu.AddItem(new GUIContent("Inverse Kinematics/Default Apply Foot IK"),
|
||
|
Target.DefaultApplyFootIK,
|
||
|
() => Target.DefaultApplyFootIK = !Target.DefaultApplyFootIK);
|
||
|
|
||
|
menu.AddSeparator("");
|
||
|
|
||
|
AnimancerEditorUtilities.AddMenuItem(menu, "Destroy States",
|
||
|
ActiveStates.Count > 0 || InactiveStates.Count > 0,
|
||
|
() => Target.DestroyStates());
|
||
|
|
||
|
AnimancerEditorUtilities.AddMenuItem(menu, "Add Layer",
|
||
|
Target.Root.Layers.Count < AnimancerPlayable.LayerList.defaultCapacity,
|
||
|
() => Target.Root.Layers.Count++);
|
||
|
AnimancerEditorUtilities.AddMenuItem(menu, "Remove Layer",
|
||
|
Target.Root.Layers.Count > 0,
|
||
|
() => Target.Root.Layers.Count--);
|
||
|
|
||
|
menu.AddSeparator("");
|
||
|
|
||
|
menu.AddItem(new GUIContent("Keep Weightless Playables Connected"),
|
||
|
Target.Root.KeepChildrenConnected,
|
||
|
() => Target.Root.KeepChildrenConnected = !Target.Root.KeepChildrenConnected);
|
||
|
|
||
|
AddPrefFunctions(menu);
|
||
|
|
||
|
menu.AddSeparator("");
|
||
|
|
||
|
AnimancerEditorUtilities.AddDocumentationLink(menu, "Layer Documentation", "/docs/manual/blending/layers");
|
||
|
|
||
|
menu.ShowAsContext();
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
private bool HasAnyStates(Func<AnimancerState, bool> condition)
|
||
|
{
|
||
|
foreach (var state in Target)
|
||
|
{
|
||
|
if (condition(state))
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
#endregion
|
||
|
/************************************************************************************************************************/
|
||
|
#region Prefs
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
internal const string
|
||
|
KeyPrefix = "Inspector",
|
||
|
MenuPrefix = "Display Options/";
|
||
|
private static readonly BoolPref
|
||
|
SortStatesByName = new BoolPref(KeyPrefix, MenuPrefix + "Sort By Name", true),
|
||
|
HideInactiveStates = new BoolPref(KeyPrefix, MenuPrefix + "Hide Inactive", false),
|
||
|
SeparateActiveFromInactiveStates = new BoolPref(KeyPrefix, MenuPrefix + "Separate Active From Inactive", false);
|
||
|
internal static readonly BoolPref
|
||
|
ShowUpdatingNodes = new BoolPref(KeyPrefix, MenuPrefix + "Show Dirty Nodes", false);
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
private static void AddPrefFunctions(GenericMenu menu)
|
||
|
{
|
||
|
SortStatesByName.AddToggleFunction(menu);
|
||
|
HideInactiveStates.AddToggleFunction(menu);
|
||
|
SeparateActiveFromInactiveStates.AddToggleFunction(menu);
|
||
|
ShowUpdatingNodes.AddToggleFunction(menu);
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
#endregion
|
||
|
/************************************************************************************************************************/
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#endif
|
||
|
|