// 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 { /// [Editor-Only] /// A custom Inspector for an which sorts and exposes some of its internal values. /// /// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerLayerDrawer /// [CustomGUI(typeof(AnimancerLayer))] public class AnimancerLayerDrawer : AnimancerNodeDrawer { /************************************************************************************************************************/ /// The states in the target layer which have non-zero . public readonly List ActiveStates = new(); /// The states in the target layer which have zero . public readonly List InactiveStates = new(); /************************************************************************************************************************/ #region Gathering /************************************************************************************************************************/ /// Initializes an editor in the list for each layer in the `graph`. /// /// 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. /// internal static void GatherLayerEditors( AnimancerGraph graph, List 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]); } } /************************************************************************************************************************/ /// /// Sets the target `layer` and sorts its states and their keys into the active/inactive lists. /// 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); } /************************************************************************************************************************/ /// /// Sorts any entries that use another state as their key to come right after that state. /// See . /// private static void SortAndGatherKeys(List 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 /************************************************************************************************************************/ /// Draws the layer's name and weight. 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; } /************************************************************************************************************************/ /// The number of pixels of indentation required to fit the foldout arrow. const float FoldoutIndent = 12; /// 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; } /************************************************************************************************************************/ /// 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(); } /************************************************************************************************************************/ /// /// Draws controls for and . /// 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); } } /************************************************************************************************************************/ /// Are all the target's states fading out to 0? 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; } /************************************************************************************************************************/ /// Draws all `states` in the given list. public void DoStatesGUI(string label, List 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--; } /************************************************************************************************************************/ /// Cached Inspectors that have already been created for states. private readonly Dictionary StateInspectors = new(); /// Draws the Inspector for the given `state`. private void DoStateGUI(AnimancerState state) { if (!StateInspectors.TryGetValue(state, out var inspector)) { inspector = CustomGUIFactory.GetOrCreateForObject(state); StateInspectors.Add(state, inspector); } inspector?.DoGUI(); DoChildStatesGUI(state); } /************************************************************************************************************************/ /// Draws all child states of the `state`. private void DoChildStatesGUI(AnimancerState state) { if (!state._IsInspectorExpanded) return; EditorGUI.indentLevel++; foreach (var child in state) if (child != null) DoStateGUI(child); EditorGUI.indentLevel--; } /************************************************************************************************************************/ /// 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(); } /************************************************************************************************************************/ /// public override void DoGUI() { if (!Value.IsValid()) return; base.DoGUI(); var area = GUILayoutUtility.GetLastRect(); HandleDragAndDropToPlay(area, Value); } /************************************************************************************************************************/ /// /// If s or s are dropped inside the `dropArea`, /// this method creates a new state in the `target` for each animation. /// public static void HandleDragAndDropToPlay(Rect area, object layerOrGraph) { if (layerOrGraph == null) return; _DragAndDropPlayTarget = layerOrGraph; _DragAndDropPlayHandler ??= HandleDragAndDropToPlay; _DragAndDropPlayHandler.Handle(area); _DragAndDropPlayTarget = null; } private static DragAndDropHandler _DragAndDropPlayHandler; private static object _DragAndDropPlayTarget; private static AnimancerLayer DragAndDropPlayTargetLayer => _DragAndDropPlayTarget as AnimancerLayer ?? (_DragAndDropPlayTarget is AnimancerGraph graph ? graph.Layers[0] : null); /// Handles drag and drop events to play animations and transitions. 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.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 /************************************************************************************************************************/ /// 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 condition) { foreach (var state in Value) { if (condition(state)) return true; } return false; } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } } #endif