// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2023 Kybernetik // #pragma warning disable CS0649 // Field is never assigned to, and will always have its default value. //#define ANIMANCER_ULT_EVENTS // If you edit this file to change the callback type to something other than UltEvents, you will need to change this // alias as well as the HasPersistentCalls method below. #if ANIMANCER_ULT_EVENTS using SerializableCallback = UltEvents.UltEvent; #else using SerializableCallback = UnityEngine.Events.UnityEvent; #endif using UnityEngine; using System; namespace Animancer { /// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent partial struct AnimancerEvent { /// https://kybernetik.com.au/animancer/api/Animancer/Sequence partial class Sequence { /// /// An that can be serialized and uses s to define /// the s. /// /// /// If you have Animancer Pro you can replace s with /// UltEvents using the following procedure: /// /// Select the Assets/Plugins/Animancer/Animancer.asmdef and add a Reference to the /// UltEvents Assembly Definition. /// Go into the Player Settings of your project and add ANIMANCER_ULT_EVENTS as a Scripting /// Define Symbol. Or you can simply edit this script to change the event type (it is located at /// Assets/Plugins/Animancer/Internal/Core/AnimancerEvent.Sequence.Serializable.cs by default. /// /// Documentation: Animancer Events /// /// https://kybernetik.com.au/animancer/api/Animancer/Serializable /// [Serializable] public class Serializable : ICopyable #if UNITY_EDITOR , ISerializationCallbackReceiver #endif { /************************************************************************************************************************/ [SerializeField] private float[] _NormalizedTimes; /// [] The serialized s. public ref float[] NormalizedTimes => ref _NormalizedTimes; /************************************************************************************************************************/ [SerializeField] private SerializableCallback[] _Callbacks; /// [] The serialized s. /// /// This array only needs to be large enough to hold the last event that actually contains any calls. /// Any empty or missing elements will simply use the at runtime. /// public ref SerializableCallback[] Callbacks => ref _Callbacks; /************************************************************************************************************************/ [SerializeField] private string[] _Names; /// [] The serialized . public ref string[] Names => ref _Names; /************************************************************************************************************************/ #if UNITY_EDITOR /************************************************************************************************************************/ /// [Editor-Only, Internal] The name of the array field which stores the s. internal const string NormalizedTimesField = nameof(_NormalizedTimes); /// [Editor-Only, Internal] The name of the array field which stores the serialized s. internal const string CallbacksField = nameof(_Callbacks); /// [Editor-Only, Internal] The name of the array field which stores the serialized . internal const string NamesField = nameof(_Names); /************************************************************************************************************************/ #endif /************************************************************************************************************************/ private Sequence _Events; /// /// The runtime compiled from this . /// Each call after the first will return the same reference. /// /// /// Unlike , this property will create an empty /// instead of returning null if there are no events. /// public Sequence Events { get { if (_Events == null) { GetEventsOptional(); if (_Events == null) _Events = new Sequence(); } return _Events; } set => _Events = value; } /************************************************************************************************************************/ /// /// Returns the runtime compiled from this . /// Each call after the first will return the same reference. /// /// /// This method returns null if the sequence would be empty anyway and is used by the implicit /// conversion from to . /// public Sequence GetEventsOptional() { if (_Events != null || _NormalizedTimes == null) return _Events; var timeCount = _NormalizedTimes.Length; if (timeCount == 0) return null; var callbackCount = _Callbacks != null ? _Callbacks.Length : 0; var callback = callbackCount >= timeCount-- ? GetInvoker(_Callbacks[timeCount]) : null; var endEvent = new AnimancerEvent(_NormalizedTimes[timeCount], callback); _Events = new Sequence(timeCount) { EndEvent = endEvent, Count = timeCount, _Names = _Names, }; for (int i = 0; i < timeCount; i++) { callback = i < callbackCount ? GetInvoker(_Callbacks[i]) : DummyCallback; _Events._Events[i] = new AnimancerEvent(_NormalizedTimes[i], callback); } return _Events; } /// Calls . public static implicit operator Sequence(Serializable serializable) => serializable?.GetEventsOptional(); /************************************************************************************************************************/ /// Returns the or null if it wasn't yet initialized. internal Sequence InitializedEvents => _Events; /************************************************************************************************************************/ /// /// If the `callback` has any persistent calls, this method returns a delegate to call its /// method. Otherwise it returns the /// . /// public static Action GetInvoker(SerializableCallback callback) => HasPersistentCalls(callback) ? callback.Invoke : DummyCallback; #if UNITY_EDITOR /// [Editor-Only] /// Casts the `callback` and calls . /// public static Action GetInvoker(object callback) => GetInvoker((SerializableCallback)callback); #endif /************************************************************************************************************************/ /// /// Determines if the `callback` contains any method calls that will be serialized (otherwise the /// can be used instead of creating a new delegate to invoke the empty /// `callback`). /// public static bool HasPersistentCalls(SerializableCallback callback) { if (callback == null) return false; // UnityEvents do not allow us to check if any dynamic calls are present. // But we are not giving runtime access to the events so it does not really matter. // UltEvents does allow it (via the HasCalls property), but we might as well be consistent. #if ANIMANCER_ULT_EVENTS var calls = callback.PersistentCallsList; return calls != null && calls.Count > 0; #else return callback.GetPersistentEventCount() > 0; #endif } #if UNITY_EDITOR /// [Editor-Only] /// Casts the `callback` and calls . /// public static bool HasPersistentCalls(object callback) => HasPersistentCalls((SerializableCallback)callback); #endif /************************************************************************************************************************/ /// Returns the of the . /// If the value is not set, the value is determined by . public float GetNormalizedEndTime(float speed = 1) { if (_NormalizedTimes.IsNullOrEmpty()) return GetDefaultNormalizedEndTime(speed); else return _NormalizedTimes[_NormalizedTimes.Length - 1]; } /************************************************************************************************************************/ /// Sets the of the . public void SetNormalizedEndTime(float normalizedTime) { if (_NormalizedTimes.IsNullOrEmpty()) _NormalizedTimes = new float[] { normalizedTime }; else _NormalizedTimes[_NormalizedTimes.Length - 1] = normalizedTime; } /************************************************************************************************************************/ /// public void CopyFrom(Serializable copyFrom) { if (copyFrom == null) { _NormalizedTimes = default; _Callbacks = default; _Names = default; return; } AnimancerUtilities.CopyExactArray(copyFrom._NormalizedTimes, ref _NormalizedTimes); AnimancerUtilities.CopyExactArray(copyFrom._Callbacks, ref _Callbacks); AnimancerUtilities.CopyExactArray(copyFrom._Names, ref _Names); } /************************************************************************************************************************/ #if UNITY_EDITOR /************************************************************************************************************************/ /// [Editor-Only] Does nothing. /// /// Keeping the runtime in sync with the serialized data is handled by /// . /// void ISerializationCallbackReceiver.OnAfterDeserialize() { } /************************************************************************************************************************/ /// [Editor-Only] Ensures that the events are sorted by time (excluding the end event). void ISerializationCallbackReceiver.OnBeforeSerialize() { if (_NormalizedTimes == null || _NormalizedTimes.Length <= 2) { CompactArrays(); return; } var eventContext = Editor.SerializableEventSequenceDrawer.Context.Current; var selectedEvent = eventContext?.Property != null ? eventContext.SelectedEvent : -1; var timeCount = _NormalizedTimes.Length - 1; var previousTime = _NormalizedTimes[0]; // Bubble Sort based on the normalized times. for (int i = 1; i < timeCount; i++) { var time = _NormalizedTimes[i]; if (time >= previousTime) { previousTime = time; continue; } _NormalizedTimes.Swap(i, i - 1); DynamicSwap(ref _Callbacks, i); DynamicSwap(ref _Names, i); if (selectedEvent == i) selectedEvent = i - 1; else if (selectedEvent == i - 1) selectedEvent = i; if (i == 1) { i = 0; previousTime = float.NegativeInfinity; } else { i -= 2; previousTime = _NormalizedTimes[i]; } } // If the current animation is looping, clamp all times within the 0-1 range. var transitionContext = Editor.TransitionDrawer.Context; if (transitionContext != null && transitionContext.Transition != null && transitionContext.Transition.IsLooping) { for (int i = _NormalizedTimes.Length - 1; i >= 0; i--) { var time = _NormalizedTimes[i]; if (time < 0) _NormalizedTimes[i] = 0; else if (time > AlmostOne) _NormalizedTimes[i] = AlmostOne; } } // If the selected event was moved adjust the selection. if (eventContext?.Property != null && eventContext.SelectedEvent != selectedEvent) { eventContext.SelectedEvent = selectedEvent; Editor.TransitionPreviewWindow.PreviewNormalizedTime = _NormalizedTimes[selectedEvent]; } CompactArrays(); } /************************************************************************************************************************/ /// [Editor-Only] /// Swaps array[index] with array[index - 1] while accounting for the possibility of the /// `index` being beyond the bounds of the `array`. /// private static void DynamicSwap(ref T[] array, int index) { var count = array != null ? array.Length : 0; if (index == count) Array.Resize(ref array, ++count); if (index < count) array.Swap(index, index - 1); } /************************************************************************************************************************/ /// [Internal] /// Should the arrays be prevented from reducing their size when their last elements are unused? /// internal static bool DisableCompactArrays { get; set; } /// [Editor-Only] /// Removes empty data from the ends of the arrays to reduce the serialized data size. /// private void CompactArrays() { if (DisableCompactArrays) return; // If there is only one time and it is NaN, we don't need to store anything. if (_NormalizedTimes == null || (_NormalizedTimes.Length == 1 && (_Callbacks == null || _Callbacks.Length == 0) && (_Names == null || _Names.Length == 0) && float.IsNaN(_NormalizedTimes[0]))) { _NormalizedTimes = Array.Empty(); _Callbacks = Array.Empty(); _Names = Array.Empty(); return; } Trim(ref _Callbacks, _NormalizedTimes.Length, (callback) => HasPersistentCalls(callback)); Trim(ref _Names, _NormalizedTimes.Length, (name) => !string.IsNullOrEmpty(name)); } /************************************************************************************************************************/ /// [Editor-Only] Removes unimportant values from the end of the `array`. private static void Trim(ref T[] array, int maxLength, Func isImportant) { if (array == null) return; var count = Math.Min(array.Length, maxLength); while (count >= 1) { var item = array[count - 1]; if (isImportant(item)) break; else count--; } Array.Resize(ref array, count); } /************************************************************************************************************************/ #endif /************************************************************************************************************************/ } } } } /************************************************************************************************************************/ #if UNITY_EDITOR /************************************************************************************************************************/ namespace Animancer.Editor { /// [Editor-Only, Internal] /// A serializable container which holds a in a field named "_Callback". /// /// /// needs to be in a file with the same name as it (otherwise it can't /// draw the callback properly) and this class needs to be in the same file as /// to use the alias. /// [Serializable] internal sealed class SerializableCallbackHolder { #pragma warning disable CS0169 // Field is never used. [SerializeField] private SerializableCallback _Callback; #pragma warning restore CS0169 // Field is never used. /// The name of the field which stores the . internal const string CallbackField = nameof(_Callback); } } /************************************************************************************************************************/ #endif /************************************************************************************************************************/