chore: initial commit

This commit is contained in:
2026-05-08 11:04:00 +08:00
commit f55d2a57c3
6278 changed files with 866081 additions and 0 deletions

View File

@@ -0,0 +1,535 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 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 && AnimancerGraphDrawer.ShowInactiveStates))
{
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

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 5c7bae7cf59ceb14cbdd08bea1a62717
timeCreated: 1516751545
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,416 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using System;
using UnityEditor;
using UnityEngine;
using UnityEngine.Playables;
using static Animancer.Editor.AnimancerGUI;
using Object = UnityEngine.Object;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] Draws the Inspector GUI for an <see cref="AnimancerNode"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerNodeDrawer_1
///
public abstract class AnimancerNodeDrawer<T> : CustomGUI<T>
where T : AnimancerNode
{
/************************************************************************************************************************/
/// <summary>Extra padding for the left side of the labels.</summary>
public const float ExtraLeftPadding = 3;
/************************************************************************************************************************/
/// <summary>Should the target node's details be expanded in the Inspector?</summary>
public ref bool IsExpanded
=> ref Value._IsInspectorExpanded;
/************************************************************************************************************************/
/// <inheritdoc/>
public override void DoGUI()
{
if (!Value.IsValid())
return;
GUILayout.BeginVertical();
{
DoHeaderGUI();
DoDetailsGUI();
}
GUILayout.EndVertical();
if (TryUseClickEvent(GUILayoutUtility.GetLastRect(), 1))
OpenContextMenu();
}
/************************************************************************************************************************/
/// <summary>Draws the name and other details of the <see cref="CustomGUI{T}.Value"/> in the GUI.</summary>
protected virtual void DoHeaderGUI()
{
var area = LayoutSingleLineRect(SpacingMode.Before);
DoLabelGUI(area);
DoFoldoutGUI(area);
}
/************************************************************************************************************************/
/// <summary>
/// Draws a field for the <see cref="AnimancerState.MainObject"/> if it has one, otherwise just a simple text
/// label.
/// </summary>
protected abstract void DoLabelGUI(Rect area);
/// <summary>Draws a foldout arrow to expand/collapse the node details.</summary>
protected abstract void DoFoldoutGUI(Rect area);
/************************************************************************************************************************/
private FastObjectField _DebugNameField;
/// <summary>Draws the details of the <see cref="CustomGUI{T}.Value"/>.</summary>
protected virtual void DoDetailsGUI()
{
if (!IsExpanded)
return;
var debugName = Value.DebugName;
if (debugName == null)
return;
var area = LayoutSingleLineRect(SpacingMode.Before);
area = EditorGUI.IndentedRect(area);
_DebugNameField.Draw(area, "Debug Name", debugName);
}
/************************************************************************************************************************/
private static readonly int FloatFieldHash = "EditorTextField".GetHashCode();
/// <summary>
/// Draws controls for <see cref="AnimancerState.IsPlaying"/>, <see cref="AnimancerNodeBase.Speed"/>, and
/// <see cref="AnimancerNode.Weight"/>.
/// </summary>
protected void DoNodeDetailsGUI()
{
var area = LayoutSingleLineRect(SpacingMode.Before);
area.xMin += EditorGUI.indentLevel * IndentSize + ExtraLeftPadding;
var xMin = area.xMin;
var labelWidth = EditorGUIUtility.labelWidth;
var indentLevel = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
// Is Playing.
if (Value is AnimancerState state)
{
var buttonArea = StealFromLeft(ref area, LineHeight, StandardSpacing);
state.IsPlaying = DoPlayPauseToggle(buttonArea, state.IsPlaying);
}
SplitHorizontally(area, "Speed", "Weight",
out var speedWidth,
out var weightWidth,
out var speedRect,
out var weightRect);
// Speed.
EditorGUIUtility.labelWidth = speedWidth;
EditorGUI.BeginChangeCheck();
var speed = EditorGUI.FloatField(speedRect, "Speed", Value.Speed);
if (EditorGUI.EndChangeCheck())
Value.Speed = speed;
if (TryUseClickEvent(speedRect, 2))
Value.Speed = Value.Speed != 1 ? 1 : 0;
// Weight.
EditorGUIUtility.labelWidth = weightWidth;
EditorGUI.BeginChangeCheck();
var weight = EditorGUI.FloatField(weightRect, "Weight", Value.Weight);
if (EditorGUI.EndChangeCheck())
SetWeight(Mathf.Max(weight, 0));
if (TryUseClickEvent(weightRect, 2))
SetWeight(Value.Weight != 1 ? 1 : 0);
// Real Speed.
// Mixer Synchronization changes the internal Playable Speed without setting the State Speed.
speed = (float)Value._Playable.GetSpeed();
if (Value.Speed != speed)
{
using (new EditorGUI.DisabledScope(true))
{
area = LayoutSingleLineRect(SpacingMode.Before);
area.xMin = xMin;
var label = BeginTightLabel("Real Speed");
EditorGUIUtility.labelWidth = CalculateLabelWidth(label);
EditorGUI.FloatField(area, label, speed);
EndTightLabel();
}
}
else// Add a dummy ID so that subsequent IDs don't change when the Real Speed appears or disappears.
{
GUIUtility.GetControlID(FloatFieldHash, FocusType.Keyboard);
}
EditorGUI.indentLevel = indentLevel;
EditorGUIUtility.labelWidth = labelWidth;
DoFadeDetailsGUI();
}
/************************************************************************************************************************/
/// <summary>Indicates whether changing the <see cref="AnimancerNode.Weight"/> should normalize its siblings.</summary>
protected virtual bool AutoNormalizeSiblingWeights
=> false;
private void SetWeight(float weight)
{
if (weight < 0 ||
weight > 1 ||
Mathf.Approximately(Value.Weight, 1) ||
!AutoNormalizeSiblingWeights)
goto JustSetWeight;
var parent = Value.Parent;
if (parent == null)
goto JustSetWeight;
var totalWeight = 0f;
var siblingCount = parent.ChildCount;
for (int i = 0; i < siblingCount; i++)
{
var sibling = parent.GetChildNode(i);
if (sibling.IsValid())
totalWeight += sibling.Weight;
}
// If the weights weren't previously normalized, don't normalize them now.
if (!Mathf.Approximately(totalWeight, 1))
goto JustSetWeight;
var siblingWeightMultiplier = (totalWeight - weight) / (totalWeight - Value.Weight);
for (int i = 0; i < siblingCount; i++)
{
var sibling = parent.GetChildNode(i);
if (sibling != Value && sibling.IsValid())
sibling.Weight *= siblingWeightMultiplier;
}
JustSetWeight:
Value.Weight = weight;
}
/************************************************************************************************************************/
private float
_FadeDuration = float.NaN,
_TargetWeight = float.NaN;
/// <summary>
/// Draws the <see cref="AnimancerNode.FadeSpeed"/>
/// and <see cref="AnimancerNode.TargetWeight"/>.
/// </summary>
private void DoFadeDetailsGUI()
{
var area = LayoutSingleLineRect(SpacingMode.Before);
area = EditorGUI.IndentedRect(area);
area.xMin += ExtraLeftPadding;
var durationLabel = "Fade Duration";
var targetLabel = "Target Weight";
SplitHorizontally(
area,
durationLabel,
targetLabel,
out var durationWidth,
out var weightWidth,
out var durationRect,
out var weightRect);
var labelWidth = EditorGUIUtility.labelWidth;
var indentLevel = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
EditorGUI.BeginChangeCheck();
var fade = Value.FadeGroup;
var fadeDuration = DoFadeDurationGUI(durationWidth, durationRect, durationLabel, fade);
var targetWeight = DoTargetWeightGUI(weightWidth, weightRect, targetLabel, fade);
if (EditorGUI.EndChangeCheck())
SetFade(targetWeight, fadeDuration);
EditorGUI.indentLevel = indentLevel;
EditorGUIUtility.labelWidth = labelWidth;
}
/************************************************************************************************************************/
private float DoFadeDurationGUI(
float labelWidth,
Rect area,
string label,
FadeGroup fade)
{
EditorGUIUtility.labelWidth = labelWidth;
var fadeDuration = fade != null ? fade.FadeDuration : _FadeDuration;
fadeDuration = EditorGUI.DelayedFloatField(area, label, fadeDuration);
if (fadeDuration > 0)
{
}
else// NaN or Negative.
{
fadeDuration = _FadeDuration = float.NaN;
}
if (TryUseClickEvent(area, 2))
{
var defaultFadeDuration = AnimancerGraph.DefaultFadeDuration;
if (fadeDuration != 0 || defaultFadeDuration == 0)
{
fadeDuration = 0;
}
else
{
var fadeDistance = Math.Abs(Value.Weight - Value.TargetWeight);
if (fadeDistance != 0)
{
fadeDuration = fadeDistance / defaultFadeDuration;
}
else
{
fadeDuration = defaultFadeDuration;
}
}
}
return fadeDuration;
}
/************************************************************************************************************************/
private float DoTargetWeightGUI(
float labelWidth,
Rect area,
string label,
FadeGroup fade)
{
EditorGUIUtility.labelWidth = labelWidth;
var targetWeight = fade != null
? fade.TargetWeight
: _TargetWeight.IsFinite()
? _TargetWeight
: Value.Weight;
targetWeight = EditorGUI.DelayedFloatField(area, label, targetWeight);
if (targetWeight >= 0)
{
}
else// NaN or Negative.
{
targetWeight = _TargetWeight = float.NaN;
}
if (TryUseClickEvent(area, 2))
{
if (targetWeight != Value.Weight)
targetWeight = Value.Weight;
else if (targetWeight != 1)
targetWeight = 1;
else
targetWeight = 0;
}
return targetWeight;
}
/************************************************************************************************************************/
/// <summary>Starts a fade or changes the details of an existing one.</summary>
private void SetFade(float targetWeight, float fadeDuration)
{
_TargetWeight = targetWeight;
_FadeDuration = fadeDuration;
if (!targetWeight.IsFinite() ||
!fadeDuration.IsFinite() ||
targetWeight == Value.Weight ||
fadeDuration <= 0)
return;
// If it's a state attached to a layer, start a proper cross fade.
if (Value is AnimancerState state &&
state.Parent is AnimancerLayer layer)
{
layer.Play(state, fadeDuration, FadeMode.FixedDuration);
// That might not have started a fade if the state was already playing,
// So just continue to verify its details.
}
var fade = Value.FadeGroup;
if (fade != null && fade.FadeIn.Node == Value)
{
fade.TargetWeight = targetWeight;
fade.FadeDuration = fadeDuration;
return;
}
Value.StartFade(targetWeight, fadeDuration);
}
/************************************************************************************************************************/
#region Context Menu
/************************************************************************************************************************/
/// <summary>
/// The menu label prefix used for details about the <see cref="CustomGUI{T}.Value"/>.
/// </summary>
protected const string DetailsPrefix = "Details/";
/// <summary>
/// Checks if the current event is a context menu click within the `clickArea` and opens a context menu with various
/// functions for the <see cref="CustomGUI{T}.Value"/>.
/// </summary>
protected void OpenContextMenu()
{
var menu = new GenericMenu();
menu.AddDisabledItem(new(Value.ToString()));
PopulateContextMenu(menu);
menu.AddItem(new(DetailsPrefix + "Log Details"), false,
() => Debug.Log(Value.GetDescription(), Value.Graph?.Component as Object));
menu.AddItem(new(DetailsPrefix + "Log Details Of Everything"), false,
() => Debug.Log(Value.Graph.GetDescription(), Value.Graph?.Component as Object));
AnimancerGraphDrawer.AddPlayableGraphVisualizerFunction(menu, DetailsPrefix, Value.Graph._PlayableGraph);
menu.ShowAsContext();
}
/// <summary>Adds functions relevant to the <see cref="CustomGUI{T}.Value"/>.</summary>
protected abstract void PopulateContextMenu(GenericMenu menu);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: abdda148dff930e498ac8f16c742243e
timeCreated: 1516751545
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,716 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 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
{
/// <summary>[Editor-Only] Draws the Inspector GUI for an <see cref="AnimancerState"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerStateDrawer_1
[CustomGUI(typeof(AnimancerState))]
public class AnimancerStateDrawer<T> : AnimancerNodeDrawer<T>
where T : AnimancerState
{
/************************************************************************************************************************/
/// <inheritdoc/>
protected override bool AutoNormalizeSiblingWeights
=> AutoNormalizeWeights
&& typeof(T) != typeof(ManualMixerState);
/************************************************************************************************************************/
private FastObjectField _NameField;
private FastObjectField _MainObjectField;
/// <summary>Draws the state's main label with a bar to indicate its current time.</summary>
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);
}
/************************************************************************************************************************/
/// <summary>Draws a progress bar to show the animation time.</summary>
public Rect DoTimeHighlightBarGUI(Rect area, float effectiveWeight)
=> DoTimeHighlightBarGUI(
area,
Value.IsPlaying,
effectiveWeight,
Value.Time,
Value.EffectiveSpeed,
Value.Length,
Value.IsLooping);
/// <summary>Draws a progress bar to show the animation time.</summary>
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;
}
/************************************************************************************************************************/
/// <summary>Draws lines for the current weight, time, and fade destination.</summary>
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();
}
/************************************************************************************************************************/
/// <summary>Draws marks on the timeline for each event.</summary>
private void DoEventsGUI(Rect area)
{
if (!ShowEvents)
return;
DoAnimancerEventsGUI(area);
DoAnimationEventsGUI(area);
}
/// <summary>Draws marks on the timeline for each Animancer Event.</summary>
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));
}
/// <summary>Draws marks on the timeline for each Animation Event.</summary>
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);
}
/// <summary>Draws a mark on the timeline for an event.</summary>
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);
}
}
/************************************************************************************************************************/
/// <summary>Handles clicks on the label area.</summary>
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);
}
/// <summary>Fades in the target state (or its parent state if not directly attached to a layer).</summary>
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;
}
}
/// <summary>Toggles the target's details between expanded and collapsed.</summary>
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;
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void DoFoldoutGUI(Rect area)
{
var hierarchyMode = EditorGUIUtility.hierarchyMode;
EditorGUIUtility.hierarchyMode = true;
IsExpanded = EditorGUI.Foldout(area, IsExpanded, GUIContent.none, true);
EditorGUIUtility.hierarchyMode = hierarchyMode;
}
/************************************************************************************************************************/
/// <summary>
/// Gets the current <see cref="AnimancerState.Time"/>.
/// If the state is looping, the value is modulo by the <see cref="AnimancerState.Length"/>.
/// </summary>
private float GetWrappedTime(out float length)
=> GetWrappedTime(Value.Time, length = Value.Length, Value.IsLooping);
/// <summary>
/// Gets the current <see cref="AnimancerState.Time"/>.
/// If the state is looping, the value is modulo by the <see cref="AnimancerState.Length"/>.
/// </summary>
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;
/************************************************************************************************************************/
/// <summary>The display name of the <see cref="AnimancerState.MainObject"/> field.</summary>
public virtual string MainObjectName
=> "Main Object";
/************************************************************************************************************************/
/// <inheritdoc/>
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--;
}
/************************************************************************************************************************/
/// <summary>Draws a `reference` if it isn't <c>null</c>.</summary>
private static void DoOptionalReferenceGUI(ref FastObjectField field, string label, object reference)
{
if (reference != null)
field.Draw(LayoutSingleLineRect(SpacingMode.Before), label, reference);
}
/************************************************************************************************************************/
/// <summary>Draws a slider for controlling the current <see cref="AnimancerState.Time"/>.</summary>
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<int, string> _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
/************************************************************************************************************************/
/// <inheritdoc/>
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);
}
/************************************************************************************************************************/
/// <summary>Adds the details of this state to the `menu`.</summary>
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
/************************************************************************************************************************/
}
/// <summary>[Editor-Only] Colors used by <see cref="AnimancerStateDrawer{T}"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerStateDrawerColors
public static class AnimancerStateDrawerColors
{
/************************************************************************************************************************/
/// <summary>Colors used by this system.</summary>
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);
/// <summary>Colors used by this system.</summary>
public static Color EventTickColor
=> Grey(EditorGUIUtility.isProSkin ? 0.8f : 0.2f, 0.8f);
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: be16b611f2570484dbfd1a4369ef2fa1
timeCreated: 1516751545
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,36 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
namespace Animancer.Editor
{
/// <inheritdoc/>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ClipStateDrawer
[CustomGUI(typeof(ClipState))]
public class ClipStateDrawer : AnimancerStateDrawer<ClipState>
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override string MainObjectName
=> "Clip";
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void AddContextMenuFunctions(UnityEditor.GenericMenu menu)
{
menu.AddDisabledItem(new(
$"{DetailsPrefix}Animation Type: {AnimationBindings.GetAnimationType(Value.Clip)}"));
base.AddContextMenuFunctions(menu);
AnimancerNodeBase.AddContextMenuIK(menu, Value);
}
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 34f989a643e362047ad721eee5385571
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,23 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
namespace Animancer.Editor
{
/// <inheritdoc/>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ControllerStateDrawer
[CustomGUI(typeof(ControllerState))]
public class ControllerStateDrawer : ParametizedAnimancerStateDrawer<ControllerState>
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override string MainObjectName
=> "Animator Controller";
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cfb75952401f7664aac294342b0a33e8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,32 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using UnityEditor;
namespace Animancer.Editor
{
/// <inheritdoc/>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/LinearMixerStateDrawer
[CustomGUI(typeof(LinearMixerState))]
public class LinearMixerStateDrawer : ParametizedAnimancerStateDrawer<LinearMixerState>
{
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void AddContextMenuFunctions(GenericMenu menu)
{
base.AddContextMenuFunctions(menu);
menu.AddItem(new("Extrapolate Speed"), Value.ExtrapolateSpeed, () =>
{
Value.ExtrapolateSpeed = !Value.ExtrapolateSpeed;
});
}
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f416ad99b4587c543a7988a4aba85fec
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,203 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] Draws the Inspector GUI for an <see cref="AnimancerState"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ParametizedAnimancerStateDrawer_1
[CustomGUI(typeof(ManualMixerState))]
public class ParametizedAnimancerStateDrawer<T> : AnimancerStateDrawer<T>
where T : AnimancerState, IParametizedState
{
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void DoDetailsGUI()
{
base.DoDetailsGUI();
if (!IsExpanded)
return;
EditorGUI.indentLevel++;
var parameters = ListPool.Acquire<StateParameterDetails>();
Value.GetParameters(parameters);
DoParametersGUI(parameters);
ListPool.Release(parameters);
EditorGUI.indentLevel--;
}
/************************************************************************************************************************/
/// <summary>Draws fields for all `parameters`.</summary>
private void DoParametersGUI(List<StateParameterDetails> parameters)
{
if (parameters.Count == 0)
return;
var labelWidth = EditorGUIUtility.labelWidth;
EditorGUIUtility.labelWidth -= AnimancerGUI.IndentSize;
EditorGUI.BeginChangeCheck();
for (int i = 0; i < parameters.Count; i++)
parameters[i] = DoParameterGUI(i, parameters[i]);
EditorGUIUtility.labelWidth = labelWidth;
if (EditorGUI.EndChangeCheck())
Value.SetParameters(parameters);
}
/************************************************************************************************************************/
/// <summary>Draws fields for the `parameter`.</summary>
private StateParameterDetails DoParameterGUI(int index, StateParameterDetails parameter)
{
var area = AnimancerGUI.LayoutSingleLineRect(AnimancerGUI.SpacingMode.Before);
var indentLevel = EditorGUI.indentLevel;
var labelWidth = EditorGUIUtility.labelWidth;
var label = parameter.label;
if (parameter.SupportsBinding && Value.Graph.HasParameters)
{
area = EditorGUI.IndentedRect(area);
EditorGUI.indentLevel = 0;
parameter = DoBindingGUI(ref area, index, parameter, ref label);
}
else
{
EditorGUIUtility.labelWidth += AnimancerGUI.IndentSize;
}
switch (parameter.type)
{
case AnimatorControllerParameterType.Float:
parameter.value = EditorGUI.FloatField(area, label, (float)parameter.value);
break;
case AnimatorControllerParameterType.Int:
parameter.value = EditorGUI.IntField(area, label, (int)parameter.value);
break;
case AnimatorControllerParameterType.Bool:
parameter.value = EditorGUI.Toggle(area, label, (bool)parameter.value);
break;
case AnimatorControllerParameterType.Trigger:
parameter.value = EditorGUI.Toggle(area, label, (bool)parameter.value, EditorStyles.radioButton);
break;
default:
EditorGUI.LabelField(area, label, "Unsupported Type: " + parameter.type);
break;
}
EditorGUI.indentLevel = indentLevel;
EditorGUIUtility.labelWidth = labelWidth;
return parameter;
}
/************************************************************************************************************************/
/// <summary>Draws a dropdown for the `parameter`'s name binding.</summary>
private StateParameterDetails DoBindingGUI(
ref Rect area,
int index,
StateParameterDetails parameter,
ref string fieldLabel)
{
if (!parameter.SupportsBinding)
return parameter;
var spacing = AnimancerGUI.StandardSpacing;
float width;
if (string.IsNullOrEmpty(parameter.name))
{
width = area.height + spacing;
EditorGUIUtility.labelWidth -= width + AnimancerGUI.IndentSize + spacing;
}
else
{
width = EditorGUIUtility.labelWidth - AnimancerGUI.IndentSize;
fieldLabel = "";
}
var labelArea = AnimancerGUI.StealFromLeft(
ref area,
width,
spacing);
using (var label = PooledGUIContent.Acquire(parameter.name))
{
if (EditorGUI.DropdownButton(labelArea, label, FocusType.Passive))
ShowBindingSelectionMenu(labelArea, index, parameter.name);
}
return parameter;
}
/************************************************************************************************************************/
/// <summary>Shows a context menu for selecting the parameter binding.</summary>
private void ShowBindingSelectionMenu(Rect area, int index, string currentName)
{
var menu = new GenericMenu();
menu.AddItem(
new("None"),
string.IsNullOrEmpty(currentName),
() => SetParameterName(index, null));
menu.AddSeparator("");
foreach (var parameter in Value.Graph.Parameters)
{
if (parameter.ValueType != typeof(float))
continue;
var name = parameter.Key;
menu.AddItem(
new(name),
name == currentName,
() => SetParameterName(index, name));
}
menu.DropDown(area);
}
/************************************************************************************************************************/
/// <summary>Sets the name binding of the specified parameter.</summary>
private void SetParameterName(int index, string name)
{
var parameters = ListPool.Acquire<StateParameterDetails>();
Value.GetParameters(parameters);
var modify = parameters[index];
modify.name = name;
parameters[index] = modify;
Value.SetParameters(parameters);
ListPool.Release(parameters);
}
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 3c4b9498f1412de40a107ac24fb55910
timeCreated: 1516751545
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,24 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
namespace Animancer.Editor
{
/// <inheritdoc/>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/PlayableAssetStateDrawer
[CustomGUI(typeof(PlayableAssetState))]
public class PlayableAssetStateDrawer : AnimancerStateDrawer<PlayableAssetState>
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override string MainObjectName
=> "Playable Asset";
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0f5aa603bfca9f446ad64081df30c975
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: