// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using UnityEngine.Playables;
using static Animancer.Editor.AnimancerGUI;
using Object = UnityEngine.Object;
namespace Animancer.Editor
{
/// [Editor-Only] Draws the Inspector GUI for an .
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerGraphDrawer
///
public class AnimancerGraphDrawer
{
/************************************************************************************************************************/
/// The currently drawing instance.
public static AnimancerGraphDrawer Current { get; private set; }
/// A lazy list of information about the layers currently being displayed.
private readonly List
LayerDrawers = new();
/// The number of elements in that are currently being used.
private int _LayerCount;
/************************************************************************************************************************/
/// Draws the GUI of the if there is only one target.
public void DoGUI(IAnimancerComponent[] targets)
{
if (targets.Length != 1)
return;
DoGUI(targets[0]);
}
/************************************************************************************************************************/
/// Draws the GUI of the .
public void DoGUI(IAnimancerComponent target)
{
Current = this;
DoNativeAnimatorControllerGUI(target);
if (!target.IsGraphInitialized)
{
DoGraphNotInitializedGUI(target);
return;
}
GUILayout.BeginVertical();
var hierarchyMode = EditorGUIUtility.hierarchyMode;
EditorGUIUtility.hierarchyMode = true;
EditorGUI.BeginChangeCheck();
var graph = target.Graph;
// Gather the during the layout event and use the same ones during subsequent events to avoid GUI errors
// in case they change (they shouldn't, but this is also more efficient).
if (Event.current.type == EventType.Layout)
{
AnimancerLayerDrawer.GatherLayerEditors(graph, LayerDrawers, out _LayerCount);
GatherMainObjectUsage(graph);
}
AnimancerGraphControls.DoGraphGUI(graph, out var area);
CheckContextMenu(area, graph);
for (int i = 0; i < _LayerCount; i++)
LayerDrawers[i].DoGUI();
DoOrphanStatesGUI(graph);
GUILayout.Space(StandardSpacing);
DoLayerWeightWarningGUI(target);
ParameterDictionaryDrawer.DoParametersGUI(graph);
NamedEventDictionaryDrawer.DoEventsGUI(graph);
if (ShowInternalDetails)
DoInternalDetailsGUI(graph);
if (EditorGUI.EndChangeCheck() && !graph.IsGraphPlaying)
graph.Evaluate();
DoMultipleAnimationSystemWarningGUI(target);
EditorGUIUtility.hierarchyMode = hierarchyMode;
GUILayout.EndVertical();
AnimancerLayerDrawer.HandleDragAndDropToPlay(GUILayoutUtility.GetLastRect(), graph);
Current = null;
}
/************************************************************************************************************************/
/// Draws a GUI for the if there is one.
private void DoNativeAnimatorControllerGUI(IAnimancerComponent target)
{
if (!EditorApplication.isPlaying &&
!target.IsGraphInitialized)
return;
var animator = target.Animator;
if (animator == null)
return;
var controller = animator.runtimeAnimatorController;
if (controller == null)
return;
BeginVerticalBox(GUI.skin.box);
var label = "Native Animator Controller";
EditorGUI.BeginChangeCheck();
controller = DoObjectFieldGUI(label, controller, false);
if (EditorGUI.EndChangeCheck())
animator.runtimeAnimatorController = controller;
if (controller is AnimatorController editorController)
{
var layers = editorController.layers;
for (int i = 0; i < layers.Length; i++)
{
var layer = layers[i];
var runtimeState = animator.IsInTransition(i) ?
animator.GetNextAnimatorStateInfo(i) :
animator.GetCurrentAnimatorStateInfo(i);
var states = layer.stateMachine.states;
var editorState = GetState(states, runtimeState.shortNameHash);
var area = LayoutSingleLineRect(SpacingMode.Before);
var weight = i == 0 ? 1 : animator.GetLayerWeight(i);
string stateName;
if (editorState != null)
{
stateName = editorState.GetCachedName();
var isLooping = editorState.motion != null && editorState.motion.isLooping;
AnimancerStateDrawer.DoTimeHighlightBarGUI(
area,
true,
weight,
runtimeState.normalizedTime * runtimeState.length,
runtimeState.speed,
runtimeState.length,
isLooping);
}
else
{
stateName = "State Not Found";
}
DoWeightLabel(ref area, weight, weight);
EditorGUI.LabelField(area, layer.name, stateName);
}
}
EndVerticalBox(GUI.skin.box);
}
/************************************************************************************************************************/
/// Returns the state with the specified .
private static AnimatorState GetState(ChildAnimatorState[] states, int nameHash)
{
for (int i = 0; i < states.Length; i++)
{
var state = states[i].state;
if (state.nameHash == nameHash)
{
return state;
}
}
return null;
}
/************************************************************************************************************************/
private void DoGraphNotInitializedGUI(IAnimancerComponent target)
{
if (!EditorApplication.isPlaying ||
target.Animator == null ||
EditorUtility.IsPersistent(target.Animator))
return;
EditorGUILayout.HelpBox("Animancer is not initialized." +
" It will be initialized automatically when something uses it, such as playing an animation.",
MessageType.Info);
if (TryUseClickEventInLastRect(1))
{
var menu = new GenericMenu();
menu.AddItem(new("Initialize"), false, () => target.Graph.Evaluate());
AnimancerEditorUtilities.AddDocumentationLink(menu, "Layer Documentation", Strings.DocsURLs.Layers);
menu.ShowAsContext();
}
}
/************************************************************************************************************************/
private readonly AnimancerLayerDrawer OrphanStatesDrawer = new();
private void DoOrphanStatesGUI(AnimancerGraph graph)
{
var states = OrphanStatesDrawer.ActiveStates;
states.Clear();
foreach (var state in graph.States)
if (state.Parent == null)
states.Add(state);
if (states.Count > 0)
{
ApplySortStatesByName(states);
OrphanStatesDrawer.DoStatesGUI("Orphans", states);
}
}
/************************************************************************************************************************/
private void DoLayerWeightWarningGUI(IAnimancerComponent target)
{
if (_LayerCount == 0)
{
EditorGUILayout.HelpBox(
"No layers have been created, which likely means no animations have been played yet.",
MessageType.Warning);
if (GUILayout.Button("Create Base Layer"))
target.Graph.Layers.Count = 1;
return;
}
if (!target.gameObject.activeInHierarchy ||
!target.enabled ||
(target.Animator != null && target.Animator.runtimeAnimatorController != null))
return;
if (_LayerCount == 1)
{
var layer = LayerDrawers[0].Value;
if (layer.Weight == 0)
EditorGUILayout.HelpBox(
layer + " is at 0 weight, which likely means no animations have been played yet.",
MessageType.Warning);
return;
}
for (int i = 0; i < _LayerCount; i++)
{
var layer = LayerDrawers[i].Value;
if (layer.Weight == 1 &&
!layer.IsAdditive &&
layer._Mask == null &&
Mathf.Approximately(layer.GetTotalChildWeight(), 1))
return;
}
EditorGUILayout.HelpBox(
"There are no Override layers at weight 1, which will likely give undesirable results." +
" Click here for more information.",
MessageType.Warning);
if (TryUseClickEventInLastRect())
EditorUtility.OpenWithDefaultApp(Strings.DocsURLs.Layers + "#blending");
}
/************************************************************************************************************************/
private void DoMultipleAnimationSystemWarningGUI(IAnimancerComponent target)
{
const string OnlyOneSystemWarning =
"This is not supported. Each object can only be controlled by one system at a time.";
using (ListPool.Instance.Acquire(out var animancers))
{
target.gameObject.GetComponents(animancers);
if (animancers.Count > 1)
{
for (int i = 0; i < animancers.Count; i++)
{
var other = animancers[i];
if (other != target && other.Animator == target.Animator)
{
EditorGUILayout.HelpBox(
$"There are multiple {nameof(IAnimancerComponent)}s trying to control the target" +
$" {nameof(Animator)}. {OnlyOneSystemWarning}",
MessageType.Warning);
break;
}
}
}
}
if (target.Animator.TryGetComponent(out _))
{
EditorGUILayout.HelpBox(
$"There is a Legacy {nameof(Animation)} component on the same object as the target" +
$" {nameof(Animator)}. {OnlyOneSystemWarning}",
MessageType.Warning);
}
}
/************************************************************************************************************************/
private static readonly BoolPref
ArePreUpdatablesExpanded = new(KeyPrefix + nameof(ArePreUpdatablesExpanded), false),
ArePostUpdatablesExpanded = new(KeyPrefix + nameof(ArePostUpdatablesExpanded), false),
AreDisposablesExpanded = new(KeyPrefix + nameof(AreDisposablesExpanded), false);
/// Draws a box describing the internal details of the `graph`.
private void DoInternalDetailsGUI(AnimancerGraph graph)
{
EditorGUI.indentLevel++;
DoGroupDetailsGUI(graph.PreUpdatables, "Pre-Updatables", ArePreUpdatablesExpanded);
DoGroupDetailsGUI(graph.PostUpdatables, "Post-Updatables", ArePostUpdatablesExpanded);
DoGroupDetailsGUI(graph.Disposables, "Disposables", AreDisposablesExpanded);
EditorGUI.indentLevel--;
}
/// Draws the `items`.
private static void DoGroupDetailsGUI(IReadOnlyList items, string groupName, BoolPref isExpanded)
{
var count = items.Count;
isExpanded.Value = DoLabelFoldoutFieldGUI(groupName, count.ToStringCached(), isExpanded);
EditorGUI.indentLevel++;
if (isExpanded)
for (int i = 0; i < count; i++)
DoDetailsGUI(items[i]);
EditorGUI.indentLevel--;
}
/// Draws the details of the `item`.
private static void DoDetailsGUI(object item)
{
if (item is AnimancerNode node)
{
var area = LayoutSingleLineRect(SpacingMode.Before);
area = EditorGUI.IndentedRect(area);
var field = new FastObjectField();
field.Set(node, node.GetPath(), FastObjectField.GetIcon(node));
field.Draw(area);
return;
}
var gui = CustomGUIFactory.GetOrCreateForObject(item);
if (gui != null)
{
gui.DoGUI();
return;
}
EditorGUILayout.LabelField(item.ToString());
}
/************************************************************************************************************************/
#region Main Object Lookup
/************************************************************************************************************************/
private readonly Dictionary