// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik // #if UNITY_EDITOR && UNITY_IMGUI using System; using UnityEditor; using UnityEngine; using static Animancer.Editor.AnimancerGraphDrawer; using static Animancer.Editor.AnimancerGUI; using static Animancer.Editor.AnimancerStateDrawerColors; using Object = UnityEngine.Object; namespace Animancer.Editor { /// [Editor-Only] Draws the Inspector GUI for an . /// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerStateDrawer_1 [CustomGUI(typeof(AnimancerState))] public class AnimancerStateDrawer : AnimancerNodeDrawer where T : AnimancerState { /************************************************************************************************************************/ /// protected override bool AutoNormalizeSiblingWeights => AutoNormalizeWeights && typeof(T) != typeof(ManualMixerState); /************************************************************************************************************************/ private FastObjectField _NameField; private FastObjectField _MainObjectField; /// Draws the state's main label with a bar to indicate its current time. protected override void DoLabelGUI(Rect area) { area = area.Expand(StandardSpacing, 0); var wholeArea = area; var effectiveWeight = Value.EffectiveWeight; var highlightArea = default(Rect); var isRepaint = Event.current.type == EventType.Repaint; if (isRepaint) { EditorGUI.DrawRect(wholeArea, HeaderBackgroundColor); highlightArea = DoTimeHighlightBarGUI(wholeArea, effectiveWeight); DoEventsGUI(wholeArea); ObjectHighlightGUI.Draw(wholeArea, Value); } DoWeightLabel(ref area, Value.Weight, effectiveWeight); AnimationBindings.DoBindingMatchGUI(ref area, Value); HandleLabelClick(wholeArea); area = EditorGUI.IndentedRect(area); var name = Value.DebugName ?? Value.Key; var mainObject = Value.MainObject; if (mainObject == null) { var value = name ?? Value; var drawPing = value != Value; _NameField.Draw(area, value, drawPing); } else if (ReferenceEquals(name, mainObject) || (name is Object nameObject && nameObject == mainObject) || (name is ITransition && Current != null && !Current.IsMainObjectUsedMultipleTimes(mainObject))) { _MainObjectField.Draw(area, mainObject, false); } else { if (name != null) { var nameArea = StealFromLeft(ref area, EditorGUIUtility.labelWidth - IndentSize); _NameField.Draw(nameArea, name, true); } _MainObjectField.Draw(area, mainObject, false); } if (isRepaint) DoDetailLinesGUI(wholeArea, highlightArea, effectiveWeight); } /************************************************************************************************************************/ /// Draws a progress bar to show the animation time. public Rect DoTimeHighlightBarGUI(Rect area, float effectiveWeight) => DoTimeHighlightBarGUI( area, Value.IsPlaying, effectiveWeight, Value.Time, Value.EffectiveSpeed, Value.Length, Value.IsLooping); /// Draws a progress bar to show the animation time. public static Rect DoTimeHighlightBarGUI( Rect area, bool isPlaying, float effectiveWeight, float time, float speed, float length, bool isLooping) { if (ScaleTimeBarByWeight) { var height = area.height; area.height *= Mathf.Clamp01(effectiveWeight); area.y += height - area.height; } var color = isPlaying ? PlayingBarColor : PausedBarColor; var wrappedTime = GetWrappedTime(time, length, isLooping); if (length == 0) { if (time == 0) return area; } else { if (speed >= 0 || time == 0) { area.width *= Mathf.Clamp01(wrappedTime / length); } else { var xMax = area.xMax; area.x += area.width * Mathf.Clamp01(wrappedTime / length); area.x = Mathf.Floor(area.x); area.xMax = xMax; } } EditorGUI.DrawRect(area, color); return area; } /************************************************************************************************************************/ /// Draws lines for the current weight, time, and fade destination. public void DoDetailLinesGUI( Rect totalArea, Rect highlightArea, float effectiveWeight) { var length = Value.Length; var speed = Value.Speed; var speedSign = speed >= 0 ? 1 : -1; var currentX = speed >= 0 ? highlightArea.xMax : highlightArea.xMin - 1; var forwardEdge = speed >= 0 ? totalArea.xMax : totalArea.xMin - 1; var color = FadeLineColor; color.a = color.a * effectiveWeight * 0.75f + 0.25f; if (Value.Time != 0 || Value.IsPlaying || Value.Weight != 0) { EditorGUI.DrawRect( new(highlightArea.x, highlightArea.yMin, highlightArea.width, 1), color); if (length == 0) return; EditorGUI.DrawRect( new(currentX - speedSign, totalArea.y, 1, totalArea.height), color); } else if (length == 0) { return; } if (!Value.IsPlaying) return; var fade = Value.FadeGroup; if (fade == null || !fade.IsValid) return; var currentCorner = new Vector2(currentX, highlightArea.yMin); var targetWeight = Value.TargetWeight; var remainingFadeDuration = fade.RemainingFadeDuration; var targetCorner = new Vector2( currentCorner.x + speed * remainingFadeDuration / Value.Length * totalArea.width, Mathf.Lerp(totalArea.yMax, totalArea.yMin, targetWeight)); var intersect = Mathf.InverseLerp(currentCorner.x, targetCorner.x, forwardEdge); var end = Vector2.LerpUnclamped(currentCorner, targetCorner, intersect); BeginTriangles(color); DrawLineBatched( currentCorner, end, 1); if (intersect < 1 && Value.IsLooping) { end.x -= speedSign * totalArea.width; targetCorner.x -= speedSign * totalArea.width; DrawLineBatched( end, targetCorner, 1); } EndTriangles(); } /************************************************************************************************************************/ /// Draws marks on the timeline for each event. private void DoEventsGUI(Rect area) { if (!ShowEvents) return; DoAnimancerEventsGUI(area); DoAnimationEventsGUI(area); } /// Draws marks on the timeline for each Animancer Event. private void DoAnimancerEventsGUI(Rect area) { var events = Value.SharedEvents; if (events == null) return; for (int i = 0; i < events.Count; i++) DoEventTick(area, events[i].normalizedTime); if (events.OnEnd != null) DoEventTick(area, events.GetRealNormalizedEndTime(Value.Speed)); } /// Draws marks on the timeline for each Animation Event. private void DoAnimationEventsGUI(Rect area) { var clip = Value.MainObject as AnimationClip; if (clip == null) return; var inverseLength = 1f / Value.Length; var events = clip.GetCachedEvents(); for (int i = 0; i < events.Length; i++) DoEventTick(area, events[i].time * inverseLength); } /// Draws a mark on the timeline for an event. private static void DoEventTick(Rect area, float normalizedTime) { if (normalizedTime >= 0 && normalizedTime <= 1) { var x = area.x + area.width * normalizedTime; var eventArea = new Rect(x - 1, area.y, 2, area.height * 0.3f); EditorGUI.DrawRect(eventArea, EventTickColor); } } /************************************************************************************************************************/ /// Handles clicks on the label area. private void HandleLabelClick(Rect area) { var currentEvent = Event.current; if (currentEvent.type != EventType.MouseUp || currentEvent.button != 0 || !area.Contains(currentEvent.mousePosition)) return; currentEvent.Use(0); if (currentEvent.control) FadeInTarget(); else ToggleExpanded(currentEvent.alt); } /// Fades in the target state (or its parent state if not directly attached to a layer). private void FadeInTarget() { Value.Graph.UnpauseGraph(); AnimancerState target = Value; while (target != null) { var parent = target.Parent; if (parent is AnimancerLayer layer) { var fadeDuration = target.CalculateEditorFadeDuration( AnimancerGraph.DefaultFadeDuration); layer.Play(target, fadeDuration); return; } target = parent as AnimancerState; } } /// Toggles the target's details between expanded and collapsed. private void ToggleExpanded(bool toggleSiblings) { IsExpanded = !IsExpanded; if (toggleSiblings) { var parent = Value.Parent; var childCount = parent.ChildCount; for (int i = 0; i < childCount; i++) parent.GetChildNode(i)._IsInspectorExpanded = IsExpanded; } } /************************************************************************************************************************/ /// protected override void DoFoldoutGUI(Rect area) { var hierarchyMode = EditorGUIUtility.hierarchyMode; EditorGUIUtility.hierarchyMode = true; IsExpanded = EditorGUI.Foldout(area, IsExpanded, GUIContent.none, true); EditorGUIUtility.hierarchyMode = hierarchyMode; } /************************************************************************************************************************/ /// /// Gets the current . /// If the state is looping, the value is modulo by the . /// private float GetWrappedTime(out float length) => GetWrappedTime(Value.Time, length = Value.Length, Value.IsLooping); /// /// Gets the current . /// If the state is looping, the value is modulo by the . /// private static float GetWrappedTime(float time, float length, bool isLooping) { var wrappedTime = time; if (isLooping) { wrappedTime = AnimancerUtilities.Wrap(wrappedTime, length); if (wrappedTime == 0 && time != 0) wrappedTime = length; } return wrappedTime; } /************************************************************************************************************************/ private FastObjectField _KeyField; private FastObjectField _OwnerField; /************************************************************************************************************************/ /// The display name of the field. public virtual string MainObjectName => "Main Object"; /************************************************************************************************************************/ /// protected override void DoDetailsGUI() { base.DoDetailsGUI(); if (!IsExpanded) return; EditorGUI.indentLevel++; DoOptionalReferenceGUI(ref _KeyField, "Key", Value.Key); DoOptionalReferenceGUI(ref _OwnerField, "Owner", Value.Owner); var mainObject = Value.MainObject; if (mainObject != null) { var mainObjectType = Value.MainObjectType ?? typeof(Object); EditorGUI.BeginChangeCheck(); var area = LayoutSingleLineRect(SpacingMode.Before); mainObject = EditorGUI.ObjectField( area, MainObjectName, mainObject, mainObjectType, true); if (EditorGUI.EndChangeCheck()) Value.MainObject = mainObject; } DoTimeSliderGUI(); DoNodeDetailsGUI(); DoOnEndGUI(); EditorGUI.indentLevel--; } /************************************************************************************************************************/ /// Draws a `reference` if it isn't null. private static void DoOptionalReferenceGUI(ref FastObjectField field, string label, object reference) { if (reference != null) field.Draw(LayoutSingleLineRect(SpacingMode.Before), label, reference); } /************************************************************************************************************************/ /// Draws a slider for controlling the current . private void DoTimeSliderGUI() { if (Value.Length <= 0) return; var time = GetWrappedTime(out var length); if (length == 0) return; var area = LayoutSingleLineRect(SpacingMode.Before); var normalized = DoNormalizedTimeToggle(ref area); string label; float max; if (normalized) { label = "Normalized Time"; time /= length; max = 1; } else { label = "Time"; max = length; } DoLoopCounterGUI(ref area, length); EditorGUI.BeginChangeCheck(); label = BeginTightLabel(label); time = EditorGUI.Slider(area, label, time, 0, max); EndTightLabel(); if (TryUseClickEvent(area, 2)) time = 0; if (EditorGUI.EndChangeCheck()) { if (normalized) Value.NormalizedTime = time; else Value.Time = time; } } /************************************************************************************************************************/ private static bool DoNormalizedTimeToggle(ref Rect area) { using (var label = PooledGUIContent.Acquire("N")) { var style = MiniButtonStyle; var width = style.CalculateWidth(label); var toggleArea = StealFromRight(ref area, width); UseNormalizedTimeSliders.Value = GUI.Toggle(toggleArea, UseNormalizedTimeSliders, label, style); } return UseNormalizedTimeSliders; } /************************************************************************************************************************/ private static ConversionCache _LoopCounterCache; private void DoLoopCounterGUI(ref Rect area, float length) { _LoopCounterCache ??= new(x => $"x{x}"); string label; var normalizedTime = Value.Time / length; if (float.IsNaN(normalizedTime)) { label = "NaN"; } else { var loops = Mathf.FloorToInt(Value.Time / length); label = _LoopCounterCache.Convert(loops); } var width = CalculateLabelWidth(label); var labelArea = StealFromRight(ref area, width); GUI.Label(labelArea, label); } /************************************************************************************************************************/ private void DoOnEndGUI() { var events = Value.SharedEvents; if (events == null) return; var drawer = EventSequenceDrawer.Get(events); var area = LayoutRect(drawer.CalculateHeight(events), SpacingMode.Before); using (var label = PooledGUIContent.Acquire("Events")) drawer.DoGUI(ref area, events, label); } /************************************************************************************************************************/ #region Context Menu /************************************************************************************************************************/ /// protected override void PopulateContextMenu(GenericMenu menu) { AddContextMenuFunctions(menu); menu.AddFunction("Play", !Value.IsPlaying || Value.Weight != 1, () => { AnimancerState.SkipNextExpectFade(); Value.Graph.UnpauseGraph(); Value.Graph.Layers[0].Play(Value); }); AnimancerEditorUtilities.AddFadeFunction(menu, "Cross Fade (Ctrl + Click)", Value.Weight != 1, Value, duration => { AnimancerState.SkipNextExpectFade(); Value.Graph.UnpauseGraph(); Value.Graph.Layers[0].Play(Value, duration); }); menu.AddSeparator(""); menu.AddItem(new("Destroy State"), false, () => Value.Destroy()); menu.AddSeparator(""); AddDisplayOptions(menu); AnimancerEditorUtilities.AddDocumentationLink( menu, "State Documentation", Strings.DocsURLs.States); } /************************************************************************************************************************/ /// Adds the details of this state to the `menu`. protected virtual void AddContextMenuFunctions(GenericMenu menu) { menu.AddDisabledItem(new($"{DetailsPrefix}{nameof(Value.Key)}: {AnimancerUtilities.ToStringOrNull(Value.Key)}")); menu.AddDisabledItem(new($"{DetailsPrefix}{nameof(Value.Owner)}: {AnimancerUtilities.ToStringOrNull(Value.Owner)}")); var length = Value.Length; if (!float.IsNaN(length)) menu.AddDisabledItem(new($"{DetailsPrefix}{nameof(Value.Length)}: {length}")); menu.AddDisabledItem(new($"{DetailsPrefix}Playable Path: {Value.GetPath()}")); var mainAsset = Value.MainObject; if (mainAsset != null) { var assetPath = AssetDatabase.GetAssetPath(mainAsset); if (assetPath != null) menu.AddDisabledItem(new($"{DetailsPrefix}Asset Path: {assetPath.Replace("/", "->")}")); } var events = Value.SharedEvents; if (events != null) { for (int i = 0; i < events.Count; i++) { var index = i; var name = events.GetName(i); AddEventFunctions( menu, name.IsNullOrEmpty() ? "Event " + index : name, name, events[index], () => events.SetCallback(index, AnimancerEvent.InvokeBoundCallback), () => events.Remove(index)); } AddEventFunctions( menu, "End Event", default, events.EndEvent, () => events.EndEvent = new(float.NaN, null), null); } } /************************************************************************************************************************/ private void AddEventFunctions( GenericMenu menu, string displayName, StringReference name, AnimancerEvent animancerEvent, GenericMenu.MenuFunction clearEvent, GenericMenu.MenuFunction removeEvent) { displayName = $"Events/{displayName}/"; menu.AddDisabledItem(new($"{displayName}{nameof(AnimancerState.NormalizedTime)}: {animancerEvent.normalizedTime}")); bool canInvoke; if (animancerEvent.callback == null) { menu.AddDisabledItem(new(displayName + "Callback: null")); canInvoke = false; } else if (animancerEvent.callback == AnimancerEvent.DummyCallback) { menu.AddDisabledItem(new(displayName + "Callback: Dummy")); canInvoke = false; } else { var label = displayName + (animancerEvent.callback.Target != null ? ("Target: " + animancerEvent.callback.Target) : "Target: null"); var targetObject = animancerEvent.callback.Target as Object; menu.AddFunction(label, targetObject != null, () => Selection.activeObject = targetObject); menu.AddDisabledItem(new( $"{displayName}Declaring Type: {animancerEvent.callback.Method.DeclaringType.GetNameCS()}")); menu.AddDisabledItem(new( $"{displayName}Method: {animancerEvent.callback.Method}")); canInvoke = true; } if (clearEvent != null) menu.AddFunction(displayName + "Clear", canInvoke || !float.IsNaN(animancerEvent.normalizedTime), clearEvent); if (removeEvent != null) menu.AddFunction(displayName + "Remove", true, removeEvent); menu.AddFunction(displayName + "Invoke", canInvoke, () => animancerEvent.DelayInvoke(name, Value)); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } /// [Editor-Only] Colors used by . /// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerStateDrawerColors public static class AnimancerStateDrawerColors { /************************************************************************************************************************/ /// Colors used by this system. public static readonly Color HeaderBackgroundColor = Grey(0.35f, 0.35f), PlayingBarColor = new(0.15f, 0.7f, 0.15f, 0.4f),// Green = Playing. PausedBarColor = new(0.7f, 0.7f, 0.15f, 0.4f),// Yelow = Paused. FadeLineColor = new(0.3f, 1, 0.3f, 1); /// Colors used by this system. public static Color EventTickColor => Grey(EditorGUIUtility.isProSkin ? 0.8f : 0.2f, 0.8f); /************************************************************************************************************************/ } } #endif