chore: initial commit
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 19179535da292bd4a9ed920618535c3f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -0,0 +1,97 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] A custom Inspector for <see cref="AnimancerEvent.Invoker"/>s.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerEventInvokerEditor
|
||||
///
|
||||
[CustomEditor(typeof(AnimancerEvent.Invoker), true), CanEditMultipleObjects]
|
||||
public class AnimancerEventInvokerEditor : UnityEditor.Editor
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private readonly Field[]
|
||||
Fields = new Field[100];
|
||||
|
||||
private struct Field
|
||||
{
|
||||
public FastObjectField component;
|
||||
public FastObjectField state;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
if (target is Behaviour behaviour &&
|
||||
!behaviour.enabled)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
"This component is disabled so it won't invoke any events.",
|
||||
MessageType.Warning);
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
|
||||
var isLayoutEvent = Event.current.type == EventType.Layout;
|
||||
|
||||
var enumerator = AnimancerEvent.Invoker.EnumerateInvocationQueue();
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
if (index < Fields.Length)
|
||||
{
|
||||
var invocation = enumerator.Current;
|
||||
|
||||
if (invocation.State == null)
|
||||
{
|
||||
GUILayout.Label("State is Null");
|
||||
return;
|
||||
}
|
||||
|
||||
ref var field = ref Fields[index];
|
||||
|
||||
var area = AnimancerGUI.LayoutSingleLineRect();
|
||||
|
||||
var labelArea = AnimancerGUI.StealFromLeft(ref area, EditorGUIUtility.labelWidth);
|
||||
labelArea = EditorGUI.IndentedRect(labelArea);
|
||||
|
||||
if (isLayoutEvent)
|
||||
{
|
||||
field.component.SetValue(invocation.State.Graph?.Component);
|
||||
field.state.SetValue(invocation.State, invocation.State.GetPath());
|
||||
}
|
||||
|
||||
field.component.Draw(labelArea);
|
||||
field.state.Draw(area);
|
||||
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
EditorGUILayout.LabelField("Event Name", invocation.Name);
|
||||
EditorGUILayout.LabelField("Normalized Time", invocation.Event.normalizedTime.ToString());
|
||||
NamedEventDictionaryDrawer.DoEventGUI("Direct Callback", invocation.Event.callback);
|
||||
NamedEventDictionaryDrawer.DoEventGUI("Bound Callback", invocation.GetBoundCallback());
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
if (index > Fields.Length)
|
||||
GUILayout.Label($"And {index - Fields.Length} more events.");
|
||||
|
||||
Repaint();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c2b16204ed669cd40a5ed4ea9db5d2fa
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using UnityEditor;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] A custom editor for <see cref="TransitionAssetBase"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerTransitionAssetBaseEditor
|
||||
[CustomEditor(typeof(TransitionAssetBase), true), CanEditMultipleObjects]
|
||||
public class AnimancerTransitionAssetBaseEditor : ScriptableObjectEditor { }
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 15f56340d39a9ec40b5fbde43575c974
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,458 @@
|
||||
// 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 Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] [Pro-Only] A custom Inspector for <see cref="AnimationClip"/>s</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimationClipEditor
|
||||
///
|
||||
[CustomEditor(typeof(AnimationClip))]
|
||||
public class AnimationClipEditor : UnityEditor.Editor
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private const string DefaultEditorTypeName = nameof(UnityEditor) + "." + nameof(AnimationClipEditor);
|
||||
|
||||
private static readonly Type
|
||||
DefaultEditorType = typeof(UnityEditor.Editor).Assembly.GetType(DefaultEditorTypeName);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private UnityEditor.Editor _DefaultEditor;
|
||||
|
||||
private bool TryGetDefaultEditor(out UnityEditor.Editor editor)
|
||||
{
|
||||
if (_DefaultEditor == null)
|
||||
{
|
||||
if (DefaultEditorType == null || AnimancerEditorUtilities.IsChangingPlayMode)
|
||||
{
|
||||
editor = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
_DefaultEditor = CreateEditor(targets, DefaultEditorType);
|
||||
_DefaultEditor.hideFlags = HideFlags.DontSave;
|
||||
DestroyOnPlayModeStateChanged(_DefaultEditor);
|
||||
}
|
||||
|
||||
editor = _DefaultEditor;
|
||||
return true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
protected virtual void OnDestroy()
|
||||
{
|
||||
_DestroyOnPlayModeStateChanged?.Remove(_DefaultEditor);
|
||||
DestroyImmediate(_DefaultEditor);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static HashSet<Object> _DestroyOnPlayModeStateChanged;
|
||||
|
||||
private static void DestroyOnPlayModeStateChanged(Object obj)
|
||||
{
|
||||
if (_DestroyOnPlayModeStateChanged == null)
|
||||
{
|
||||
_DestroyOnPlayModeStateChanged = new();
|
||||
|
||||
EditorApplication.playModeStateChanged += (change) =>
|
||||
{
|
||||
foreach (var destroy in _DestroyOnPlayModeStateChanged)
|
||||
DestroyImmediate(destroy);
|
||||
|
||||
_DestroyOnPlayModeStateChanged.Clear();
|
||||
};
|
||||
}
|
||||
|
||||
_DestroyOnPlayModeStateChanged.Add(obj);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the regular Inspector then adds a better preview for <see cref="Sprite"/> animations.</summary>
|
||||
/// <remarks>Called by the Unity editor to draw the custom Inspector GUI elements.</remarks>
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
if (DefaultEditorType == null)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
$"Unable to find type '{DefaultEditorTypeName}' in '{typeof(UnityEditor.Editor).Assembly}'." +
|
||||
$" The {nameof(AnimationClipEditor)} script will need to be fixed" +
|
||||
$" or you can simply delete it to use Unity's regular {nameof(AnimationClip)} Inspector.",
|
||||
MessageType.Error);
|
||||
|
||||
const string Label = "Delete " + nameof(AnimationClipEditor) + " Script";
|
||||
if (GUILayout.Button(Label))
|
||||
{
|
||||
if (EditorUtility.DisplayDialog(Label,
|
||||
$"Are you sure you want to delete the {nameof(AnimationClipEditor)} script?" +
|
||||
$" This operation cannot be undone.",
|
||||
"Delete",
|
||||
"Cancel"))
|
||||
{
|
||||
var script = MonoScript.FromScriptableObject(this);
|
||||
var path = AssetDatabase.GetAssetPath(script);
|
||||
AssetDatabase.DeleteAsset(path);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryGetDefaultEditor(out var editor))
|
||||
editor.OnInspectorGUI();
|
||||
|
||||
if (GUILayout.Button("Open Animation Window"))
|
||||
EditorApplication.ExecuteMenuItem("Window/Animation/Animation");
|
||||
|
||||
if (GUILayout.Button("Open Animancer Tools"))
|
||||
Tools.AnimancerToolsWindow.Open();
|
||||
|
||||
var targets = this.targets;
|
||||
if (targets.Length == 1)
|
||||
{
|
||||
var clip = GetTargetClip(out var type);
|
||||
|
||||
DrawEvents(clip);
|
||||
|
||||
if (type == AnimationType.Sprite)
|
||||
{
|
||||
InitializeSpritePreview(editor, clip);
|
||||
DrawSpriteFrames(clip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private AnimationClip GetTargetClip(out AnimationType type)
|
||||
{
|
||||
var clip = (AnimationClip)target;
|
||||
type = AnimationBindings.GetAnimationType(clip);
|
||||
return clip;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private bool _ShowEvents = true;
|
||||
|
||||
private void DrawEvents(AnimationClip clip)
|
||||
{
|
||||
var events = clip.events;
|
||||
if (events == null ||
|
||||
events.Length == 0)
|
||||
return;
|
||||
|
||||
_ShowEvents = EditorGUILayout.Foldout(_ShowEvents, "Events", true);
|
||||
if (!_ShowEvents)
|
||||
return;
|
||||
|
||||
using (new EditorGUI.DisabledScope(true))
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
for (int i = 0; i < events.Length; i++)
|
||||
{
|
||||
var animationEvent = events[i];
|
||||
EditorGUILayout.FloatField(animationEvent.functionName, animationEvent.time);
|
||||
|
||||
EditorGUI.indentLevel++;
|
||||
EditorGUILayout.IntField("Int", animationEvent.intParameter);
|
||||
EditorGUILayout.FloatField("Float", animationEvent.floatParameter);
|
||||
EditorGUILayout.TextField("String", animationEvent.stringParameter);
|
||||
EditorGUILayout.ObjectField("Object", animationEvent.objectReferenceParameter, typeof(Object), false);
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[NonSerialized]
|
||||
private bool _HasInitializedSpritePreview;
|
||||
|
||||
private void InitializeSpritePreview(UnityEditor.Editor editor, AnimationClip clip)
|
||||
{
|
||||
if (_HasInitializedSpritePreview
|
||||
|| editor == null)
|
||||
return;
|
||||
|
||||
_HasInitializedSpritePreview = true;
|
||||
|
||||
// Get the avatar preview.
|
||||
|
||||
var field = editor.GetType().GetField("m_AvatarPreview", AnimancerReflection.InstanceBindings);
|
||||
if (field == null)
|
||||
return;
|
||||
|
||||
var preview = field.GetValue(editor);
|
||||
if (preview == null)
|
||||
return;
|
||||
|
||||
var previewType = preview.GetType();
|
||||
|
||||
// Make sure a proper preview object isn't already assigned.
|
||||
|
||||
var previewObject = previewType.GetProperty("PreviewObject", AnimancerReflection.InstanceBindings);
|
||||
if (previewObject == null)
|
||||
return;
|
||||
|
||||
var previewGameObject = previewObject.GetValue(preview) as GameObject;
|
||||
if (previewGameObject != null &&
|
||||
previewGameObject.GetComponentInChildren<Renderer>() != null)
|
||||
return;
|
||||
|
||||
// Get the SetPreview method.
|
||||
|
||||
var method = previewType.GetMethod(
|
||||
"SetPreview",
|
||||
AnimancerReflection.InstanceBindings,
|
||||
null,
|
||||
new Type[] { typeof(GameObject) },
|
||||
null);
|
||||
if (method == null)
|
||||
return;
|
||||
|
||||
// Get the Sprite from the target animation's first keyframe.
|
||||
|
||||
var keyframes = GetSpriteReferences(clip);
|
||||
if (keyframes == null ||
|
||||
keyframes.Length == 0)
|
||||
return;
|
||||
|
||||
var sprite = keyframes[0].value as Sprite;
|
||||
if (sprite == null)
|
||||
return;
|
||||
|
||||
// Create an object with an Animator and SpriteRenderer.
|
||||
// The Sprite must be assigned for it to be accepted as the preview object.
|
||||
|
||||
var gameObject = EditorUtility.CreateGameObjectWithHideFlags(
|
||||
"SpritePreview",
|
||||
HideFlags.HideInHierarchy | HideFlags.DontSave);
|
||||
gameObject.AddComponent<Animator>();
|
||||
gameObject.AddComponent<SpriteRenderer>().sprite = sprite;
|
||||
|
||||
// Set it as the preview object (which creates a copy of it) and destroy it.
|
||||
|
||||
method.Invoke(preview, new object[] { gameObject });
|
||||
|
||||
DestroyImmediate(gameObject);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static ConversionCache<int, string> _FrameCache;
|
||||
private static ConversionCache<float, string> _TimeCache;
|
||||
|
||||
private static void DrawSpriteFrames(AnimationClip clip)
|
||||
{
|
||||
var keyframes = GetSpriteReferences(clip);
|
||||
if (keyframes == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < keyframes.Length; i++)
|
||||
{
|
||||
var keyframe = keyframes[i];
|
||||
var sprite = keyframe.value as Sprite;
|
||||
if (sprite != null)
|
||||
{
|
||||
if (_FrameCache == null)
|
||||
{
|
||||
_FrameCache = new(
|
||||
(value) => $"Frame: {value}");
|
||||
|
||||
_TimeCache = new(
|
||||
(value) => $"Time: {value}s");
|
||||
}
|
||||
|
||||
var texture = sprite.texture;
|
||||
|
||||
var area = AnimancerGUI.LayoutRect(AnimancerGUI.LineHeight * 4);
|
||||
var width = area.width;
|
||||
|
||||
var rect = sprite.rect;
|
||||
area.width = area.height * rect.width / rect.height;
|
||||
|
||||
rect.x /= texture.width;
|
||||
rect.y /= texture.height;
|
||||
rect.width /= texture.width;
|
||||
rect.height /= texture.height;
|
||||
|
||||
GUI.DrawTextureWithTexCoords(area, texture, rect);
|
||||
|
||||
var offset = area.width + AnimancerGUI.StandardSpacing;
|
||||
area.x += offset;
|
||||
area.width = width - offset;
|
||||
area.height = AnimancerGUI.LineHeight;
|
||||
area.y += Mathf.Round(area.height * 0.5f);
|
||||
|
||||
GUI.Label(area, _FrameCache.Convert(i));
|
||||
|
||||
AnimancerGUI.NextVerticalArea(ref area);
|
||||
GUI.Label(area, _TimeCache.Convert(keyframe.time));
|
||||
|
||||
AnimancerGUI.NextVerticalArea(ref area);
|
||||
GUI.Label(area, sprite.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static ObjectReferenceKeyframe[] GetSpriteReferences(AnimationClip clip)
|
||||
{
|
||||
var bindings = AnimationBindings.GetBindings(clip);
|
||||
for (int i = 0; i < bindings.Length; i++)
|
||||
{
|
||||
var binding = bindings[i];
|
||||
if (binding.path == "" &&
|
||||
binding.type == typeof(SpriteRenderer) &&
|
||||
binding.propertyName == "m_Sprite")
|
||||
return AnimationUtility.GetObjectReferenceCurve(clip, binding);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Redirects
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DrawPreview(Rect previewArea)
|
||||
{
|
||||
if (TryGetDefaultEditor(out var editor))
|
||||
editor.DrawPreview(previewArea);
|
||||
else
|
||||
base.DrawPreview(previewArea);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string GetInfoString()
|
||||
{
|
||||
if (TryGetDefaultEditor(out var editor))
|
||||
return editor.GetInfoString();
|
||||
else
|
||||
return base.GetInfoString();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override GUIContent GetPreviewTitle()
|
||||
{
|
||||
if (TryGetDefaultEditor(out var editor))
|
||||
return editor.GetPreviewTitle();
|
||||
else
|
||||
return base.GetPreviewTitle();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool HasPreviewGUI()
|
||||
{
|
||||
if (TryGetDefaultEditor(out var editor))
|
||||
return editor.HasPreviewGUI();
|
||||
else
|
||||
return base.HasPreviewGUI();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnInteractivePreviewGUI(Rect area, GUIStyle background)
|
||||
{
|
||||
if (TryGetDefaultEditor(out var editor))
|
||||
editor.OnInteractivePreviewGUI(area, background);
|
||||
else
|
||||
base.OnInteractivePreviewGUI(area, background);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnPreviewGUI(Rect area, GUIStyle background)
|
||||
{
|
||||
if (TryGetDefaultEditor(out var editor))
|
||||
editor.OnPreviewGUI(area, background);
|
||||
else
|
||||
base.OnPreviewGUI(area, background);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnPreviewSettings()
|
||||
{
|
||||
if (TryGetDefaultEditor(out var editor))
|
||||
editor.OnPreviewSettings();
|
||||
else
|
||||
base.OnPreviewSettings();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void ReloadPreviewInstances()
|
||||
{
|
||||
if (TryGetDefaultEditor(out var editor))
|
||||
editor.ReloadPreviewInstances();
|
||||
else
|
||||
base.ReloadPreviewInstances();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Texture2D RenderStaticPreview(string assetPath, Object[] subAssets, int width, int height)
|
||||
{
|
||||
if (TryGetDefaultEditor(out var editor))
|
||||
return editor.RenderStaticPreview(assetPath, subAssets, width, height);
|
||||
else
|
||||
return base.RenderStaticPreview(assetPath, subAssets, width, height);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool RequiresConstantRepaint()
|
||||
{
|
||||
if (TryGetDefaultEditor(out var editor))
|
||||
return editor.RequiresConstantRepaint();
|
||||
else
|
||||
return base.RequiresConstantRepaint();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool UseDefaultMargins()
|
||||
{
|
||||
if (TryGetDefaultEditor(out var editor))
|
||||
return editor.UseDefaultMargins();
|
||||
else
|
||||
return base.UseDefaultMargins();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7d78f1d1b391ea458ebc768caf0055d
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,87 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only]
|
||||
/// A utility for manually drawing a <see cref="UnityEditor.Editor"/>.
|
||||
/// </summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/CachedEditor
|
||||
public class CachedEditor : IDisposable
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[NonSerialized] private Object[] _Targets = Array.Empty<Object>();
|
||||
[NonSerialized] private UnityEditor.Editor _Editor;
|
||||
[NonSerialized] private bool _WillCleanup;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="UnityEditor.Editor"/> for the `target`
|
||||
/// and caches it to be returned by subsequent calls with the same `target`.
|
||||
/// </summary>
|
||||
public UnityEditor.Editor GetEditor(Object target)
|
||||
{
|
||||
if (_Targets.Length == 1 &&
|
||||
_Targets[0] == target &&
|
||||
_Editor != null)
|
||||
return _Editor;
|
||||
|
||||
Dispose();
|
||||
|
||||
AnimancerUtilities.SetLength(ref _Targets, 1);
|
||||
_Targets[0] = target;
|
||||
_Editor = UnityEditor.Editor.CreateEditor(target);
|
||||
EnsureCleanup();
|
||||
return _Editor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="UnityEditor.Editor"/> for the `targets`
|
||||
/// and caches it to be returned by subsequent calls with the same `targets`.
|
||||
/// </summary>
|
||||
public UnityEditor.Editor GetEditor(Object[] targets)
|
||||
{
|
||||
if (AnimancerUtilities.ContentsAreEqual(targets, _Targets) &&
|
||||
_Editor != null)
|
||||
return _Editor;
|
||||
|
||||
Dispose();
|
||||
|
||||
_Targets = targets;
|
||||
_Editor = UnityEditor.Editor.CreateEditor(targets);
|
||||
EnsureCleanup();
|
||||
return _Editor;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Destroys the cached <see cref="UnityEditor.Editor"/>.</summary>
|
||||
public void Dispose()
|
||||
=> Object.DestroyImmediate(_Editor);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Ensures that <see cref="Dispose"/> will be called before assemblies are reloaded.</summary>
|
||||
private void EnsureCleanup()
|
||||
{
|
||||
if (_WillCleanup)
|
||||
return;
|
||||
|
||||
_WillCleanup = true;
|
||||
|
||||
AssemblyReloadEvents.beforeAssemblyReload += Dispose;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8309947d8cd02bc47957253706973079
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,216 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] A custom Inspector for <see cref="DirectionalAnimationSet4"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/DirectionalAnimationSet4Editor
|
||||
[CustomEditor(typeof(DirectionalAnimationSet4), true)]
|
||||
public class DirectionalAnimationSet4Editor : DirectionalAnimationSetEditor { }
|
||||
|
||||
/// <summary>[Editor-Only] A custom Inspector for <see cref="DirectionalAnimationSet8"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/DirectionalAnimationSet8Editor
|
||||
[CustomEditor(typeof(DirectionalAnimationSet8), true)]
|
||||
public class DirectionalAnimationSet8Editor : DirectionalAnimationSetEditor { }
|
||||
|
||||
/// <summary>[Editor-Only]
|
||||
/// A custom Inspector for
|
||||
/// <see cref="DirectionalAnimationSet4"/> and <see cref="DirectionalAnimationSet8"/>.
|
||||
/// </summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/DirectionalAnimationSetEditor
|
||||
[CanEditMultipleObjects]
|
||||
public class DirectionalAnimationSetEditor : ScriptableObjectEditor
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[MenuItem("CONTEXT/" + nameof(DirectionalAnimationSet2) + "/Find Animations")]
|
||||
[MenuItem("CONTEXT/" + nameof(DirectionalAnimationSet4) + "/Find Animations")]
|
||||
[MenuItem("CONTEXT/" + nameof(DirectionalAnimationSet8) + "/Find Animations")]
|
||||
private static void FindSimilarAnimations(MenuCommand command)
|
||||
{
|
||||
var set = (DirectionalSet<AnimationClip>)command.context;
|
||||
var setName = set.name;
|
||||
|
||||
var directory = AssetDatabase.GetAssetPath(set);
|
||||
directory = Path.GetDirectoryName(directory);
|
||||
|
||||
var guids = AssetDatabase.FindAssets(
|
||||
$"{set.name} t:{nameof(AnimationClip)}",
|
||||
new string[] { directory });
|
||||
|
||||
using (new ModifySerializedField(set, "Find Animations"))
|
||||
{
|
||||
for (int i = 0; i < guids.Length; i++)
|
||||
{
|
||||
var path = AssetDatabase.GUIDToAssetPath(guids[i]);
|
||||
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(path);
|
||||
if (clip == null)
|
||||
continue;
|
||||
|
||||
var clipName = clip.name;
|
||||
if (clipName.StartsWith(setName))
|
||||
clipName = clipName[setName.Length..];
|
||||
|
||||
set.SetByName(clipName, clip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[MenuItem(
|
||||
itemName: Strings.CreateMenuPrefix + "Directional Animation Set/From Selection",
|
||||
priority = Strings.AssetMenuOrder + 7)]
|
||||
private static void CreateDirectionalAnimationSet()
|
||||
{
|
||||
GatherSelectedAnimationClips(out var clips, out var namesLowercase);
|
||||
|
||||
if (clips.Count == 0)
|
||||
throw new InvalidOperationException("No animation clips are selected");
|
||||
else if (clips.Count == 1)
|
||||
throw new InvalidOperationException("Only 1 animation clip is selected");
|
||||
|
||||
var prefix = GetCommonPrefix(namesLowercase);
|
||||
|
||||
var count = clips.Count;
|
||||
DirectionalSet<AnimationClip> set
|
||||
= count <= 2
|
||||
? CreateInstance<DirectionalAnimationSet2>()
|
||||
: count <= 4
|
||||
? CreateInstance<DirectionalAnimationSet4>()
|
||||
: CreateInstance<DirectionalAnimationSet8>();
|
||||
|
||||
set.AllowChanges();
|
||||
for (int i = 0; i < clips.Count; i++)
|
||||
{
|
||||
var name = namesLowercase[i][prefix.Length..];
|
||||
set.SetByName(name, clips[i]);
|
||||
}
|
||||
|
||||
// The prefix is lowercase so get the original case from the first clip.
|
||||
var firstClip = clips[0];
|
||||
var setName = firstClip.name;
|
||||
var nameLength = prefix.Length;
|
||||
while (nameLength > 0)
|
||||
{
|
||||
var character = setName[nameLength - 1];
|
||||
if (char.IsLetterOrDigit(character))
|
||||
break;
|
||||
|
||||
nameLength--;
|
||||
}
|
||||
|
||||
if (nameLength <= 0)
|
||||
nameLength = prefix.Length;
|
||||
|
||||
setName = setName[..nameLength];
|
||||
|
||||
var path = AssetDatabase.GetAssetPath(firstClip);
|
||||
path = $"{Path.GetDirectoryName(path)}/{setName}.asset";
|
||||
AssetDatabase.CreateAsset(set, path);
|
||||
|
||||
Selection.objects = new Object[] { set };
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
public static void GatherSelectedAnimationClips(
|
||||
out List<AnimationClip> clips,
|
||||
out List<string> namesLowercase)
|
||||
{
|
||||
clips = new();
|
||||
namesLowercase = new();
|
||||
|
||||
var selection = Selection.objects;
|
||||
for (int i = 0; i < selection.Length; i++)
|
||||
{
|
||||
var clip = selection[i] as AnimationClip;
|
||||
if (clip == null)
|
||||
continue;
|
||||
|
||||
clips.Add(clip);
|
||||
namesLowercase.Add(clip.name.ToLower());
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string containing the section from the start of each of the `strings`
|
||||
/// which is exactly the same.
|
||||
/// </summary>
|
||||
public static string GetCommonPrefix(IList<string> strings)
|
||||
{
|
||||
if (strings == null ||
|
||||
strings.Count == 0)
|
||||
return "";
|
||||
|
||||
// Start with the first string as the candidate prefix.
|
||||
var prefix = strings[0];
|
||||
|
||||
for (int i = 1; i < strings.Count; i++)
|
||||
{
|
||||
var current = strings[i];
|
||||
var length = Math.Min(prefix.Length, current.Length);
|
||||
|
||||
// Find the common prefix length between the prefix and current string.
|
||||
int j;
|
||||
for (j = 0; j < length; j++)
|
||||
if (prefix[j] != current[j])
|
||||
break;
|
||||
|
||||
// Shorten the prefix to the common part.
|
||||
prefix = prefix[..j];
|
||||
|
||||
// Early exit if there's no common prefix left.
|
||||
if (prefix.Length == 0)
|
||||
break;
|
||||
}
|
||||
|
||||
return prefix;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[MenuItem("CONTEXT/" + nameof(DirectionalAnimationSet2) + "/Toggle Looping")]
|
||||
[MenuItem("CONTEXT/" + nameof(DirectionalAnimationSet4) + "/Toggle Looping")]
|
||||
[MenuItem("CONTEXT/" + nameof(DirectionalAnimationSet8) + "/Toggle Looping")]
|
||||
private static void ToggleLooping(MenuCommand command)
|
||||
{
|
||||
var set = (DirectionalSet<AnimationClip>)command.context;
|
||||
|
||||
var count = set.DirectionCount;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var clip = set.Get(i);
|
||||
if (clip == null)
|
||||
continue;
|
||||
|
||||
var isLooping = !clip.isLooping;
|
||||
for (i = 0; i < count; i++)
|
||||
{
|
||||
clip = set.Get(i);
|
||||
if (clip == null)
|
||||
continue;
|
||||
|
||||
AnimancerEditorUtilities.SetLooping(clip, isLooping);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f1c90fd4c20410f4ea2e482d97f24276
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,48 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
// Uncomment this #define to apply this custom editor to all ScriptableObjects.
|
||||
// If you have another plugin with a custom ScriptableObject editor, you will probably want that one instead.
|
||||
//#define ANIMANCER_SCRIPTABLE_OBJECT_EDITOR
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only]
|
||||
/// A custom Inspector for <see cref="ScriptableObject"/>s which adds a message explaining that changes in play
|
||||
/// mode will persist.
|
||||
/// </summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ScriptableObjectEditor
|
||||
///
|
||||
#if ANIMANCER_SCRIPTABLE_OBJECT_EDITOR
|
||||
[CustomEditor(typeof(ScriptableObject), true, isFallback = true), CanEditMultipleObjects]
|
||||
#endif
|
||||
public class ScriptableObjectEditor : UnityEditor.Editor
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the regular Inspector then adds a message explaining that changes in Play Mode will persist.</summary>
|
||||
/// <remarks>Called by the Unity editor to draw the custom Inspector GUI elements.</remarks>
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
base.OnInspectorGUI();
|
||||
|
||||
if (target != null &&
|
||||
EditorApplication.isPlayingOrWillChangePlaymode &&
|
||||
EditorUtility.IsPersistent(target))
|
||||
{
|
||||
EditorGUILayout.HelpBox("This is an asset, not a scene object," +
|
||||
" which means that any changes you make to it are permanent" +
|
||||
" and will NOT be undone when you exit Play Mode.", MessageType.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3823dee181bc7dc46b20e907cd32dbeb
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,221 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] A custom Inspector for <see cref="SoloAnimation"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/SoloAnimationEditor
|
||||
[UnityEditor.CustomEditor(typeof(SoloAnimation), true), UnityEditor.CanEditMultipleObjects]
|
||||
public class SoloAnimationEditor : UnityEditor.Editor
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The <see cref="UnityEditor.Editor.target"/>.</summary>
|
||||
[field: NonSerialized]
|
||||
public SoloAnimation Target { get; private set; }
|
||||
|
||||
/// <summary>The <see cref="UnityEditor.Editor.targets"/>.</summary>
|
||||
[field: NonSerialized]
|
||||
public Object[] Targets { get; private set; }
|
||||
|
||||
/// <summary>The animator referenced by each target.</summary>
|
||||
[NonSerialized]
|
||||
private Animator[] _Animators;
|
||||
|
||||
/// <summary>A <see cref="UnityEditor.SerializedObject"/> encapsulating the <see cref="_Animators"/>.</summary>
|
||||
[NonSerialized]
|
||||
private UnityEditor.SerializedObject _SerializedAnimator;
|
||||
|
||||
/// <summary>The <see cref="Animator.keepAnimatorStateOnDisable"/> property.</summary>
|
||||
[NonSerialized]
|
||||
private UnityEditor.SerializedProperty _KeepStateOnDisable;
|
||||
|
||||
/// <summary>The backing field of the <see cref="Animator.keepAnimatorStateOnDisable"/> property.</summary>
|
||||
private const string KeeyStateOnDisableField = "m_KeepAnimatorStateOnDisable";
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Initializes the targets.</summary>
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
Target = (SoloAnimation)target;
|
||||
Targets = targets;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
DoSerializedFieldsGUI();
|
||||
RefreshSerializedAnimator();
|
||||
DoStopOnDisableGUI();
|
||||
DoRuntimeDetailsGUI();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the target's serialized fields.</summary>
|
||||
private void DoSerializedFieldsGUI()
|
||||
{
|
||||
serializedObject.Update();
|
||||
|
||||
var property = serializedObject.GetIterator();
|
||||
|
||||
property.NextVisible(true);
|
||||
|
||||
if (property.name != "m_Script")
|
||||
UnityEditor.EditorGUILayout.PropertyField(property, true);
|
||||
|
||||
while (property.NextVisible(false))
|
||||
{
|
||||
UnityEditor.EditorGUILayout.PropertyField(property, true);
|
||||
}
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Ensures that the cached references relating to the target's <see cref="Animator"/> are correct.</summary>
|
||||
private void RefreshSerializedAnimator()
|
||||
{
|
||||
AnimancerUtilities.SetLength(ref _Animators, Targets.Length);
|
||||
|
||||
var dirty = false;
|
||||
var hasAll = true;
|
||||
|
||||
for (int i = 0; i < _Animators.Length; i++)
|
||||
{
|
||||
var animator = (Targets[i] as SoloAnimation).Animator;
|
||||
if (_Animators[i] != animator)
|
||||
{
|
||||
_Animators[i] = animator;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
if (animator == null)
|
||||
hasAll = false;
|
||||
}
|
||||
|
||||
if (!dirty)
|
||||
return;
|
||||
|
||||
OnDisable();
|
||||
|
||||
if (!hasAll)
|
||||
return;
|
||||
|
||||
_SerializedAnimator = new(_Animators);
|
||||
_KeepStateOnDisable = _SerializedAnimator.FindProperty(KeeyStateOnDisableField);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Draws a toggle inverted from the <see cref="Animator.keepAnimatorStateOnDisable"/> field.
|
||||
/// </summary>
|
||||
private void DoStopOnDisableGUI()
|
||||
{
|
||||
var area = AnimancerGUI.LayoutSingleLineRect();
|
||||
|
||||
using (var label = PooledGUIContent.Acquire("Stop On Disable",
|
||||
"If true, disabling this object will stop and rewind the animation." +
|
||||
" Otherwise it will simply be paused so it can resume from there when re-enabled."))
|
||||
{
|
||||
if (_KeepStateOnDisable != null)
|
||||
{
|
||||
_KeepStateOnDisable.serializedObject.Update();
|
||||
|
||||
var content = UnityEditor.EditorGUI.BeginProperty(area, label, _KeepStateOnDisable);
|
||||
|
||||
_KeepStateOnDisable.boolValue = !UnityEditor.EditorGUI.Toggle(area, content, !_KeepStateOnDisable.boolValue);
|
||||
|
||||
UnityEditor.EditorGUI.EndProperty();
|
||||
|
||||
_KeepStateOnDisable.serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
else
|
||||
{
|
||||
label.tooltip = $"Unable to locate field: {nameof(Animator)}.{KeeyStateOnDisableField}";
|
||||
using (new UnityEditor.EditorGUI.DisabledScope(true))
|
||||
UnityEditor.EditorGUI.Toggle(area, label, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the target's runtime details.</summary>
|
||||
private void DoRuntimeDetailsGUI()
|
||||
{
|
||||
if (Targets.Length != 1)
|
||||
return;
|
||||
|
||||
if (!UnityEditor.EditorApplication.isPlaying &&
|
||||
!Target.ApplyInEditMode)
|
||||
return;
|
||||
|
||||
AnimancerGUI.BeginVerticalBox(GUI.skin.box);
|
||||
|
||||
if (!Target.IsInitialized)
|
||||
{
|
||||
GUILayout.Label("Not Initialized");
|
||||
}
|
||||
else
|
||||
{
|
||||
UnityEditor.EditorGUILayout.LabelField("Playable Graph", "Not Serialized");
|
||||
|
||||
UnityEditor.EditorGUI.BeginChangeCheck();
|
||||
var isPlaying = UnityEditor.EditorGUILayout.Toggle("Is Playing", Target.IsPlaying);
|
||||
if (UnityEditor.EditorGUI.EndChangeCheck())
|
||||
Target.IsPlaying = isPlaying;
|
||||
|
||||
UnityEditor.EditorGUI.BeginChangeCheck();
|
||||
var time = UnityEditor.EditorGUILayout.FloatField("Time", Target.Time);
|
||||
if (UnityEditor.EditorGUI.EndChangeCheck())
|
||||
{
|
||||
Target.Time = time;
|
||||
Target.Evaluate();
|
||||
}
|
||||
|
||||
time = AnimancerUtilities.Wrap01(Target.NormalizedTime);
|
||||
if (time == 0 && Target.Time != 0)
|
||||
time = 1;
|
||||
|
||||
UnityEditor.EditorGUI.BeginChangeCheck();
|
||||
time = UnityEditor.EditorGUILayout.Slider("Normalized Time", time, 0, 1);
|
||||
if (UnityEditor.EditorGUI.EndChangeCheck())
|
||||
{
|
||||
Target.NormalizedTime = time;
|
||||
Target.Evaluate();
|
||||
}
|
||||
}
|
||||
|
||||
AnimancerGUI.EndVerticalBox(GUI.skin.box);
|
||||
Repaint();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Cleans up cached references relating to the target's <see cref="Animator"/>.</summary>
|
||||
protected virtual void OnDisable()
|
||||
{
|
||||
if (_SerializedAnimator != null)
|
||||
{
|
||||
_SerializedAnimator.Dispose();
|
||||
_SerializedAnimator = null;
|
||||
_KeepStateOnDisable = null;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8e8da6481b04c094f9e611a32da486f5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,726 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
|
||||
using Animancer.Editor.Tools;
|
||||
using Animancer.Units;
|
||||
using Animancer.Units.Editor;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only]
|
||||
/// A custom Inspector for <see cref="Sprite"/>s which allows you to directly edit them instead of just showing
|
||||
/// their details like the default one does.
|
||||
/// </summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/SpriteEditor
|
||||
[CustomEditor(typeof(Sprite), true), CanEditMultipleObjects]
|
||||
public class SpriteEditor : UnityEditor.Editor
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private const string
|
||||
NameTooltip = "The asset name of the sprite",
|
||||
RectTooltip = "The texture area occupied by the sprite",
|
||||
PivotTooltip = "The origin point of the sprite relative to its Rect",
|
||||
BorderTooltip = "The edge sizes used when 9-Slicing the sprite for the UI system (ignored by SpriteRenderers)";
|
||||
|
||||
[NonSerialized]
|
||||
private SerializedProperty
|
||||
_Name,
|
||||
_Rect,
|
||||
_Pivot,
|
||||
_Border;
|
||||
|
||||
[NonSerialized]
|
||||
private NormalizedPixelField[]
|
||||
_RectFields,
|
||||
_PivotFields,
|
||||
_BorderFields;
|
||||
|
||||
[NonSerialized]
|
||||
private bool _HasBeenModified;
|
||||
|
||||
[NonSerialized]
|
||||
private Target[] _Targets;
|
||||
|
||||
private readonly struct Target
|
||||
{
|
||||
public readonly Sprite Sprite;
|
||||
public readonly string AssetPath;
|
||||
public readonly TextureImporter Importer;
|
||||
|
||||
public Target(Object target)
|
||||
{
|
||||
Sprite = target as Sprite;
|
||||
AssetPath = AssetDatabase.GetAssetPath(target);
|
||||
Importer = AssetImporter.GetAtPath(AssetPath) as TextureImporter;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Initializes this editor.</summary>
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
var targets = this.targets;
|
||||
_Targets = new Target[targets.Length];
|
||||
for (int i = 0; i < targets.Length; i++)
|
||||
_Targets[i] = new(targets[i]);
|
||||
|
||||
InitializePreview();
|
||||
|
||||
_Name = serializedObject.FindProperty($"m{nameof(_Name)}");
|
||||
|
||||
_Rect = serializedObject.FindProperty($"m{nameof(_Rect)}");
|
||||
if (_Rect != null)
|
||||
{
|
||||
_RectFields = new NormalizedPixelField[]
|
||||
{
|
||||
new(_Rect.FindPropertyRelative(nameof(Rect.x)), new("X (Left)",
|
||||
"The distance from the left edge of the texture to the left edge of the sprite"), false),
|
||||
new(_Rect.FindPropertyRelative(nameof(Rect.y)), new("Y (Bottom)",
|
||||
"The distance from the bottom edge of the texture to the bottom edge of the sprite"), false),
|
||||
new(_Rect.FindPropertyRelative(nameof(Rect.width)), new("Width",
|
||||
"The horizontal size of the sprite"), false),
|
||||
new(_Rect.FindPropertyRelative(nameof(Rect.height)), new("Height",
|
||||
"The vertical size of the sprite"), false),
|
||||
};
|
||||
}
|
||||
|
||||
_Pivot = serializedObject.FindProperty($"m{nameof(_Pivot)}");
|
||||
if (_Pivot != null)
|
||||
{
|
||||
_PivotFields = new NormalizedPixelField[]
|
||||
{
|
||||
new(_Pivot.FindPropertyRelative(nameof(Vector2.x)), new("X",
|
||||
"The horizontal distance from the left edge of the sprite to the pivot point"), true),
|
||||
new(_Pivot.FindPropertyRelative(nameof(Vector2.y)), new("Y",
|
||||
"The vertical distance from the bottom edge of the sprite to the pivot point"), true),
|
||||
};
|
||||
}
|
||||
|
||||
_Border = serializedObject.FindProperty($"m{nameof(_Border)}");
|
||||
if (_Border != null)
|
||||
{
|
||||
_BorderFields = new NormalizedPixelField[]
|
||||
{
|
||||
new(_Border.FindPropertyRelative(nameof(Vector4.x)), new("Left",
|
||||
BorderTooltip), false),
|
||||
new(_Border.FindPropertyRelative(nameof(Vector4.y)), new("Bottom",
|
||||
BorderTooltip), false),
|
||||
new(_Border.FindPropertyRelative(nameof(Vector4.z)), new("Right",
|
||||
BorderTooltip), false),
|
||||
new(_Border.FindPropertyRelative(nameof(Vector4.w)), new("Top",
|
||||
BorderTooltip), false),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Cleans up this editor.</summary>
|
||||
protected virtual void OnDisable()
|
||||
{
|
||||
CleanUpPreview();
|
||||
|
||||
if (_HasBeenModified)
|
||||
{
|
||||
var sprite = target as Sprite;
|
||||
if (sprite == null)
|
||||
return;
|
||||
|
||||
if (EditorUtility.DisplayDialog("Unapplied Import Settings",
|
||||
$"Unapplied import settings for '{sprite.name}' in '{AssetDatabase.GetAssetPath(sprite)}'",
|
||||
nameof(Apply), nameof(Revert)))
|
||||
Apply();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Inspector
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Are all targets set to <see cref="SpriteImportMode.Multiple"/>?</summary>
|
||||
private bool AllSpriteModeMultiple
|
||||
{
|
||||
get
|
||||
{
|
||||
for (int i = 0; i < _Targets.Length; i++)
|
||||
{
|
||||
var importer = _Targets[i].Importer;
|
||||
if (importer == null ||
|
||||
importer.spriteImportMode != SpriteImportMode.Multiple)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Called by the Unity editor to draw the custom Inspector GUI elements.</summary>
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
EditorGUI.BeginChangeCheck();
|
||||
|
||||
DoNameGUI();
|
||||
|
||||
// If any target isn't set to Multiple, disable the GUI because only renaming will work.
|
||||
var enabled = GUI.enabled;
|
||||
if (!AllSpriteModeMultiple)
|
||||
GUI.enabled = false;
|
||||
|
||||
DoRectGUI();
|
||||
DoPivotGUI();
|
||||
DoBorderGUI();
|
||||
|
||||
GUI.enabled = enabled;
|
||||
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
_HasBeenModified = true;
|
||||
|
||||
GUILayout.Space(AnimancerGUI.StandardSpacing);
|
||||
GUILayout.BeginHorizontal();
|
||||
{
|
||||
GUILayout.FlexibleSpace();
|
||||
GUI.enabled = _HasBeenModified;
|
||||
if (GUILayout.Button(nameof(Revert)))
|
||||
Revert();
|
||||
if (GUILayout.Button(nameof(Apply)))
|
||||
Apply();
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void DoNameGUI()
|
||||
{
|
||||
GUILayout.BeginHorizontal();
|
||||
var enabled = GUI.enabled;
|
||||
|
||||
if (_Name.hasMultipleDifferentValues)
|
||||
GUI.enabled = false;
|
||||
|
||||
using (var label = PooledGUIContent.Acquire("Name", NameTooltip))
|
||||
EditorGUILayout.PropertyField(_Name, label, true);
|
||||
|
||||
GUI.enabled = true;
|
||||
|
||||
var changed = EditorGUI.EndChangeCheck();// Exclude the Rename button from the main change check.
|
||||
|
||||
if (GUILayout.Button("Rename Tool", EditorStyles.miniButton, AnimancerGUI.DontExpandWidth))
|
||||
AnimancerToolsWindow.Open(typeof(RenameSpritesTool));
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
AnimancerGUI.SetGuiChanged(changed);
|
||||
|
||||
GUI.enabled = enabled;
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void DoRectGUI()
|
||||
{
|
||||
if (target is not Sprite sprite)
|
||||
return;
|
||||
|
||||
var texture = sprite.texture;
|
||||
if (texture == null)
|
||||
return;
|
||||
|
||||
_RectFields[0].normalizeMultiplier = _RectFields[2].normalizeMultiplier = 1f / texture.width;
|
||||
_RectFields[1].normalizeMultiplier = _RectFields[3].normalizeMultiplier = 1f / texture.height;
|
||||
|
||||
using (var label = PooledGUIContent.Acquire("Rect", RectTooltip))
|
||||
NormalizedPixelField.DoGroupGUI(_Rect, label, _RectFields);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void DoPivotGUI()
|
||||
{
|
||||
var showMixedValue = EditorGUI.showMixedValue;
|
||||
|
||||
var targets = this.targets;
|
||||
var size = targets[0] is Sprite sprite ? sprite.rect.size : Vector2.one;
|
||||
for (int i = 1; i < targets.Length; i++)
|
||||
{
|
||||
sprite = targets[i] as Sprite;
|
||||
if (sprite == null || !sprite.rect.size.Equals(size))
|
||||
EditorGUI.showMixedValue = true;
|
||||
}
|
||||
|
||||
_PivotFields[0].normalizeMultiplier = 1f / size.x;
|
||||
_PivotFields[1].normalizeMultiplier = 1f / size.y;
|
||||
|
||||
using (var label = PooledGUIContent.Acquire("Pivot", PivotTooltip))
|
||||
NormalizedPixelField.DoGroupGUI(_Pivot, label, _PivotFields);
|
||||
|
||||
EditorGUI.showMixedValue = showMixedValue;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void DoBorderGUI()
|
||||
{
|
||||
var size = _Rect.rectValue.size;
|
||||
_BorderFields[0].normalizeMultiplier = _BorderFields[2].normalizeMultiplier = 1f / size.x;
|
||||
_BorderFields[1].normalizeMultiplier = _BorderFields[3].normalizeMultiplier = 1f / size.y;
|
||||
|
||||
using (var label = PooledGUIContent.Acquire("Border", BorderTooltip))
|
||||
NormalizedPixelField.DoGroupGUI(_Border, label, _BorderFields);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void Revert()
|
||||
{
|
||||
AnimancerGUI.Deselect();
|
||||
_HasBeenModified = false;
|
||||
serializedObject.Update();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void Apply()
|
||||
{
|
||||
AnimancerGUI.Deselect();
|
||||
_HasBeenModified = false;
|
||||
var targets = this.targets;
|
||||
|
||||
var hasError = false;
|
||||
|
||||
for (int i = 0; i < _Targets.Length; i++)
|
||||
{
|
||||
var target = _Targets[i];
|
||||
if (target.Sprite == null ||
|
||||
target.Importer == null)
|
||||
continue;
|
||||
|
||||
var data = new SpriteDataEditor(target.Importer);
|
||||
Apply(data, target.Sprite, ref hasError);
|
||||
|
||||
if (!hasError)
|
||||
data.Apply();
|
||||
}
|
||||
|
||||
for (int i = 0; i < targets.Length; i++)
|
||||
if (targets[i] == null)
|
||||
return;
|
||||
|
||||
serializedObject.Update();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void Apply(SpriteDataEditor data, Sprite sprite, ref bool hasError)
|
||||
{
|
||||
if (data.SpriteCount == 0)
|
||||
{
|
||||
if (!_Name.hasMultipleDifferentValues)
|
||||
{
|
||||
var path = AssetDatabase.GetAssetPath(sprite);
|
||||
if (path != null)
|
||||
{
|
||||
AssetDatabase.RenameAsset(path, _Name.stringValue);
|
||||
hasError = true;// Don't apply the importer.
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var index = data.IndexOf(sprite);
|
||||
if (index < 0)
|
||||
{
|
||||
hasError = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_Name.hasMultipleDifferentValues)
|
||||
data.SetName(index, _Name.stringValue);
|
||||
|
||||
if (!_Rect.hasMultipleDifferentValues)
|
||||
data.SetRect(index, _Rect.rectValue);
|
||||
|
||||
if (!_Pivot.hasMultipleDifferentValues)
|
||||
data.SetPivot(index, _Pivot.vector2Value);
|
||||
|
||||
if (!_Border.hasMultipleDifferentValues)
|
||||
data.SetBorder(index, _Border.vector4Value);
|
||||
|
||||
if (!data.ValidateBounds(index, sprite))
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Normalized Pixel Field
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// A wrapper around a <see cref="SerializedProperty"/> to display it using two float fields where one is
|
||||
/// normalized and the other is not.
|
||||
/// </summary>
|
||||
private class NormalizedPixelField
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The target property.</summary>
|
||||
public readonly SerializedProperty Property;
|
||||
|
||||
/// <summary>The label to display next to the property.</summary>
|
||||
public readonly GUIContent Label;
|
||||
|
||||
/// <summary>Is the serialized property value normalized?</summary>
|
||||
public readonly bool IsNormalized;
|
||||
|
||||
/// <summary>The multiplier to turn a non-normalized value into a normalized one.</summary>
|
||||
public float normalizeMultiplier;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="NormalizedPixelField"/>.</summary>
|
||||
public NormalizedPixelField(SerializedProperty property, GUIContent label, bool isNormalized)
|
||||
{
|
||||
Property = property;
|
||||
Label = label;
|
||||
IsNormalized = isNormalized;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws a group of <see cref="NormalizedPixelField"/>s.</summary>
|
||||
public static void DoGroupGUI(SerializedProperty baseProperty, GUIContent label, NormalizedPixelField[] fields)
|
||||
{
|
||||
var height = (AnimancerGUI.LineHeight + AnimancerGUI.StandardSpacing) * (fields.Length + 1);
|
||||
var area = AnimancerGUI.LayoutRect(height);
|
||||
|
||||
area.height = AnimancerGUI.LineHeight;
|
||||
label = EditorGUI.BeginProperty(area, label, baseProperty);
|
||||
GUI.Label(area, label);
|
||||
EditorGUI.EndProperty();
|
||||
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
for (int i = 0; i < fields.Length; i++)
|
||||
{
|
||||
AnimancerGUI.NextVerticalArea(ref area);
|
||||
fields[i].DoTwinFloatFieldGUI(area);
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws this <see cref="NormalizedPixelField"/>.</summary>
|
||||
public void DoTwinFloatFieldGUI(Rect area)
|
||||
{
|
||||
var attribute = IsNormalized ?
|
||||
NormalizedPixelFieldAttribute.Normalized :
|
||||
NormalizedPixelFieldAttribute.Pixel;
|
||||
|
||||
var drawer = IsNormalized ?
|
||||
NormalizedPixelFieldAttributeDrawer.Normalized :
|
||||
NormalizedPixelFieldAttributeDrawer.Pixel;
|
||||
|
||||
attribute.CalculateMultipliers(normalizeMultiplier);
|
||||
drawer.OnGUI(area, Property, Label);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Normalized Pixel Field
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private class NormalizedPixelFieldAttribute : UnitsAttribute
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static new readonly float[] Multipliers = new float[2];
|
||||
|
||||
public void CalculateMultipliers(float normalizeMultiplier)
|
||||
{
|
||||
if (UnitIndex == 0)// Pixels.
|
||||
{
|
||||
Multipliers[0] = 1;
|
||||
Multipliers[1] = normalizeMultiplier;
|
||||
}
|
||||
else// Normalized.
|
||||
{
|
||||
Multipliers[0] = 1f / normalizeMultiplier;
|
||||
Multipliers[1] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static new readonly string[] Suffixes =
|
||||
{
|
||||
"px",
|
||||
"x",
|
||||
};
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
public static readonly NormalizedPixelFieldAttribute Pixel = new(false);
|
||||
public static readonly NormalizedPixelFieldAttribute Normalized = new(true);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
public NormalizedPixelFieldAttribute(bool isNormalized)
|
||||
: base(Multipliers, Suffixes, isNormalized ? 1 : 0)
|
||||
{
|
||||
Rule = Validate.Value.IsFinite;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[CustomPropertyDrawer(typeof(NormalizedPixelFieldAttribute), true)]
|
||||
private class NormalizedPixelFieldAttributeDrawer : UnitsAttributeDrawer
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
public static readonly NormalizedPixelFieldAttributeDrawer Pixel = new();
|
||||
public static readonly NormalizedPixelFieldAttributeDrawer Normalized = new();
|
||||
|
||||
static NormalizedPixelFieldAttributeDrawer()
|
||||
{
|
||||
Pixel.Initialize(NormalizedPixelFieldAttribute.Pixel);
|
||||
Normalized.Initialize(NormalizedPixelFieldAttribute.Normalized);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override int GetLineCount(SerializedProperty property, GUIContent label)
|
||||
=> 1;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Preview
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static readonly Type
|
||||
DefaultEditorType = typeof(UnityEditor.Editor).Assembly.GetType("UnityEditor.SpriteInspector");
|
||||
|
||||
private readonly Dictionary<Object, UnityEditor.Editor>
|
||||
TargetToDefaultEditor = new();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void InitializePreview()
|
||||
{
|
||||
foreach (var target in targets)
|
||||
{
|
||||
if (!TargetToDefaultEditor.ContainsKey(target))
|
||||
{
|
||||
var editor = CreateEditor(target, DefaultEditorType);
|
||||
TargetToDefaultEditor.Add(target, editor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void CleanUpPreview()
|
||||
{
|
||||
foreach (var editor in TargetToDefaultEditor.Values)
|
||||
DestroyImmediate(editor);
|
||||
|
||||
TargetToDefaultEditor.Clear();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private bool TryGetDefaultEditor(out UnityEditor.Editor editor)
|
||||
=> TargetToDefaultEditor.TryGetValue(target, out editor);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string GetInfoString()
|
||||
{
|
||||
if (!TryGetDefaultEditor(out var editor))
|
||||
return null;
|
||||
|
||||
return editor.GetInfoString();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Texture2D RenderStaticPreview(string assetPath, Object[] subAssets, int width, int height)
|
||||
{
|
||||
if (!TryGetDefaultEditor(out var editor))
|
||||
return null;
|
||||
|
||||
return editor.RenderStaticPreview(assetPath, subAssets, width, height);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool HasPreviewGUI()
|
||||
{
|
||||
return TryGetDefaultEditor(out var editor) && editor.HasPreviewGUI();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnPreviewGUI(Rect area, GUIStyle background)
|
||||
{
|
||||
if (TryGetDefaultEditor(out var editor))
|
||||
editor.OnPreviewGUI(area, background);
|
||||
|
||||
var sprite = target as Sprite;
|
||||
if (sprite == null)
|
||||
return;
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
FitAspectRatio(ref area, sprite);
|
||||
DoPivotDotGUI(area, sprite);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
_HasBeenModified = true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static void FitAspectRatio(ref Rect area, Sprite sprite)
|
||||
{
|
||||
var areaAspect = area.width / area.height;
|
||||
var spriteAspect = sprite.rect.width / sprite.rect.height;
|
||||
if (areaAspect != spriteAspect)
|
||||
{
|
||||
if (areaAspect > spriteAspect)
|
||||
{
|
||||
var width = area.height * spriteAspect;
|
||||
area.x += (area.width - width) * 0.5f;
|
||||
area.width = width;
|
||||
}
|
||||
else
|
||||
{
|
||||
var height = area.width / spriteAspect;
|
||||
area.y += (area.height - height) * 0.5f;
|
||||
area.height = height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static readonly int PivotDotControlIDHint = "PivotDot".GetHashCode();
|
||||
|
||||
private static GUIStyle _PivotDot;
|
||||
private static GUIStyle _PivotDotActive;
|
||||
|
||||
[NonSerialized] private Vector2 _MouseDownPivot;
|
||||
|
||||
private void DoPivotDotGUI(Rect area, Sprite sprite)
|
||||
{
|
||||
_PivotDot ??= "U2D.pivotDot";
|
||||
_PivotDotActive ??= "U2D.pivotDotActive";
|
||||
|
||||
Vector2 pivot;
|
||||
if (_Pivot.hasMultipleDifferentValues)
|
||||
{
|
||||
pivot = sprite.pivot;
|
||||
pivot.x /= sprite.rect.width;
|
||||
pivot.y /= sprite.rect.height;
|
||||
}
|
||||
else
|
||||
{
|
||||
pivot = _Pivot.vector2Value;
|
||||
}
|
||||
pivot.x *= area.width;
|
||||
pivot.y *= area.height;
|
||||
|
||||
var pivotArea = new Rect(
|
||||
area.x + pivot.x - _PivotDot.fixedWidth * 0.5f,
|
||||
area.yMax - pivot.y - _PivotDot.fixedHeight * 0.5f,
|
||||
_PivotDot.fixedWidth,
|
||||
_PivotDot.fixedHeight);
|
||||
|
||||
var control = new GUIControl(pivotArea, PivotDotControlIDHint, FocusType.Keyboard);
|
||||
|
||||
switch (control.EventType)
|
||||
{
|
||||
case EventType.MouseDown:
|
||||
if (control.Event.button == 0 &&
|
||||
!control.Event.alt &&
|
||||
control.TryUseMouseDown())
|
||||
{
|
||||
_MouseDownPivot = _Pivot.vector2Value;
|
||||
GUIUtility.keyboardControl = control.ID;
|
||||
}
|
||||
break;
|
||||
|
||||
case EventType.MouseUp:
|
||||
if (control.TryUseMouseUp())
|
||||
GUIUtility.keyboardControl = 0;
|
||||
break;
|
||||
|
||||
case EventType.MouseDrag:
|
||||
if (control.TryUseHotControl())
|
||||
{
|
||||
pivot = control.Event.mousePosition;
|
||||
pivot.x = AnimancerUtilities.InverseLerpUnclamped(area.x, area.xMax, pivot.x);
|
||||
pivot.y = AnimancerUtilities.InverseLerpUnclamped(area.yMax, area.y, pivot.y);
|
||||
|
||||
if (control.Event.control)
|
||||
{
|
||||
var rect = sprite.rect;
|
||||
pivot.x = Mathf.Round(pivot.x * rect.width) / rect.width;
|
||||
pivot.y = Mathf.Round(pivot.y * rect.height) / rect.height;
|
||||
}
|
||||
|
||||
_Pivot.vector2Value = pivot;
|
||||
}
|
||||
break;
|
||||
|
||||
case EventType.KeyDown:
|
||||
if (control.TryUseKey(KeyCode.Escape))
|
||||
{
|
||||
_Pivot.vector2Value = _MouseDownPivot;
|
||||
AnimancerGUI.Deselect();
|
||||
}
|
||||
break;
|
||||
|
||||
case EventType.Repaint:
|
||||
EditorGUIUtility.AddCursorRect(pivotArea, MouseCursor.Arrow, control.ID);
|
||||
var style = GUIUtility.hotControl == control.ID ? _PivotDotActive : _PivotDot;
|
||||
style.Draw(pivotArea, GUIContent.none, control.ID);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9ac5522110c9917498cdf2630222fd4c
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,37 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using UnityEditor;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] A custom Inspector for <see cref="StringAsset"/>s.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/StringAssetEditor
|
||||
[CustomEditor(typeof(StringAsset), true)]
|
||||
public class StringAssetEditor : UnityEditor.Editor
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private const string InfoMessage = "This is a String Asset." +
|
||||
"\n\nThe name of this asset is what differentiates it from others" +
|
||||
" so it should be unique to avoid conflicts." +
|
||||
"\n\nThe Editor Comment field isn't used for anything and is excluded from runtime builds." +
|
||||
" It's recommended to explain what you're using this key for.";
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
EditorGUILayout.HelpBox(InfoMessage, MessageType.Info);
|
||||
|
||||
DrawDefaultInspector();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: df8eb3f6b6415f3448c16a2f28301d33
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user