chore: initial commit

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

View File

@@ -0,0 +1,105 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using UnityEditor;
namespace Animancer.Editor
{
/// <summary>[Editor-Only]
/// A simple wrapper around <see cref="EditorPrefs"/> to get and set a bool.
/// <para></para>
/// If you're interested in a more comprehensive pref wrapper that supports more types, you should check out
/// <see href="https://kybernetik.com.au/inspector-gadgets/docs/other/auto-prefs">Inspector Gadgets - Auto Prefs</see>.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/BoolPref
///
public class BoolPref
{
/************************************************************************************************************************/
/// <summary>The prefix which is automatically added before the <see cref="Key"/>.</summary>
public const string KeyPrefix = nameof(Animancer) + "/";
/// <summary>The identifier with which this pref will be saved.</summary>
public readonly string Key;
/// <summary>The label to use when adding a function to toggle this pref to a menu.</summary>
public readonly string MenuItem;
/// <summary>The starting value to use for this pref if none was previously saved.</summary>
public readonly bool DefaultValue;
/************************************************************************************************************************/
private bool _HasValue;
private bool _Value;
/// <summary>The current value of this pref.</summary>
public bool Value
{
get
{
if (!_HasValue)
{
_HasValue = true;
_Value = EditorPrefs.GetBool(Key, DefaultValue);
}
return _Value;
}
set
{
if (_Value == value &&
_HasValue)
return;
_Value = value;
_HasValue = true;
EditorPrefs.SetBool(Key, value);
}
}
/// <summary>Returns the current value of the `pref`.</summary>
public static implicit operator bool(BoolPref pref)
=> pref.Value;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="BoolPref"/>.</summary>
public BoolPref(string menuItem, bool defaultValue = default)
: this(null, menuItem, defaultValue) { }
/// <summary>Creates a new <see cref="BoolPref"/>.</summary>
public BoolPref(string keyPrefix, string menuItem, bool defaultValue = default)
{
MenuItem = menuItem + " ?";
Key = KeyPrefix + keyPrefix + menuItem;
DefaultValue = defaultValue;
}
/************************************************************************************************************************/
/// <summary>Adds a menu function to toggle the <see cref="Value"/> of this pref.</summary>
public void AddToggleFunction(GenericMenu menu)
{
menu.AddItem(new(MenuItem), Value, () =>
{
Value = !Value;
});
}
/************************************************************************************************************************/
/// <summary>Returns a string containing the <see cref="Key"/> and <see cref="Value"/>.</summary>
public override string ToString()
=> $"{nameof(BoolPref)} (" +
$"{nameof(Key)} = '{Key}'" +
$", {nameof(Value)} = {Value})";
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,132 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using System;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] A <see cref="SerializableAttribute"/> reference to a <see cref="Type"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/SerializableTypeReference
[Serializable]
public struct SerializableTypeReference : ISerializationCallbackReceiver
{
/************************************************************************************************************************/
[SerializeField]
private string _QualifiedName;
private Type _Type;
/************************************************************************************************************************/
/// <summary>[<see cref="SerializeField"/>] The <see cref="Type.AssemblyQualifiedName"/>.</summary>
public string QualifiedName
{
readonly get => _QualifiedName;
set
{
if (_QualifiedName == value)
return;
_QualifiedName = value;
_Type = null;
}
}
/************************************************************************************************************************/
/// <summary>The referenced type.</summary>
public Type Type
{
get
{
if (_Type == null && !string.IsNullOrEmpty(_QualifiedName))
_Type = Type.GetType(_QualifiedName);
return _Type;
}
set
{
if (_Type == value)
return;
_QualifiedName = value?.AssemblyQualifiedName;
_Type = value;
}
}
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="SerializableTypeReference"/>.</summary>
public SerializableTypeReference(string qualifiedName)
{
_QualifiedName = qualifiedName;
_Type = null;
}
/// <summary>Creates a new <see cref="SerializableTypeReference"/>.</summary>
public SerializableTypeReference(Type type)
{
_QualifiedName = type?.AssemblyQualifiedName;
_Type = type;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public readonly void OnBeforeSerialize() { }
/// <inheritdoc/>
public void OnAfterDeserialize()
=> _Type = null;
/************************************************************************************************************************/
#region Drawer
/************************************************************************************************************************/
/// <summary>[Editor-Only] A <see cref="PropertyDrawer"/> for <see cref="SerializableTypeReference"/>.</summary>
[CustomPropertyDrawer(typeof(SerializableTypeReference))]
public class Drawer : PropertyDrawer
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
=> AnimancerGUI.LineHeight;
/************************************************************************************************************************/
/// <inheritdoc/>
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
{
var name = property.FindPropertyRelative(nameof(_QualifiedName));
var spacing = AnimancerGUI.StandardSpacing;
var pickerArea = AnimancerGUI.StealFromRight(ref area, area.height + spacing, spacing);
name.stringValue = EditorGUI.TextField(area, label, name.stringValue);
var indentLevel = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
var picked = EditorGUI.ObjectField(pickerArea, null, typeof(Object), true);
if (picked != null)
name.stringValue = picked.GetType().AssemblyQualifiedName;
EditorGUI.indentLevel = indentLevel;
}
/************************************************************************************************************************/
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,171 @@
// Serialization // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using System;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
// Shared File Last Modified: 2026-01-17.
namespace Animancer.Editor
// namespace InspectorGadgets.Editor
{
/// <summary>[Editor-Only] Various serialization utilities.</summary>
public partial class Serialization
{
/// <summary>[Editor-Only]
/// Directly serializing an <see cref="UnityEngine.Object"/> reference doesn't always work (such as with scene
/// objects when entering Play Mode), so this class also serializes their instance ID and uses that if the
/// direct reference fails.
/// </summary>
[Serializable]
public class ObjectReference
{
/************************************************************************************************************************/
[SerializeField] private Object _Object;
#if UNITY_6000_3_OR_NEWER
[SerializeField] private EntityId _EntityID;
#else
[SerializeField] private int _InstanceID;
#endif
/************************************************************************************************************************/
/// <summary>The referenced <see cref="SerializedObject"/>.</summary>
public Object Object
{
get
{
Initialize();
return _Object;
}
}
/// <summary>The <see cref="Object.GetInstanceID"/>.</summary>
#if UNITY_6000_3_OR_NEWER
public EntityId EntityID => _EntityID;
#else
public int InstanceID => _InstanceID;
#endif
/************************************************************************************************************************/
/// <summary>
/// Creates a new <see cref="ObjectReference"/> which wraps the specified
/// <see cref="UnityEngine.Object"/>.
/// </summary>
public ObjectReference(Object obj)
{
_Object = obj;
if (obj != null)
{
#if UNITY_6000_3_OR_NEWER
_EntityID = obj.GetEntityId();
#else
_InstanceID = obj.GetInstanceID();
#endif
}
}
/************************************************************************************************************************/
private void Initialize()
{
#if UNITY_6000_3_OR_NEWER
if (_Object == null)
_Object = EditorUtility.EntityIdToObject(_EntityID);
else
_EntityID = _Object.GetEntityId();
#else
if (_Object == null)
_Object = EditorUtility.InstanceIDToObject(_InstanceID);
else
_InstanceID = _Object.GetInstanceID();
#endif
}
/************************************************************************************************************************/
/// <summary>
/// Creates a new <see cref="ObjectReference"/> which wraps the specified
/// <see cref="UnityEngine.Object"/>.
/// </summary>
public static implicit operator ObjectReference(Object obj)
=> new(obj);
/// <summary>Returns the target <see cref="Object"/>.</summary>
public static implicit operator Object(ObjectReference reference)
=> reference.Object;
/************************************************************************************************************************/
/// <summary>Creates a new array of <see cref="ObjectReference"/>s representing the `objects`.</summary>
public static ObjectReference[] Convert(params Object[] objects)
{
var references = new ObjectReference[objects.Length];
for (int i = 0; i < objects.Length; i++)
references[i] = objects[i];
return references;
}
/// <summary>
/// Creates a new array of <see cref="UnityEngine.Object"/>s containing the target <see cref="Object"/> of each
/// of the `references`.
/// </summary>
public static Object[] Convert(params ObjectReference[] references)
{
var objects = new Object[references.Length];
for (int i = 0; i < references.Length; i++)
objects[i] = references[i];
return objects;
}
/************************************************************************************************************************/
/// <summary>Indicates whether both arrays refer to the same set of objects.</summary>
public static bool AreSameObjects(ObjectReference[] references, Object[] objects)
{
if (references == null)
return objects == null;
if (objects == null)
return false;
if (references.Length != objects.Length)
return false;
for (int i = 0; i < references.Length; i++)
{
if (references[i] != objects[i])
return false;
}
return true;
}
/************************************************************************************************************************/
/// <summary>Returns a string describing this object.</summary>
public override string ToString()
#if UNITY_6000_3_OR_NEWER
=> $"Serialization.ObjectReference [{_EntityID}] {_Object}";
#else
=> $"Serialization.ObjectReference [{_InstanceID}] {_Object}";
#endif
/************************************************************************************************************************/
}
/************************************************************************************************************************/
/// <summary>Returns true if the `reference` and <see cref="ObjectReference.Object"/> are not null.</summary>
public static bool IsValid(this ObjectReference reference)
=> reference?.Object != null;
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,287 @@
// Serialization // Copyright 2018-2025 Kybernetik //
#if UNITY_EDITOR
using System;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
// Shared File Last Modified: 2023-08-12.
namespace Animancer.Editor
// namespace InspectorGadgets.Editor
{
/// <summary>[Editor-Only] Various serialization utilities.</summary>
public partial class Serialization
{
/// <summary>[Editor-Only] A serializable reference to a <see cref="SerializedProperty"/>.</summary>
[Serializable]
public class PropertyReference
{
/************************************************************************************************************************/
[SerializeField] private ObjectReference[] _TargetObjects;
/// <summary>[<see cref="SerializeField"/>] The <see cref="SerializedObject.targetObject"/>.</summary>
public ObjectReference TargetObject
{
get
{
return _TargetObjects != null && _TargetObjects.Length > 0 ?
_TargetObjects[0] : null;
}
}
/// <summary>[<see cref="SerializeField"/>] The <see cref="SerializedObject.targetObjects"/>.</summary>
public ObjectReference[] TargetObjects => _TargetObjects;
/************************************************************************************************************************/
[SerializeField] private ObjectReference _Context;
/// <summary>[<see cref="SerializeField"/>] The <see cref="SerializedObject.context"/>.</summary>
public ObjectReference Context => _Context;
/************************************************************************************************************************/
[SerializeField] private string _PropertyPath;
/// <summary>[<see cref="SerializeField"/>] The <see cref="SerializedProperty.propertyPath"/>.</summary>
public string PropertyPath => _PropertyPath;
/************************************************************************************************************************/
[NonSerialized] private bool _IsInitialized;
/// <summary>Indicates whether the <see cref="Property"/> has been accessed.</summary>
public bool IsInitialized => _IsInitialized;
/************************************************************************************************************************/
[NonSerialized] private SerializedProperty _Property;
/// <summary>[<see cref="SerializeField"/>] The referenced <see cref="SerializedProperty"/>.</summary>
public SerializedProperty Property
{
get
{
Initialize();
return _Property;
}
}
/************************************************************************************************************************/
/// <summary>
/// Creates a new <see cref="PropertyReference"/> which wraps the specified `property`.
/// </summary>
public PropertyReference(SerializedProperty property)
{
_TargetObjects = ObjectReference.Convert(property.serializedObject.targetObjects);
_Context = property.serializedObject.context;
_PropertyPath = property.propertyPath;
// Don't set the _Property. If it gets accessed we want to create out own instance.
}
/************************************************************************************************************************/
/// <summary>
/// Creates a new <see cref="PropertyReference"/> which wraps the specified `property`.
/// </summary>
public static implicit operator PropertyReference(SerializedProperty property)
=> new(property);
/// <summary>
/// Returns the target <see cref="Property"/>.
/// </summary>
public static implicit operator SerializedProperty(PropertyReference reference)
=> reference.Property;
/************************************************************************************************************************/
private void Initialize()
{
if (_IsInitialized)
{
if (!TargetsExist)
Dispose();
return;
}
_IsInitialized = true;
if (string.IsNullOrEmpty(_PropertyPath) ||
!TargetsExist)
return;
var targetObjects = ObjectReference.Convert(_TargetObjects);
var serializedObject = new SerializedObject(targetObjects, _Context);
_Property = serializedObject.FindProperty(_PropertyPath);
}
/************************************************************************************************************************/
/// <summary>Do the specified `property` and `targetObjects` match the targets of this reference?</summary>
public bool IsTarget(SerializedProperty property, Object[] targetObjects)
{
if (_Property == null ||
_Property.propertyPath != property.propertyPath ||
_TargetObjects == null ||
_TargetObjects.Length != targetObjects.Length)
return false;
for (int i = 0; i < _TargetObjects.Length; i++)
{
if (_TargetObjects[i] != targetObjects[i])
return false;
}
return true;
}
/************************************************************************************************************************/
/// <summary>Is there is at least one target and none of them are <c>null</c>?</summary>
private bool TargetsExist
{
get
{
if (_TargetObjects == null ||
_TargetObjects.Length == 0)
return false;
for (int i = 0; i < _TargetObjects.Length; i++)
{
if (_TargetObjects[i].Object == null)
return false;
}
return true;
}
}
/************************************************************************************************************************/
/// <summary>
/// Calls <see cref="SerializedObject.Update"/> if the <see cref="Property"/> has been initialized.
/// </summary>
public void Update()
{
if (_Property == null)
return;
if (!TargetsExist)
{
Dispose();
return;
}
_Property.serializedObject.Update();
}
/// <summary>
/// Calls <see cref="SerializedObject.ApplyModifiedProperties"/> if the <see cref="Property"/> has been initialized.
/// </summary>
public void ApplyModifiedProperties()
{
if (_Property == null)
return;
if (!TargetsExist)
{
Dispose();
return;
}
_Property.serializedObject.ApplyModifiedProperties();
}
/// <summary>
/// Calls <see cref="SerializedObject.Dispose"/> if the <see cref="Property"/> has been initialized.
/// </summary>
public void Dispose()
{
if (_Property != null)
{
_Property.serializedObject.Dispose();
_Property = null;
}
}
/************************************************************************************************************************/
/// <summary>Gets the height needed to draw the target property.</summary>
public float GetPropertyHeight()
{
if (_Property == null)
return 0;
return EditorGUI.GetPropertyHeight(_Property, _Property.isExpanded);
}
/************************************************************************************************************************/
/// <summary>Draws the target object within the specified `area`.</summary>
public void DoTargetGUI(Rect area)
{
area.height = EditorGUIUtility.singleLineHeight;
Initialize();
if (_Property == null)
{
GUI.Label(area, "Missing " + this);
return;
}
var targets = _Property.serializedObject.targetObjects;
using (new EditorGUI.DisabledScope(true))
{
var showMixedValue = EditorGUI.showMixedValue;
EditorGUI.showMixedValue = targets.Length > 1;
var target = targets.Length > 0 ? targets[0] : null;
EditorGUI.ObjectField(area, target, typeof(Object), true);
EditorGUI.showMixedValue = showMixedValue;
}
}
/************************************************************************************************************************/
/// <summary>Draws the target property within the specified `area`.</summary>
public void DoPropertyGUI(Rect area)
{
Initialize();
if (_Property == null)
return;
_Property.serializedObject.Update();
GUI.BeginGroup(area);
area.x = area.y = 0;
EditorGUI.PropertyField(area, _Property, _Property.isExpanded);
GUI.EndGroup();
_Property.serializedObject.ApplyModifiedProperties();
}
/************************************************************************************************************************/
}
/************************************************************************************************************************/
/// <summary>Returns true if the `reference` and <see cref="PropertyReference.Property"/> are not null.</summary>
public static bool IsValid(this PropertyReference reference) => reference?.Property != null;
/************************************************************************************************************************/
}
}
#endif

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,96 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using UnityEditor;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] A wrapper around a <see cref="SerializedProperty"/> representing an array field.</summary>
public class SerializedArrayProperty
{
/************************************************************************************************************************/
private SerializedProperty _Property;
/// <summary>The target property.</summary>
public SerializedProperty Property
{
get => _Property;
set
{
_Property = value;
Refresh();
}
}
/************************************************************************************************************************/
private string _Path;
/// <summary>The cached <see cref="SerializedProperty.propertyPath"/> of the <see cref="Property"/>.</summary>
public string Path => _Path ?? (_Path = Property.propertyPath);
/************************************************************************************************************************/
private int _Count;
/// <summary>The cached <see cref="SerializedProperty.arraySize"/> of the <see cref="Property"/>.</summary>
public int Count
{
get => _Count;
set => Property.arraySize = _Count = value;
}
/************************************************************************************************************************/
private bool _HasMultipleDifferentValues;
private bool _GotHasMultipleDifferentValues;
/// <summary>The cached <see cref="SerializedProperty.hasMultipleDifferentValues"/> of the <see cref="Property"/>.</summary>
public bool HasMultipleDifferentValues
{
get
{
if (!_GotHasMultipleDifferentValues)
{
_GotHasMultipleDifferentValues = true;
_HasMultipleDifferentValues = Property.hasMultipleDifferentValues;
}
return _HasMultipleDifferentValues;
}
}
/************************************************************************************************************************/
/// <summary>Updates the cached <see cref="Count"/> and <see cref="HasMultipleDifferentValues"/>.</summary>
public void Refresh()
{
_Path = null;
_Count = _Property != null ? _Property.arraySize : 0;
_GotHasMultipleDifferentValues = false;
}
/************************************************************************************************************************/
/// <summary>Calls <see cref="SerializedProperty.GetArrayElementAtIndex"/> on the <see cref="Property"/>.</summary>
/// <remarks>
/// Returns <c>null</c> if the element is not actually a child of the <see cref="Property"/>, which can happen
/// if multiple objects are selected with different array sizes.
/// </remarks>
public SerializedProperty GetElement(int index)
{
var element = Property.GetArrayElementAtIndex(index);
if (!HasMultipleDifferentValues || element.propertyPath.StartsWith(Path))
return element;
else
return null;
}
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,81 @@
// 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 <see cref="SerializedDataEditorWindow{TObject, TData}"/> for <see cref="Component"/>s.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/SerializedComponentDataEditorWindow_2
public abstract class SerializedComponentDataEditorWindow<TObject, TData> :
SerializedDataEditorWindow<TObject, TData>
where TObject : Component
where TData : class, ICopyable<TData>, IEquatable<TData>, new()
{
/************************************************************************************************************************/
[SerializeField] private GameObject _SourceGameObject;
[SerializeField] private int _SourceComponentInstanceID;
/************************************************************************************************************************/
/// <inheritdoc/>
public override TObject SourceObject
{
get
{
// For whatever reason, component references in an EditorWindow can't survive entering Play Mode but
// a GameObject or Transform reference can so we use that to recover the component.
// Storing the Instance ID also works, but seems to also survive restarting the Unity Editor which is
// bad because the scene references inside the data don't survive that which would leave us
// with an open window full of empty references. Working around that isn't worth the effort.
// So if the GameObject still exists, we use the Component's Instance ID to find it.
var source = base.SourceObject;
if (source == null && _SourceGameObject != null)
{
#if UNITY_6000_3_OR_NEWER
var component = EditorUtility.EntityIdToObject(_SourceComponentInstanceID);
#else
var component = EditorUtility.InstanceIDToObject(_SourceComponentInstanceID);
#endif
source = base.SourceObject = component as TObject;
}
return source;
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override bool SourceObjectMightBePrefab
=> true;
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void CaptureData()
{
base.CaptureData();
if (SourceObject != null)
{
_SourceGameObject = SourceObject.gameObject;
_SourceComponentInstanceID = SourceObject.GetInstanceID();
}
}
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,445 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Animancer.Editor
{
/// <summary>[Editor-Only]
/// A window for managing a copy of some serialized data and applying or reverting it.
/// </summary>
/// <remarks>
/// This system assumes the implementation of <see cref="IEquatable{T}"/>
/// compares the values of all fields in <typeparamref name="TData"/>.
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/SerializedDataEditorWindow_2
public abstract class SerializedDataEditorWindow<TObject, TData> : EditorWindow
where TObject : Object
where TData : class, ICopyable<TData>, IEquatable<TData>, new()
{
/************************************************************************************************************************/
[SerializeField]
private TObject _SourceObject;
/// <summary>The object which contains the data this class manages.</summary>
/// <remarks><see cref="SetAndCaptureSource"/> should generally be used instead of setting this property directly.</remarks>
public virtual TObject SourceObject
{
get => _SourceObject;
protected set => _SourceObject = value;
}
/************************************************************************************************************************/
/// <summary>The <see cref="Data"/> field of the <see cref="SourceObject"/>.</summary>
public abstract TData SourceData { get; set; }
/************************************************************************************************************************/
[SerializeField]
private TData _Data;
/// <summary>A copy of the <see cref="SourceData"/> being managed by this window.</summary>
public ref TData Data
=> ref _Data;
/************************************************************************************************************************/
/// <summary>Is the <see cref="Data"/> managed by this window different to the <see cref="SourceData"/>?</summary>
public virtual bool HasDataChanged
{
get
{
try
{
if (_Data == null)
return false;
var sourceData = SourceData;
return sourceData != null && !_Data.Equals(sourceData);
}
catch (Exception exception)
{
Debug.LogException(exception);
return false;
}
}
}
/************************************************************************************************************************/
/// <summary>Initializes this window.</summary>
protected virtual void OnEnable()
{
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
EditorApplication.wantsToQuit += OnTryCloseEditor;
Undo.undoRedoPerformed += Repaint;
}
/// <summary>Cleans up this window.</summary>
protected virtual void OnDisable()
{
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
EditorApplication.wantsToQuit -= OnTryCloseEditor;
Undo.undoRedoPerformed -= Repaint;
}
/************************************************************************************************************************/
/// <summary>
/// Prompts the user to <see cref="Apply"/> or <see cref="Revert"/>
/// if there are changes in the <see cref="Data"/> when this window is closed.
/// </summary>
protected virtual void OnDestroy()
{
var sourceObject = SourceObject;
if (sourceObject == null ||
!HasDataChanged ||
titleContent == null)
return;
if (EditorUtility.DisplayDialog(
titleContent.text,
$"Apply unsaved changes to '{sourceObject.name}'?",
"Apply",
"Revert"))
{
Apply();
}
}
/************************************************************************************************************************/
/// <summary>Called before closing the Unity Editor to confirm that un-saved data is applied.</summary>
private bool OnTryCloseEditor()
{
var sourceObject = SourceObject;
if (sourceObject == null ||
!HasDataChanged ||
titleContent == null)
return true;
var option = EditorUtility.DisplayDialogComplex(
titleContent.text,
$"Apply unsaved changes to '{sourceObject.name}'?",
"Apply",
"Cancel",
"Revert");
switch (option)
{
case 0:// Apply.
Apply();
return true;
case 2:// Revert.
Revert();
return true;
case 1:// Cancel.
default:
return false;
}
}
/************************************************************************************************************************/
/// <summary>
/// Sets the <see cref="SourceObject"/> and captures the <see cref="Data"/>
/// as a copy of its <see cref="SourceData"/>.
/// </summary>
protected void SetAndCaptureSource(TObject sourceObject)
{
_SourceObject = sourceObject;
CaptureData();
Repaint();
}
/************************************************************************************************************************/
/// <summary>
/// Override this to return <c>true</c> if the <see cref="SourceObject"/> could be part of a prefab
/// to ensure that modifications are serialized properly.
/// </summary>
public virtual bool SourceObjectMightBePrefab
=> false;
/************************************************************************************************************************/
/// <summary>Saves the edited <see cref="Data"/> into the <see cref="SourceObject"/>.</summary>
public virtual void Apply()
{
var sourceObject = SourceObject;
if (sourceObject == null)
return;
using (new ModifySerializedField(sourceObject, name, SourceObjectMightBePrefab))
{
SourceData = _Data.CopyableClone();
if (EditorUtility.IsPersistent(SourceObject))
{
var objects = SetPool.Acquire<Object>();
GatherObjectReferences(sourceObject, objects);
foreach (var obj in objects)
if (!EditorUtility.IsPersistent(obj))
AssetDatabase.AddObjectToAsset(obj, SourceObject);
SetPool.Release(objects);
}
}
Repaint();
AssetDatabase.SaveAssets();
}
/************************************************************************************************************************/
/// <summary>Gathers all objects referenced by the `root`.</summary>
public static void GatherObjectReferences(Object root, HashSet<Object> objects)
{
using var serializedObject = new SerializedObject(root);
var property = serializedObject.GetIterator();
while (property.Next(true))
{
if (property.propertyType == SerializedPropertyType.ObjectReference)
{
var value = property.objectReferenceValue;
if (value != null)
objects.Add(value);
}
}
}
/************************************************************************************************************************/
/// <summary>Restores the <see cref="Data"/> to the original values from the <see cref="SourceData"/>.</summary>
public virtual void Revert()
{
RecordUndo();
CaptureData();
}
/************************************************************************************************************************/
/// <summary>Stores a copy of the <see cref="SourceData"/> in the <see cref="Data"/>.</summary>
protected virtual void CaptureData()
{
_Data = SourceData?.CopyableClone() ?? new();
AnimancerReflection.TryInvoke(_Data, "OnValidate");
}
/************************************************************************************************************************/
/// <summary>Records the current state of this window so it can be undone later.</summary>
public TData RecordUndo()
=> RecordUndo(titleContent.text);
/// <summary>Records the current state of this window so it can be undone later.</summary>
public virtual TData RecordUndo(string name)
{
Undo.RecordObject(this, name);
Repaint();
return _Data;
}
/************************************************************************************************************************/
/// <summary>
/// Opens a new <typeparamref name="TWindow"/> for the `sourceObject`
/// or gives focus to an existing window that was already displaying it.
/// </summary>
public static TWindow Open<TWindow>(
TObject sourceObject,
bool onlyOneWindow = false,
params Type[] desiredDockNextTo)
where TWindow : SerializedDataEditorWindow<TObject, TData>
{
if (!onlyOneWindow)
{
foreach (var window in Resources.FindObjectsOfTypeAll<TWindow>())
{
if (window.SourceObject == sourceObject)
{
window.Show();
window.SetAndCaptureSource(sourceObject);
window.Focus();
return window;
}
}
}
var newWindow = onlyOneWindow
? GetWindow<TWindow>(desiredDockNextTo ?? Type.EmptyTypes)
: CreateInstance<TWindow>();
newWindow.Show();
newWindow.SetAndCaptureSource(sourceObject);
return newWindow;
}
/************************************************************************************************************************/
#region Auto Apply
/************************************************************************************************************************/
/// <summary>The <see cref="EditorPrefs"/> key for <see cref="AutoApply"/>.</summary>
protected virtual string AutoApplyPref
=> $"{titleContent.text}.{nameof(AutoApply)}";
/************************************************************************************************************************/
private bool _HasLoadedAutoApply;
private bool _AutoApply;
private bool _EnabledAutoApplyInPlayMode;
/// <summary>Should changes be automatically applied as soon as they're made?</summary>
public bool AutoApply
{
get
{
if (!_HasLoadedAutoApply)
{
_HasLoadedAutoApply = true;
_AutoApply = EditorPrefs.GetBool(AutoApplyPref);
}
return _AutoApply;
}
set
{
_HasLoadedAutoApply = true;
_AutoApply = value;
_EnabledAutoApplyInPlayMode = _AutoApply && EditorApplication.isPlayingOrWillChangePlaymode;
EditorPrefs.SetBool(AutoApplyPref, value);
}
}
/************************************************************************************************************************/
/// <summary>Handles entering and exiting Play Mode.</summary>
protected virtual void OnPlayModeStateChanged(PlayModeStateChange change)
{
switch (change)
{
case PlayModeStateChange.EnteredPlayMode:
if (HasDataChanged && focusedWindow != null)
focusedWindow.ShowNotification(new($"{titleContent.text} window has un-applied changes"));
break;
case PlayModeStateChange.ExitingPlayMode:
if (_EnabledAutoApplyInPlayMode)
AutoApply = false;
break;
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region GUI
/************************************************************************************************************************/
private static readonly GUIContent
RevertLabel = new(
"Revert",
"Undo all changes made in this window"),
ApplyLabel = new(
"Apply",
"Apply all changes made in this window to the source object"),
AutoApplyLabel = new(
"Auto",
"Immediately apply all changes made in this window to the source object?" +
"\n\nIf enabled in Play Mode, this toggle will be disabled when returning to Edit Mode.");
/************************************************************************************************************************/
/// <summary>
/// Calculates the pixel width required for
/// <see cref="DoApplyRevertGUI(Rect, Rect, Rect, ButtonGroupStyles)"/>.
/// </summary>
public float CalculateApplyRevertWidth(ButtonGroupStyles styles = default)
{
styles.CopyMissingStyles(ButtonGroupStyles.Button);
return
styles.left.CalculateWidth(RevertLabel) + 1 +
styles.middle.CalculateWidth(ApplyLabel) + 1 +
styles.right.CalculateWidth(AutoApplyLabel) + 1;
}
/************************************************************************************************************************/
/// <summary>Draws GUI controls for <see cref="Revert"/>, <see cref="Apply"/>, and <see cref="AutoApply"/>.</summary>
public void DoApplyRevertGUI(ButtonGroupStyles styles = default)
{
styles.CopyMissingStyles(ButtonGroupStyles.Button);
GUILayout.BeginHorizontal();
var leftArea = GUILayoutUtility.GetRect(RevertLabel, styles.left);
var middleArea = GUILayoutUtility.GetRect(ApplyLabel, styles.middle);
var rightArea = GUILayoutUtility.GetRect(AutoApplyLabel, styles.right);
DoApplyRevertGUI(leftArea, middleArea, rightArea, styles);
GUILayout.EndHorizontal();
}
/************************************************************************************************************************/
/// <summary>Draws GUI controls for <see cref="Revert"/>, <see cref="Apply"/>, and <see cref="AutoApply"/>.</summary>
public void DoApplyRevertGUI(Rect area, ButtonGroupStyles styles = default)
{
styles.CopyMissingStyles(ButtonGroupStyles.Button);
var leftArea = AnimancerGUI.StealFromLeft(ref area, styles.left.CalculateWidth(RevertLabel) + 1);
var middleArea = AnimancerGUI.StealFromLeft(ref area, styles.middle.CalculateWidth(ApplyLabel) + 1);
DoApplyRevertGUI(leftArea, middleArea, area, styles);
}
/************************************************************************************************************************/
/// <summary>Draws GUI controls for <see cref="Revert"/>, <see cref="Apply"/>, and <see cref="AutoApply"/>.</summary>
public void DoApplyRevertGUI(
Rect leftArea,
Rect middleArea,
Rect rightArea,
ButtonGroupStyles styles = default)
{
styles.CopyMissingStyles(ButtonGroupStyles.Button);
var enabled = GUI.enabled;
GUI.enabled = SourceObject != null && HasDataChanged;
// Revert.
if (GUI.Button(leftArea, RevertLabel, styles.left))
Revert();
// Apply.
if (GUI.Button(middleArea, ApplyLabel, styles.middle))
Apply();
// Auto Apply.
var autoApply = AutoApply;
if (autoApply && GUI.enabled)
Apply();
GUI.enabled = enabled;
if (autoApply != GUI.Toggle(rightArea, autoApply, AutoApplyLabel, styles.right))
AutoApply = !autoApply;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,188 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor
{
/// <summary>[Editor-Only]
/// A system which gathers information about <see cref="SerializeReference"/> fields to detect when multiple fields
/// are referencing the same object.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/SharedReferenceCache
public class SharedReferenceCache :
IEnumerable<KeyValuePair<object, List<SharedReferenceCache.Field>>>
{
/************************************************************************************************************************/
#region Static Caching
/************************************************************************************************************************/
private static readonly Dictionary<SerializedObject, SharedReferenceCache>
SerializedObjectToCache = new();
/// <summary>Returns a cached <see cref="SharedReferenceCache"/> for the `serializedObject`.</summary>
public static SharedReferenceCache Get(SerializedObject serializedObject)
{
CheckFlush(serializedObject);
if (!SerializedObjectToCache.TryGetValue(serializedObject, out var cache))
SerializedObjectToCache.Add(serializedObject, cache = new(serializedObject));
return cache;
}
/************************************************************************************************************************/
private static readonly HashSet<SerializedObject>
NotRecentlyUsed = new();
private const double
FlushInterval = 5;
private static double
_LastFlushTime;
/// <summary>Discards any caches not used during the last <see cref="FlushInterval"/> when it elapses.</summary>
private static void CheckFlush(SerializedObject serializedObject)
{
var currentTime = EditorApplication.timeSinceStartup;
if (currentTime >= _LastFlushTime + FlushInterval)
{
_LastFlushTime = currentTime;
foreach (var unused in NotRecentlyUsed)
SerializedObjectToCache.Remove(unused);
NotRecentlyUsed.Clear();
NotRecentlyUsed.UnionWith(SerializedObjectToCache.Keys);
}
NotRecentlyUsed.Remove(serializedObject);
}
/************************************************************************************************************************/
/// <summary>The number of editor updates that have occurred since startup.</summary>
public static ulong FrameCount { get; private set; }
static SharedReferenceCache()
{
EditorApplication.update += () => FrameCount++;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
/// <summary>Information about a field.</summary>
public struct Field
{
/************************************************************************************************************************/
/// <summary>The <see cref="Serialization.GetFriendlyPath"/> of the field.</summary>
public string path;
/// <summary>The area where the field was last drawn.</summary>
public Rect area;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="Field"/>.</summary>
public Field(string path)
{
this.path = path;
area = default;
}
/************************************************************************************************************************/
}
/************************************************************************************************************************/
private readonly SerializedObject
SerializedObject;
private readonly Dictionary<object, List<Field>>
ObjectToReferences = new();
private ulong
_LastGatherFrameCount;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="SharedReferenceCache"/>.</summary>
public SharedReferenceCache(SerializedObject serializedObject)
{
SerializedObject = serializedObject;
}
/************************************************************************************************************************/
/// <summary>Should <see cref="GatherReferences"/> be called?</summary>
public bool ShouldGather
=> _LastGatherFrameCount != FrameCount;
/// <summary>Updates the cached reference info.</summary>
public void GatherReferences()
{
_LastGatherFrameCount = FrameCount;
ObjectToReferences.Clear();
var property = SerializedObject.GetIterator();
while (property.Next(true))
{
if (property.propertyType != SerializedPropertyType.ManagedReference)
continue;
var reference = property.managedReferenceValue;
if (reference == null)
continue;
if (!ObjectToReferences.TryGetValue(reference, out var paths))
ObjectToReferences.Add(reference, paths = new());
paths.Add(new(property.GetFriendlyPath()));
}
}
/************************************************************************************************************************/
/// <summary>Tries to get the info about all fields containing the `reference`.</summary>
public bool TryGetInfo(object reference, out List<Field> references)
{
if (ShouldGather)
GatherReferences();
return ObjectToReferences.TryGetValue(reference, out references);
}
/************************************************************************************************************************/
/// <summary>Returns an enumerator for all references and their info.</summary>
public Dictionary<object, List<Field>>.Enumerator GetEnumerator()
{
if (ShouldGather)
GatherReferences();
return ObjectToReferences.GetEnumerator();
}
/// <summary>Returns an enumerator for all references and their info.</summary>
IEnumerator<KeyValuePair<object, List<Field>>> IEnumerable<KeyValuePair<object, List<Field>>>.GetEnumerator()
=> GetEnumerator();
/// <summary>Returns an enumerator for all references and their info.</summary>
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,165 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor
{
/// <summary>[Editor-Only]
/// Stores data which needs to survive assembly reloading (such as from script compilation), but can be discarded
/// when the Unity Editor is closed.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/TemporarySettings
internal class TemporarySettings : ScriptableObject
{
/************************************************************************************************************************/
#region Instance
/************************************************************************************************************************/
private static TemporarySettings _Instance;
/// <summary>Finds an existing instance of this class or creates a new one.</summary>
private static TemporarySettings Instance
=> AnimancerEditorUtilities.FindOrCreate(
ref _Instance,
HideFlags.HideAndDontSave | HideFlags.DontUnloadUnusedAsset);
/************************************************************************************************************************/
protected virtual void OnEnable()
{
OnEnableSelection();
}
protected virtual void OnDisable()
{
OnDisableSelection();
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Event Selection
/************************************************************************************************************************/
private readonly Dictionary<Object, Dictionary<string, int>>
ObjectToPropertyPathToSelectedEvent = new();
/************************************************************************************************************************/
public static int GetSelectedEvent(SerializedProperty property)
{
var instance = Instance;
if (!instance.ObjectToPropertyPathToSelectedEvent.TryGetValue(property.serializedObject.targetObject, out var pathToSelection))
return -1;
else if (pathToSelection.TryGetValue(property.propertyPath, out var selection))
return selection;
else
return -1;
}
/************************************************************************************************************************/
public static void SetSelectedEvent(SerializedProperty property, int eventIndex)
{
var pathToSelection = GetOrCreatePathToSelection(property.serializedObject.targetObject);
if (eventIndex >= 0)
pathToSelection[property.propertyPath] = eventIndex;
else
pathToSelection.Remove(property.propertyPath);
}
/************************************************************************************************************************/
private static Dictionary<string, int> GetOrCreatePathToSelection(Object obj)
{
var instance = Instance;
if (!instance.ObjectToPropertyPathToSelectedEvent.TryGetValue(obj, out var pathToSelection))
instance.ObjectToPropertyPathToSelectedEvent.Add(obj, pathToSelection = new());
return pathToSelection;
}
/************************************************************************************************************************/
[SerializeField] private Serialization.ObjectReference[] _EventSelectionObjects;
[SerializeField] private string[] _EventSelectionPropertyPaths;
[SerializeField] private int[] _EventSelectionIndices;
/************************************************************************************************************************/
private void OnDisableSelection()
{
var objects = new List<Serialization.ObjectReference>();
var paths = new List<string>();
var indices = new List<int>();
foreach (var objectToSelection in ObjectToPropertyPathToSelectedEvent)
{
foreach (var pathToSelection in objectToSelection.Value)
{
objects.Add(objectToSelection.Key);
paths.Add(pathToSelection.Key);
indices.Add(pathToSelection.Value);
}
}
_EventSelectionObjects = objects.ToArray();
_EventSelectionPropertyPaths = paths.ToArray();
_EventSelectionIndices = indices.ToArray();
}
/************************************************************************************************************************/
private void OnEnableSelection()
{
if (_EventSelectionObjects == null ||
_EventSelectionPropertyPaths == null ||
_EventSelectionIndices == null)
return;
var count = _EventSelectionObjects.Length;
if (count > _EventSelectionPropertyPaths.Length)
count = _EventSelectionPropertyPaths.Length;
if (count > _EventSelectionIndices.Length)
count = _EventSelectionIndices.Length;
for (int i = 0; i < count; i++)
{
var obj = _EventSelectionObjects[i];
if (obj.IsValid())
{
var pathToSelection = GetOrCreatePathToSelection(obj);
pathToSelection.Add(_EventSelectionPropertyPaths[i], _EventSelectionIndices[i]);
}
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Preview Models
/************************************************************************************************************************/
[SerializeField]
private List<GameObject> _PreviewModels;
public static List<GameObject> PreviewModels
{
get
{
var instance = Instance;
AnimancerEditorUtilities.RemoveMissingAndDuplicates(ref instance._PreviewModels);
return instance._PreviewModels;
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif

View File

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