chore: initial commit
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] A custom Inspector for <see cref="AnimancerComponent"/>s.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerComponentEditor
|
||||
///
|
||||
[CustomEditor(typeof(AnimancerComponent), true), CanEditMultipleObjects]
|
||||
public class AnimancerComponentEditor : BaseAnimancerComponentEditor
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private bool _ShowResetOnDisableWarning;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool DoOverridePropertyGUI(string path, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
var target = Targets[0];
|
||||
if (path == target.AnimatorFieldName)
|
||||
{
|
||||
DoAnimatorGUI(property, label);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (path == target.ActionOnDisableFieldName)
|
||||
{
|
||||
DoActionOnDisableGUI(property, label);
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.DoOverridePropertyGUI(path, property, label);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void DoAnimatorGUI(SerializedProperty property, GUIContent label)
|
||||
{
|
||||
var animator = property.objectReferenceValue as Animator;
|
||||
|
||||
var color = GUI.color;
|
||||
if (animator == null)
|
||||
GUI.color = AnimancerGUI.WarningFieldColor;
|
||||
|
||||
EditorGUILayout.PropertyField(property, label);
|
||||
|
||||
if (animator == null)
|
||||
{
|
||||
GUI.color = color;
|
||||
|
||||
EditorGUILayout.HelpBox($"An {nameof(Animator)} is required in order to play animations." +
|
||||
" Click here to search for one nearby.",
|
||||
MessageType.Warning);
|
||||
|
||||
if (AnimancerGUI.TryUseClickEventInLastRect())
|
||||
{
|
||||
Serialization.ForEachTarget(property, (targetProperty) =>
|
||||
{
|
||||
var target = (IAnimancerComponent)targetProperty.serializedObject.targetObject;
|
||||
|
||||
animator = target.gameObject.GetComponentInParentOrChildren<Animator>();
|
||||
if (animator == null)
|
||||
{
|
||||
Debug.Log($"No {nameof(Animator)} found on '{target.gameObject.name}' or any of its parents or children." +
|
||||
" You must assign one manually.", target.gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
targetProperty.objectReferenceValue = animator;
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!animator.enabled)
|
||||
{
|
||||
EditorGUILayout.HelpBox(Strings.AnimatorDisabledMessage, MessageType.Warning);
|
||||
|
||||
if (AnimancerGUI.TryUseClickEventInLastRect())
|
||||
{
|
||||
Undo.RecordObject(animator, "Inspector");
|
||||
animator.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (animator.gameObject != Targets[0].gameObject)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
$"It is recommended that you keep this component on the same {nameof(GameObject)}" +
|
||||
$" as its target {nameof(Animator)} so that they get enabled and disabled at the same time.",
|
||||
MessageType.Info);
|
||||
}
|
||||
|
||||
var initialUpdateMode = Targets[0].InitialUpdateMode;
|
||||
var updateMode = animator.updateMode;
|
||||
if (AnimancerGraphCleanup.HasChangedToOrFromAnimatePhysics(initialUpdateMode, updateMode))
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
$"Changing to or from " +
|
||||
#if UNITY_2023_1_OR_NEWER
|
||||
$"{nameof(AnimatorUpdateMode.Fixed)}" +
|
||||
#else
|
||||
$"{nameof(AnimatorUpdateMode.AnimatePhysics)}" +
|
||||
#endif
|
||||
$" mode at runtime has no effect when using the Playables API." +
|
||||
$" It will continue using the original mode it had on startup.",
|
||||
MessageType.Warning);
|
||||
|
||||
if (AnimancerGUI.TryUseClickEventInLastRect())
|
||||
EditorUtility.OpenWithDefaultApp(Strings.DocsURLs.UpdateModes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void DoActionOnDisableGUI(SerializedProperty property, GUIContent label)
|
||||
{
|
||||
EditorGUILayout.PropertyField(property, label, true);
|
||||
|
||||
if (property.enumValueIndex == (int)AnimancerComponent.DisableAction.Reset)
|
||||
{
|
||||
// Since getting all the components creates garbage, only do it during layout events.
|
||||
if (Event.current.type == EventType.Layout)
|
||||
{
|
||||
_ShowResetOnDisableWarning = !AreAllResettingTargetsAboveTheirAnimator();
|
||||
}
|
||||
|
||||
if (_ShowResetOnDisableWarning)
|
||||
{
|
||||
EditorGUILayout.HelpBox("Reset only works if this component is above the Animator" +
|
||||
" so OnDisable can perform the Reset before the Animator actually gets disabled." +
|
||||
" Click here to fix." +
|
||||
"\n\nOtherwise you can use Stop and call Animator.Rebind before disabling this GameObject.",
|
||||
MessageType.Error);
|
||||
|
||||
if (AnimancerGUI.TryUseClickEventInLastRect())
|
||||
MoveResettingTargetsAboveTheirAnimator();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private bool AreAllResettingTargetsAboveTheirAnimator()
|
||||
{
|
||||
for (int i = 0; i < Targets.Length; i++)
|
||||
{
|
||||
var target = Targets[i];
|
||||
if (!target.ResetOnDisable)
|
||||
continue;
|
||||
|
||||
var animator = target.Animator;
|
||||
if (animator == null ||
|
||||
target.gameObject != animator.gameObject)
|
||||
continue;
|
||||
|
||||
var targetObject = (Object)target;
|
||||
var components = target.gameObject.GetComponents<Component>();
|
||||
for (int j = 0; j < components.Length; j++)
|
||||
{
|
||||
var component = components[j];
|
||||
if (component == targetObject)
|
||||
break;
|
||||
else if (component == animator)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void MoveResettingTargetsAboveTheirAnimator()
|
||||
{
|
||||
for (int i = 0; i < Targets.Length; i++)
|
||||
{
|
||||
var target = Targets[i];
|
||||
if (!target.ResetOnDisable)
|
||||
continue;
|
||||
|
||||
var animator = target.Animator;
|
||||
if (animator == null ||
|
||||
target.gameObject != animator.gameObject)
|
||||
continue;
|
||||
|
||||
int animatorIndex = -1;
|
||||
|
||||
var targetObject = (Object)target;
|
||||
var components = target.gameObject.GetComponents<Component>();
|
||||
for (int j = 0; j < components.Length; j++)
|
||||
{
|
||||
var component = components[j];
|
||||
if (component == targetObject)
|
||||
{
|
||||
if (animatorIndex >= 0)
|
||||
{
|
||||
var count = j - animatorIndex;
|
||||
while (count-- > 0)
|
||||
UnityEditorInternal.ComponentUtility.MoveComponentUp((Component)target);
|
||||
}
|
||||
break;
|
||||
}
|
||||
else if (component == animator)
|
||||
{
|
||||
animatorIndex = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private const string InitializeGraphFunction =
|
||||
"CONTEXT/" + nameof(AnimancerComponent) + "/Initialize Animancer Graph";
|
||||
|
||||
/// <summary>Context menu function to call <see cref="AnimancerComponent.InitializeGraph"/>.</summary>
|
||||
[MenuItem(InitializeGraphFunction)]
|
||||
private static void InitializeGraph(MenuCommand command)
|
||||
{
|
||||
if (command.context is AnimancerComponent animancer &&
|
||||
animancer.Graph.Layers.Count < 1)
|
||||
animancer.Graph.Layers.Count = 1;
|
||||
}
|
||||
|
||||
/// <summary>Should <see cref="InitializeGraph"/> be enabled?</summary>
|
||||
[MenuItem(InitializeGraphFunction, validate = true)]
|
||||
private static bool InitializeGraphValidate(MenuCommand command)
|
||||
=> command.context is AnimancerComponent animancer
|
||||
&& (!animancer.IsGraphInitialized || animancer.Graph.Layers.Count < 1);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: acdbf1bb0c07d294d9bbbac9812c226f
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,210 @@
|
||||
// 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.AnimancerGUI;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer.Editor.Previews
|
||||
{
|
||||
/// <summary>[Editor-Only]
|
||||
/// An interactive preview which displays the internal details of an <see cref="AnimancerComponent"/>.
|
||||
/// </summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.Previews/AnimancerComponentPreview
|
||||
[CustomPreview(typeof(AnimancerComponent))]
|
||||
public class AnimancerComponentPreview : ObjectPreview
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static readonly GUIContent
|
||||
Title = new(nameof(Animancer));
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override GUIContent GetPreviewTitle()
|
||||
=> Title;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[NonSerialized] private IAnimancerComponent _Animancer;
|
||||
[NonSerialized] private UnityEditor.Editor _Editor;
|
||||
|
||||
/// <summary>The drawer for the <see cref="IAnimancerComponent.Graph"/>.</summary>
|
||||
private readonly AnimancerGraphDrawer
|
||||
GraphDrawer = new();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize(Object[] targets)
|
||||
{
|
||||
_Animancer = targets.Length == 1
|
||||
? targets[0] as IAnimancerComponent
|
||||
: null;
|
||||
|
||||
_Editor = UnityEditor.Editor.CreateEditor(targets);
|
||||
|
||||
base.Initialize(targets);
|
||||
|
||||
EditorApplication.update += Update;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Cleanup()
|
||||
{
|
||||
EditorApplication.update -= Update;
|
||||
|
||||
Object.DestroyImmediate(_Editor);
|
||||
|
||||
base.Cleanup();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool HasPreviewGUI()
|
||||
=> !_Animancer.IsNullOrDestroyed()
|
||||
&& _Animancer.IsGraphInitialized;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static GUIStyle _ToolbarButtonStyle;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnPreviewSettings()
|
||||
{
|
||||
base.OnPreviewSettings();
|
||||
|
||||
_ToolbarButtonStyle ??= new(EditorStyles.toolbarButton)
|
||||
{
|
||||
padding = new(),
|
||||
};
|
||||
|
||||
var graph = _Animancer.Graph;
|
||||
|
||||
if (!graph.IsGraphPlaying)
|
||||
{
|
||||
var stepArea = GUILayoutUtility.GetRect(LineHeight * 1.5f, LineHeight);
|
||||
AnimancerGraphControls.DoFrameStepButton(stepArea, graph, _ToolbarButtonStyle);
|
||||
}
|
||||
|
||||
var area = GUILayoutUtility.GetRect(LineHeight * 1.5f, LineHeight);
|
||||
AnimancerGraphControls.DoPlayPauseToggle(area, graph, _ToolbarButtonStyle);
|
||||
|
||||
area = GUILayoutUtility.GetRect(LineHeight * 2f, LineHeight);
|
||||
AnimancerGraphSpeedSlider.Instance.Graph = graph;
|
||||
AnimancerGraphSpeedSlider.Instance.DoToggleGUI(area, _ToolbarButtonStyle);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[NonSerialized]
|
||||
private static GUIStyle _PaddingStyle;
|
||||
|
||||
[NonSerialized]
|
||||
private Rect _Area;
|
||||
|
||||
[SerializeField]
|
||||
private Vector2 _ScrollPosition;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnInteractivePreviewGUI(Rect area, GUIStyle background)
|
||||
{
|
||||
_PaddingStyle ??= new()
|
||||
{
|
||||
padding = new((int)StandardSpacing, (int)StandardSpacing, (int)StandardSpacing, (int)StandardSpacing),
|
||||
};
|
||||
|
||||
// The area isn't properly set during Layout events so remember it after each Repaint.
|
||||
|
||||
if (area.y == 0)
|
||||
area.y = EditorStyles.toolbar.fixedHeight + 1;
|
||||
|
||||
if (Event.current.type == EventType.Repaint)
|
||||
_Area = area;
|
||||
|
||||
// Draw the graph.
|
||||
|
||||
var labelWidth = EditorGUIUtility.labelWidth;
|
||||
EditorGUIUtility.labelWidth += IndentSize;
|
||||
|
||||
GUILayout.BeginArea(_Area);
|
||||
_ScrollPosition = GUILayout.BeginScrollView(_ScrollPosition, _PaddingStyle);
|
||||
|
||||
GraphDrawer.DoGUI(_Animancer);
|
||||
|
||||
GUILayout.EndScrollView();
|
||||
GUILayout.EndArea();
|
||||
|
||||
EditorGUIUtility.labelWidth = labelWidth;
|
||||
|
||||
_LastRepaintTime = EditorApplication.timeSinceStartup;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[NonSerialized]
|
||||
private double _LastRepaintTime = double.NegativeInfinity;
|
||||
|
||||
/// <summary>Repaints the preview if necessary.</summary>
|
||||
private void Update()
|
||||
{
|
||||
if (!HasPreviewGUI() ||
|
||||
!UnityEditorInternal.InternalEditorUtility.isApplicationActive)
|
||||
return;
|
||||
|
||||
var targetDeltaTime = 1f / AnimancerComponentPreviewSettings.RepaintRate;
|
||||
var nextRepaintTime = _LastRepaintTime + targetDeltaTime;
|
||||
|
||||
if (EditorApplication.timeSinceStartup > nextRepaintTime)
|
||||
_Editor.Repaint();
|
||||
|
||||
// This seems to be the least hacky way to repaint only the Inspector window.
|
||||
// Ideally an interactive preview would have a way to repaint itself.
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Settings
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Editor-Only] Settings for <see cref="AnimancerComponentPreview"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.Previews/AnimancerComponentPreviewSettings
|
||||
[Serializable, InternalSerializableType]
|
||||
public class AnimancerComponentPreviewSettings : AnimancerSettingsGroup
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string DisplayName
|
||||
=> "Live Inspector";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int Index
|
||||
=> 1;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField, Range(1, 100)]
|
||||
[Tooltip("The target frame rate of repaint commands (FPS)")]
|
||||
private float _RepaintRate = 30;
|
||||
|
||||
/// <summary>The target frame rate of repaint commands (FPS).</summary>
|
||||
public static float RepaintRate
|
||||
=> AnimancerSettingsGroup<AnimancerComponentPreviewSettings>.Instance._RepaintRate;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3d34fbf3c20578f4eb099d104a251bce
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,107 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] A custom Inspector for <see cref="IAnimancerComponent"/>s.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/BaseAnimancerComponentEditor
|
||||
public abstract class BaseAnimancerComponentEditor : UnityEditor.Editor
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[NonSerialized]
|
||||
private IAnimancerComponent[] _Targets;
|
||||
|
||||
/// <summary><see cref="UnityEditor.Editor.targets"/> casted to <see cref="IAnimancerComponent"/>.</summary>
|
||||
public IAnimancerComponent[] Targets
|
||||
=> _Targets;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Initializes this <see cref="UnityEditor.Editor"/>.</summary>
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
var targets = this.targets;
|
||||
_Targets = new IAnimancerComponent[targets.Length];
|
||||
GatherTargets();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Copies the <see cref="UnityEditor.Editor.targets"/> into the <see cref="_Targets"/> array.
|
||||
/// </summary>
|
||||
private void GatherTargets()
|
||||
{
|
||||
for (int i = 0; i < _Targets.Length; i++)
|
||||
_Targets[i] = (IAnimancerComponent)targets[i];
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Called by the Unity editor to draw the custom Inspector GUI elements.</summary>
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
// Normally the targets wouldn't change after OnEnable, but the trick AnimancerComponent.Reset uses to
|
||||
// swap the type of an existing component when a new one is added causes the old target to be destroyed.
|
||||
GatherTargets();
|
||||
|
||||
serializedObject.Update();
|
||||
|
||||
DoSerializedFieldsGUI();
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the rest of the Inspector fields after the Animator field.</summary>
|
||||
protected void DoSerializedFieldsGUI()
|
||||
{
|
||||
var property = serializedObject.GetIterator();
|
||||
|
||||
if (!property.NextVisible(true))
|
||||
return;
|
||||
|
||||
do
|
||||
{
|
||||
var path = property.propertyPath;
|
||||
if (path == "m_Script")
|
||||
continue;
|
||||
|
||||
using (var label = PooledGUIContent.Acquire(property))
|
||||
{
|
||||
// Let the target try to override.
|
||||
if (DoOverridePropertyGUI(path, property, label))
|
||||
continue;
|
||||
|
||||
// Otherwise draw the property normally.
|
||||
EditorGUILayout.PropertyField(property, label, true);
|
||||
}
|
||||
}
|
||||
while (property.NextVisible(false));
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Editor-Only]
|
||||
/// Draws any custom GUI for the `property`.
|
||||
/// The return value indicates whether the GUI should replace the regular call to
|
||||
/// <see cref="EditorGUILayout.PropertyField(SerializedProperty, GUIContent, bool, GUILayoutOption[])"/>.
|
||||
/// True = GUI was drawn, so don't draw the regular GUI.
|
||||
/// False = Draw the regular GUI.
|
||||
/// </summary>
|
||||
protected virtual bool DoOverridePropertyGUI(string path, SerializedProperty property, GUIContent label)
|
||||
=> false;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 24a478866ba8a5248a582ad85ed36212
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,80 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] A custom Inspector for <see cref="HybridAnimancerComponentEditor"/>s.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/HybridAnimancerComponentEditor
|
||||
///
|
||||
[CustomEditor(typeof(HybridAnimancerComponent), true), CanEditMultipleObjects]
|
||||
public class HybridAnimancerComponentEditor : NamedAnimancerComponentEditor
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool DoOverridePropertyGUI(string path, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
switch (path)
|
||||
{
|
||||
case "_Controller":
|
||||
|
||||
EditorGUILayout.PropertyField(property, label, true);
|
||||
|
||||
property = property.FindPropertyRelative("_Controller");
|
||||
|
||||
var hasAnimatorController = property?.objectReferenceValue != null;
|
||||
var warning = GetAnimatorControllerWarning(hasAnimatorController, out var messageType);
|
||||
if (warning is not null)
|
||||
{
|
||||
EditorGUILayout.HelpBox(warning, messageType);
|
||||
if (AnimancerGUI.TryUseClickEventInLastRect())
|
||||
Application.OpenURL(Strings.DocsURLs.AnimatorControllers);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.DoOverridePropertyGUI(path, property, label);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private string GetAnimatorControllerWarning(bool hasAnimatorController, out MessageType messageType)
|
||||
{
|
||||
messageType = MessageType.Warning;
|
||||
|
||||
if (!hasAnimatorController)
|
||||
{
|
||||
return
|
||||
$"No Animator Controller is assigned to this component so" +
|
||||
$" you should likely use a base {nameof(AnimancerComponent)} instead." +
|
||||
$" Click here for more information.";
|
||||
}
|
||||
|
||||
if (Targets.Length > 0)
|
||||
{
|
||||
var animator = Targets[0].Animator;
|
||||
if (animator != null && animator.runtimeAnimatorController != null)
|
||||
{
|
||||
return
|
||||
$"A Native Animator Controller is assigned to the Animator component" +
|
||||
$" and a Hybrid Animator Controller is also assigned to this component." +
|
||||
$" That's not necessarily a problem, but using both systems at the same time is very unusual" +
|
||||
$" and likely a waste of performance if you don't need to play both Animator Controllers at once." +
|
||||
$" Click here for more information.";
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3d3a2fcce784d5f46a8aae3603a8a10c
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,407 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEditorInternal;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] A custom Inspector for <see cref="NamedAnimancerComponent"/>s.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/NamedAnimancerComponentEditor
|
||||
///
|
||||
[CustomEditor(typeof(NamedAnimancerComponent), true), CanEditMultipleObjects]
|
||||
public class NamedAnimancerComponentEditor : AnimancerComponentEditor
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool DoOverridePropertyGUI(string path, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
switch (path)
|
||||
{
|
||||
case NamedAnimancerComponent.PlayAutomaticallyField:
|
||||
if (ShouldShowAnimationFields())
|
||||
DoDefaultAnimationField(property);
|
||||
return true;
|
||||
|
||||
case NamedAnimancerComponent.NamesField:
|
||||
// Names are drawn in the Animations list.
|
||||
return true;
|
||||
|
||||
case NamedAnimancerComponent.AnimationsField:
|
||||
if (ShouldShowAnimationFields())
|
||||
DoAnimationsField(property);
|
||||
return true;
|
||||
|
||||
default:
|
||||
return base.DoOverridePropertyGUI(path, property, label);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="NamedAnimancerComponent.PlayAutomatically"/> and
|
||||
/// <see cref="NamedAnimancerComponent.Animations"/> fields are only used on startup, so we don't need to show
|
||||
/// them in Play Mode after the object is already enabled.
|
||||
/// </summary>
|
||||
private bool ShouldShowAnimationFields()
|
||||
{
|
||||
if (!EditorApplication.isPlayingOrWillChangePlaymode)
|
||||
return true;
|
||||
|
||||
for (int i = 0; i < Targets.Length; i++)
|
||||
if (!Targets[i].IsGraphInitialized)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void DoDefaultAnimationField(SerializedProperty playAutomatically)
|
||||
{
|
||||
var area = AnimancerGUI.LayoutSingleLineRect();
|
||||
|
||||
var playAutomaticallyWidth = EditorGUIUtility.labelWidth + AnimancerGUI.ToggleWidth;
|
||||
var playAutomaticallyArea = AnimancerGUI.StealFromLeft(ref area, playAutomaticallyWidth);
|
||||
|
||||
using (var label = PooledGUIContent.Acquire(playAutomatically))
|
||||
EditorGUI.PropertyField(playAutomaticallyArea, playAutomatically, label);
|
||||
|
||||
SerializedProperty firstAnimation;
|
||||
AnimationClip clip;
|
||||
|
||||
var animations = serializedObject.FindProperty(NamedAnimancerComponent.AnimationsField);
|
||||
if (animations.arraySize > 0)
|
||||
{
|
||||
firstAnimation = animations.GetArrayElementAtIndex(0);
|
||||
clip = (AnimationClip)firstAnimation.objectReferenceValue;
|
||||
EditorGUI.BeginProperty(area, null, firstAnimation);
|
||||
}
|
||||
else
|
||||
{
|
||||
firstAnimation = null;
|
||||
clip = null;
|
||||
EditorGUI.BeginProperty(area, null, animations);
|
||||
}
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
|
||||
var indentLevel = EditorGUI.indentLevel;
|
||||
EditorGUI.indentLevel = 0;
|
||||
|
||||
clip = AnimancerGUI.DoObjectFieldGUI(area, GUIContent.none, clip, true);
|
||||
|
||||
EditorGUI.indentLevel = indentLevel;
|
||||
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
if (clip != null)
|
||||
{
|
||||
if (firstAnimation == null)
|
||||
{
|
||||
animations.arraySize = 1;
|
||||
firstAnimation = animations.GetArrayElementAtIndex(0);
|
||||
}
|
||||
|
||||
firstAnimation.objectReferenceValue = clip;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (firstAnimation == null || animations.arraySize == 1)
|
||||
animations.arraySize = 0;
|
||||
else
|
||||
firstAnimation.objectReferenceValue = clip;
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private ReorderableList _Animations;
|
||||
private SerializedProperty _Names;
|
||||
|
||||
private static int _RemoveAnimationIndex;
|
||||
|
||||
private void DoAnimationsField(SerializedProperty property)
|
||||
{
|
||||
GUILayout.Space(AnimancerGUI.StandardSpacing - 1);
|
||||
|
||||
var serializedObject = property.serializedObject;
|
||||
|
||||
_Names = serializedObject.FindProperty(NamedAnimancerComponent.NamesField);
|
||||
|
||||
_Animations ??= new(serializedObject, property.Copy())
|
||||
{
|
||||
drawHeaderCallback = DrawAnimationsHeader,
|
||||
drawElementCallback = DrawAnimationElement,
|
||||
elementHeight = AnimancerGUI.LineHeight,
|
||||
onAddCallback = AddNullElement,
|
||||
onRemoveCallback = RemoveSelectedAnimation,
|
||||
};
|
||||
|
||||
_RemoveAnimationIndex = -1;
|
||||
|
||||
GUILayout.BeginVertical();
|
||||
_Animations.DoLayoutList();
|
||||
GUILayout.EndVertical();
|
||||
|
||||
if (_RemoveAnimationIndex >= 0)
|
||||
property.DeleteArrayElementAtIndex(_RemoveAnimationIndex);
|
||||
|
||||
HandleDragAndDropToAddAnimations(GUILayoutUtility.GetLastRect(), property);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private SerializedProperty _AnimationsArraySize;
|
||||
|
||||
private void DrawAnimationsHeader(Rect area)
|
||||
{
|
||||
var labelWidth = EditorGUIUtility.labelWidth;
|
||||
EditorGUIUtility.labelWidth -= 6;
|
||||
|
||||
area.width += 5;
|
||||
|
||||
var property = _Animations.serializedProperty;
|
||||
using (var label = PooledGUIContent.Acquire(property))
|
||||
{
|
||||
var propertyLabel = EditorGUI.BeginProperty(area, label, property);
|
||||
|
||||
if (_AnimationsArraySize == null)
|
||||
{
|
||||
_AnimationsArraySize = property.Copy();
|
||||
_AnimationsArraySize.Next(true);
|
||||
_AnimationsArraySize.Next(true);
|
||||
}
|
||||
|
||||
var oldSize = _AnimationsArraySize.intValue;
|
||||
EditorGUI.PropertyField(area, _AnimationsArraySize, propertyLabel);
|
||||
var newSize = _AnimationsArraySize.intValue;
|
||||
|
||||
if (oldSize < newSize)
|
||||
for (int i = oldSize; i < newSize; i++)
|
||||
property.GetArrayElementAtIndex(i).objectReferenceValue = null;
|
||||
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
|
||||
EditorGUIUtility.labelWidth = labelWidth;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static readonly HashSet<Object>
|
||||
PreviousAnimations = new();
|
||||
|
||||
private void DrawAnimationElement(Rect area, int index, bool isActive, bool isFocused)
|
||||
{
|
||||
if (index == 0)
|
||||
PreviousAnimations.Clear();
|
||||
|
||||
var labelWidth = EditorGUIUtility.labelWidth;
|
||||
EditorGUIUtility.labelWidth -= 20;
|
||||
|
||||
DrawNameField(ref area, index);
|
||||
|
||||
var animation = _Animations.serializedProperty.GetArrayElementAtIndex(index);
|
||||
|
||||
var color = GUI.color;
|
||||
var clip = animation.objectReferenceValue;
|
||||
if (clip == null || PreviousAnimations.Contains(clip))
|
||||
GUI.color = AnimancerGUI.WarningFieldColor;
|
||||
else
|
||||
PreviousAnimations.Add(clip);
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
|
||||
EditorGUI.ObjectField(area, animation, GUIContent.none);
|
||||
|
||||
if (EditorGUI.EndChangeCheck() && animation.objectReferenceValue == null)
|
||||
_RemoveAnimationIndex = index;
|
||||
|
||||
GUI.color = color;
|
||||
EditorGUIUtility.labelWidth = labelWidth;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void DrawNameField(ref Rect area, int index)
|
||||
{
|
||||
EditorGUI.BeginChangeCheck();
|
||||
|
||||
var nameCount = _Names.arraySize;
|
||||
var name = index < nameCount
|
||||
? _Names.GetArrayElementAtIndex(index)
|
||||
: null;
|
||||
|
||||
var nameArea = AnimancerGUI.StealFromLeft(
|
||||
ref area,
|
||||
EditorGUIUtility.labelWidth,
|
||||
AnimancerGUI.StandardSpacing);
|
||||
|
||||
if (name != null)
|
||||
EditorGUI.BeginProperty(nameArea, null, name);
|
||||
|
||||
var nameAsset = name?.objectReferenceValue;
|
||||
var allowSceneObjects = !EditorUtility.IsPersistent(target);
|
||||
nameAsset = EditorGUI.ObjectField(
|
||||
nameArea,
|
||||
GUIContent.none,
|
||||
nameAsset,
|
||||
typeof(StringAsset),
|
||||
allowSceneObjects);
|
||||
|
||||
if (name != null)
|
||||
EditorGUI.EndProperty();
|
||||
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
if (nameAsset != null)// Set.
|
||||
{
|
||||
ExpandWithNullsForNewItems(_Names, index + 1);
|
||||
|
||||
name ??= _Names.GetArrayElementAtIndex(index);
|
||||
|
||||
name.objectReferenceValue = nameAsset;
|
||||
}
|
||||
else// Remove.
|
||||
{
|
||||
name.objectReferenceValue = null;
|
||||
TrimTrailingNulls(_Names);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static void ExpandWithNullsForNewItems(SerializedProperty array, int newCount)
|
||||
{
|
||||
var oldCount = array.arraySize;
|
||||
if (newCount <= oldCount)
|
||||
return;
|
||||
|
||||
// If we expand more than 1 at a time, clear the first new item before doing the full expansion
|
||||
// so that all the new items are cleared.
|
||||
|
||||
array.arraySize = oldCount + 1;
|
||||
if (newCount > oldCount + 1)
|
||||
{
|
||||
array.GetArrayElementAtIndex(oldCount).objectReferenceValue = null;
|
||||
array.arraySize = newCount;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static void TrimTrailingNulls(SerializedProperty array)
|
||||
{
|
||||
var oldCount = array.arraySize;
|
||||
var newCount = oldCount;
|
||||
while (newCount > 0)
|
||||
{
|
||||
if (array.GetArrayElementAtIndex(newCount - 1).objectReferenceValue != null)
|
||||
break;
|
||||
|
||||
newCount--;
|
||||
}
|
||||
|
||||
if (newCount < oldCount)
|
||||
array.arraySize = newCount;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static void AddNullElement(ReorderableList list)
|
||||
{
|
||||
var property = list.serializedProperty;
|
||||
var count = list.count;
|
||||
|
||||
property.arraySize = count + 1;
|
||||
list.index = count;
|
||||
|
||||
property.GetArrayElementAtIndex(count).objectReferenceValue = null;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void RemoveSelectedAnimation(ReorderableList list)
|
||||
{
|
||||
var property = list.serializedProperty;
|
||||
var index = list.index;
|
||||
|
||||
if (index < _Names.arraySize)
|
||||
RemoveElement(_Names, index);
|
||||
|
||||
RemoveElement(property, index);
|
||||
|
||||
if (index >= property.arraySize - 1)
|
||||
list.index = property.arraySize - 1;
|
||||
}
|
||||
|
||||
private static void RemoveElement(SerializedProperty array, int index)
|
||||
{
|
||||
var element = array.GetArrayElementAtIndex(index);
|
||||
|
||||
// Deleting a non-null element sets it to null, so we make sure it's null to actually remove it.
|
||||
if (element.objectReferenceValue != null)
|
||||
element.objectReferenceValue = null;
|
||||
|
||||
array.DeleteArrayElementAtIndex(index);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static DragAndDropHandler<object> _DropToAddAnimations;
|
||||
private static SerializedProperty _DropToAddAnimationsProperty;
|
||||
private static void HandleDragAndDropToAddAnimations(Rect area, SerializedProperty property)
|
||||
{
|
||||
_DropToAddAnimationsProperty = property;
|
||||
|
||||
_DropToAddAnimations ??= (obj, isDrop) =>
|
||||
{
|
||||
using (ListPool<AnimationClip>.Instance.Acquire(out var clips))
|
||||
{
|
||||
clips.GatherFromSource(obj);
|
||||
|
||||
var anyValid = false;
|
||||
|
||||
for (int i = 0; i < clips.Count; i++)
|
||||
{
|
||||
var clip = clips[i];
|
||||
if (clip.legacy)
|
||||
continue;
|
||||
|
||||
if (!isDrop)
|
||||
return true;
|
||||
|
||||
anyValid = true;
|
||||
|
||||
var targetProperty = _DropToAddAnimationsProperty;
|
||||
var index = targetProperty.arraySize;
|
||||
targetProperty.arraySize = index + 1;
|
||||
var element = targetProperty.GetArrayElementAtIndex(index);
|
||||
element.objectReferenceValue = clip;
|
||||
targetProperty.serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
return anyValid;
|
||||
}
|
||||
};
|
||||
|
||||
_DropToAddAnimations.Handle(area);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9b409e7645821a549b4767ce5a72f1d0
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user