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,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

View File

@@ -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:

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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: