// Animancer // Copyright 2020 Kybernetik // #pragma warning disable CS0649 // Field is never assigned to, and will always have its default value. using System; using System.Collections.Generic; using System.Text; using UnityEngine; using UnityEngine.Animations; using UnityEngine.Playables; using System.Collections; #if UNITY_EDITOR using UnityEditor; using UnityEditorInternal; #endif namespace Animancer { /// [Pro-Only] /// Base class for s which blend other states together. /// public abstract partial class MixerState : AnimancerState { /************************************************************************************************************************/ #region Properties /************************************************************************************************************************/ /// The number of input ports in the . public abstract int PortCount { get; } /************************************************************************************************************************/ /// Mixers should keep child playables connected to the graph at all times. public override bool KeepChildrenConnected { get { return true; } } /// An has no . public override AnimationClip Clip { get { return null; } } /************************************************************************************************************************/ /// /// Returns the collection of states connected to this mixer. Note that some elements may be null. /// /// Getting an enumerator that automatically skips over null states is slower and creates garbage, so /// internally we use this property and perform null checks manually even though it increases the code /// complexity a bit. /// public abstract IList ChildStates { get; } /// Returns an enumerator which will iterate through each state connected to this mixer. public override IEnumerator GetEnumerator() { var childStates = ChildStates; if (childStates == null) yield break; var count = childStates.Count; for (int i = 0; i < count; i++) { var state = childStates[i]; if (state == null) continue; yield return state; } } /************************************************************************************************************************/ /// Indicates whether the weights of all child states should be recalculated. public bool WeightsAreDirty { get; set; } /************************************************************************************************************************/ /// /// Determines whether the states in this mixer are playing. /// public override bool IsPlaying { get { return base.IsPlaying; } set { base.IsPlaying = value; var childStates = ChildStates; if (childStates == null) return; var count = childStates.Count; for (int i = 0; i < count; i++) { var state = childStates[i]; if (state == null) continue; state.IsPlaying = value; } } } /************************************************************************************************************************/ /// Returns true if any child state is looping. public override bool IsLooping { get { var childStates = ChildStates; if (childStates == null) return false; var count = childStates.Count; for (int i = 0; i < count; i++) { var state = childStates[i]; if (state == null) continue; if (state.IsLooping) return true; } return false; } } /************************************************************************************************************************/ private bool[] _SynchroniseChildren; /// /// Indicates which children should have their modified in order to keep /// their at approximately the same value. /// /// The array can be null or empty. Any elements not in the array will be treated as true. /// /// /// The is modified to allow each state to trigger its events properly /// where setting the directly would prevent any events. /// public bool[] SynchroniseChildren { get { return _SynchroniseChildren; } set { if (value == null || value.Length == 0) { _SynchroniseChildren = null; } else { // Reset the speed of any children that are no longer synced to 1. var childStates = ChildStates; if (childStates != null) { var count = Mathf.Min(childStates.Count, value.Length); for (int i = 0; i < count; i++) { if (value[i]) continue; var state = childStates[i]; if (state == null) continue; state.Speed = 1; } } _SynchroniseChildren = value; } } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Initialisation /************************************************************************************************************************/ /// /// Constructs a new without connecting it to the . /// public MixerState(AnimancerPlayable root) : base(root) { _Playable = AnimationMixerPlayable.Create(root._Graph, 0, true); } /// /// Constructs a new and connects it to the `layer`. /// public MixerState(AnimancerLayer layer) : this(layer.Root) { layer.AddChild(this); } /// /// Constructs a new and connects it to the `parent` at the specified /// `index`. /// public MixerState(AnimancerNode parent, int index) : this(parent.Root) { SetParent(parent, index); } /************************************************************************************************************************/ /// /// Creates and returns a new to play the `clip` with this /// as its parent. /// public virtual ClipState CreateState(int index, AnimationClip clip) { return new ClipState(this, index, clip) { IsPlaying = IsPlaying, Time = Time }; } /************************************************************************************************************************/ /// The number of states using this mixer as their . public override int ChildCount { get { return ChildStates.Count; } } /// /// Returns the state connected to the specified `index` as a child of this mixer. /// public override AnimancerState GetChild(int index) { return ChildStates[index]; } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Updates /************************************************************************************************************************/ /// /// Updates the time of this mixer and all of its child states. /// protected internal override void Update(out bool needsMoreUpdates) { base.Update(out needsMoreUpdates); var childStates = ChildStates; if (childStates == null) return; if (WeightsAreDirty && Weight != 0) { RecalculateWeights(); Debug.Assert(!WeightsAreDirty, "MixerState.AreWeightsDirty has not been set to false by RecalculateWeights()."); // We need to apply the child weights immediately to ensure they are all in sync. Otherwise some of the // child states might have already been updated before the mixer and would not apply it until next frame. var count = childStates.Count; for (int i = 0; i < count; i++) { var state = childStates[i]; if (state == null) continue; state.ApplyWeight(); } } ApplySynchroniseChildren(ref needsMoreUpdates); } /************************************************************************************************************************/ /// Applies the effects of . protected void ApplySynchroniseChildren(ref bool needsMoreUpdates) { if (AnimancerPlayable.DeltaTime == 0) return; var childStates = ChildStates; var childCount = childStates.Count; if (childStates == null) return; // Get the number of elements in the array. int flagCount; if (_SynchroniseChildren == null) { flagCount = 0; } else { flagCount = _SynchroniseChildren.Length; if (flagCount >= childCount) { for (int i = 0; i < flagCount; i++) { if (_SynchroniseChildren[i]) goto Continue; } // If none of the flags are true, do nothing. return; } } Continue: needsMoreUpdates = true; // Calculate the weighted average normalized time of all children. var totalWeight = 0f; var weightedNormalizedTime = 0f; var weightedLength = 0f; for (int i = 0; i < childCount; i++) { if (i < flagCount && !_SynchroniseChildren[i]) continue; var state = childStates[i]; if (state == null) continue; var weight = state.Weight; if (weight == 0) continue; totalWeight += weight; weightedNormalizedTime += state.NormalizedTime * weight; weightedLength += state.Length * weight; } if (totalWeight <= 0 || weightedLength <= 0) return; // Increment that time value according to delta time. weightedNormalizedTime /= totalWeight; weightedNormalizedTime += AnimancerPlayable.DeltaTime * totalWeight / weightedLength; // Modify the speed of all children to go from their current normalized time to the average in one frame. var inverseDeltaTime = 1f / AnimancerPlayable.DeltaTime; for (int i = 0; i < childCount; i++) { if (i < flagCount && !_SynchroniseChildren[i]) continue; var state = childStates[i]; if (state == null) continue; state.Speed = (weightedNormalizedTime - state.NormalizedTime) * state.Length * inverseDeltaTime; } // After this, all the playables will update and advance according to their new speeds this frame. } /************************************************************************************************************************/ /// /// Recalculates the weights of all based on the current value of the /// and the thresholds. /// /// Overrides of this method must set = false. /// public abstract void RecalculateWeights(); /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Inverse Kinematics /************************************************************************************************************************/ /// /// Determines whether OnAnimatorIK(int layerIndex) will be called on the animated object for any /// . /// The initial value is determined by . /// /// This is equivalent to the "IK Pass" toggle in Animator Controller layers. /// /// It requires Unity 2018.1 or newer, however 2018.3 or newer is recommended because a bug in earlier versions /// of the Playables API caused this value to only take effect while a state was at /// == 1 which meant that IK would not work while fading between animations. /// public override bool ApplyAnimatorIK { get { var childStates = ChildStates; if (childStates == null) return false; var count = childStates.Count; for (int i = 0; i < count; i++) { var state = childStates[i]; if (state == null) continue; if (state.ApplyAnimatorIK) return true; } return false; } set { var childStates = ChildStates; if (childStates == null) return; var count = childStates.Count; for (int i = 0; i < count; i++) { var state = childStates[i]; if (state == null) continue; state.ApplyAnimatorIK = value; } } } /************************************************************************************************************************/ /// /// Indicates whether any of the in this mixer are applying IK to the character's feet. /// The initial value is determined by . /// /// This is equivalent to the "Foot IK" toggle in Animator Controller states. /// public override bool ApplyFootIK { get { var childStates = ChildStates; if (childStates == null) return false; var count = childStates.Count; for (int i = 0; i < count; i++) { var state = childStates[i]; if (state == null) continue; if (state.ApplyFootIK) return true; } return false; } set { var childStates = ChildStates; if (childStates == null) return; var count = childStates.Count; for (int i = 0; i < count; i++) { var state = childStates[i]; if (state == null) continue; state.ApplyFootIK = value; } } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Other Methods /************************************************************************************************************************/ /// /// Sets for all . /// public void SetChildrenTime(float value, bool normalized = false) { var childStates = ChildStates; if (childStates == null) return; var count = childStates.Count; for (int i = 0; i < count; i++) { var state = childStates[i]; if (state == null) continue; if (normalized) state.NormalizedTime = value; else state.Time = value; } } /************************************************************************************************************************/ /// The average velocity of the root motion caused by this state. public override Vector3 AverageVelocity { get { var velocity = default(Vector3); var childStates = ChildStates; if (childStates != null) { var count = childStates.Count; for (int i = 0; i < count; i++) { var state = childStates[i]; if (state == null) continue; velocity += state.AverageVelocity * state.Weight; } } return velocity; } } /************************************************************************************************************************/ /// /// Recalculates the of all child states so that they add up to 1. /// /// Thrown if there are any states with no . public void NormalizeDurations() { var childStates = ChildStates; if (childStates == null) return; int divideBy = 0; float totalDuration = 0f; // Count the number of states that exist and their total duration. var count = childStates.Count; for (int i = 0; i < count; i++) { var state = childStates[i]; if (state == null) continue; divideBy++; totalDuration += state.Duration; } // Calculate the average duration. totalDuration /= divideBy; // Set all states to that duration. for (int i = 0; i < count; i++) { var state = childStates[i]; if (state == null) continue; state.Duration = totalDuration; } } /************************************************************************************************************************/ /// Gets a user-friendly key to identify the `state` in the Inspector. public abstract string GetDisplayKey(AnimancerState state); /************************************************************************************************************************/ /// /// Returns a string describing the type of this mixer and the name of s connected to it. /// public override string ToString() { var name = new StringBuilder(); name.Append(GetType().Name); name.Append(" ("); bool first = true; var childStates = ChildStates; if (childStates == null) { name.Append("null"); } else { var count = childStates.Count; for (int i = 0; i < count; i++) { var state = childStates[i]; if (state == null) continue; if (first) first = false; else name.Append(", "); if (state.Clip != null) name.Append(state.Clip.name); else name.Append(state); } } name.Append(")"); return name.ToString(); } /************************************************************************************************************************/ /// [] /// Gathers all the animations in this state. /// public override void GatherAnimationClips(ICollection clips) { clips.GatherFromSources((IList)ChildStates); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Inspector /************************************************************************************************************************/ /// The number of parameters being managed by this state. protected virtual int ParameterCount { get { return 0; } } /// Returns the name of a parameter being managed by this state. /// Thrown if this state doesn't manage any parameters. protected virtual string GetParameterName(int index) { throw new NotSupportedException(); } /// Returns the type of a parameter being managed by this state. /// Thrown if this state doesn't manage any parameters. protected virtual UnityEngine.AnimatorControllerParameterType GetParameterType(int index) { throw new NotSupportedException(); } /// Returns the value of a parameter being managed by this state. /// Thrown if this state doesn't manage any parameters. protected virtual object GetParameterValue(int index) { throw new NotSupportedException(); } /// Sets the value of a parameter being managed by this state. /// Thrown if this state doesn't manage any parameters. protected virtual void SetParameterValue(int index, object value) { throw new NotSupportedException(); } /************************************************************************************************************************/ #if UNITY_EDITOR /************************************************************************************************************************/ /// [Editor-Only] Returns a for this state. protected internal override Editor.IAnimancerNodeDrawer GetDrawer() { return new Drawer(this); } /************************************************************************************************************************/ /// [Editor-Only] Draws the Inspector GUI for an . public sealed class Drawer : Editor.ParametizedAnimancerStateDrawer { /************************************************************************************************************************/ /// /// Constructs a new to manage the Inspector GUI for the `state`. /// public Drawer(MixerState state) : base(state) { } /************************************************************************************************************************/ /// The number of parameters being managed by the target state. public override int ParameterCount { get { return Target.ParameterCount; } } /// Returns the name of a parameter being managed by the target state. /// Thrown if the target state doesn't manage any parameters. public override string GetParameterName(int index) { return Target.GetParameterName(index); } /// Returns the type of a parameter being managed by the target state. /// Thrown if the target state doesn't manage any parameters. public override UnityEngine.AnimatorControllerParameterType GetParameterType(int index) { return Target.GetParameterType(index); } /// Returns the value of a parameter being managed by the target state. /// Thrown if the target state doesn't manage any parameters. public override object GetParameterValue(int index) { return Target.GetParameterValue(index); } /// Sets the value of a parameter being managed by the target state. /// Thrown if the target state doesn't manage any parameters. public override void SetParameterValue(int index, object value) { Target.SetParameterValue(index, value); } /************************************************************************************************************************/ } /************************************************************************************************************************/ #endif /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Transition /************************************************************************************************************************/ /// /// Base class for serializable s which can create a particular type of /// when passed into /// . /// /// /// Unfortunately the tool used to generate this documentation does not currently support nested types with /// identical names, so only one Transition class will actually have a documentation page. /// /// Even though it has the , this class won't actually get serialized /// by Unity because it's generic and abstract. Each child class still needs to include the attribute. /// [Serializable] public abstract class Transition : ManualMixerState.Transition where TMixer : MixerState { /************************************************************************************************************************/ [SerializeField, HideInInspector] private TParameter[] _Thresholds; /// [] /// The parameter values at which each of the states are used and blended. /// public TParameter[] Thresholds { get { return _Thresholds; } set { _Thresholds = value; } } /************************************************************************************************************************/ [SerializeField] private TParameter _DefaultParameter; /// [] /// The initial parameter value to give the mixer when it is first created. /// public TParameter DefaultParameter { get { return _DefaultParameter; } set { _DefaultParameter = value; } } /************************************************************************************************************************/ /// /// Initialises the immediately after it is created. /// public override void InitialiseState() { base.InitialiseState(); State.SetThresholds(_Thresholds); State.Parameter = _DefaultParameter; } /************************************************************************************************************************/ } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Transition 2D /************************************************************************************************************************/ /// /// A serializable which can create a or /// when passed into /// . /// [Serializable] public class Transition2D : Transition, Vector2> { /************************************************************************************************************************/ /// /// A type of that can be created by a . /// public enum MixerType { /// Cartesian, /// Directional, } [SerializeField] private MixerType _Type; /// [] /// The type of that this transition will create. /// public MixerType Type { get { return _Type; } set { _Type = value; } } /************************************************************************************************************************/ /// /// Creates and returns a new or /// depending on the connected to the `layer`. /// /// This method also assigns it as the . /// public override MixerState CreateState(AnimancerLayer layer) { switch (_Type) { case MixerType.Cartesian: State = new CartesianMixerState(layer); break; case MixerType.Directional: State = new DirectionalMixerState(layer); break; default: throw new ArgumentOutOfRangeException("_Type"); } InitialiseState(); return State; } /************************************************************************************************************************/ #region Drawer #if UNITY_EDITOR /************************************************************************************************************************/ /// [Editor-Only] Adds context menu functions for this transition. protected override void AddItemsToContextMenu(GenericMenu menu, SerializedProperty property, Editor.Serialization.PropertyAccessor accessor) { base.AddItemsToContextMenu(menu, property, accessor); Drawer.AddItemsToContextMenu(menu); } /************************************************************************************************************************/ /// [Editor-Only] /// Draws the Inspector GUI for a . /// [CustomPropertyDrawer(typeof(Transition2D), true)] public class Drawer : TransitionDrawer { /************************************************************************************************************************/ /// /// Constructs a new using the a wider `thresholdWidth` than usual to accomodate /// both the X and Y values. /// public Drawer() : base(ThresholdLabelWidth * 2 + 20) { } /************************************************************************************************************************/ /// /// Fills the `menu` with functions relevant to the `rootProperty`. /// public static void AddItemsToContextMenu(GenericMenu menu) { AddPropertyModifierFunction(menu, "Initialise Standard 4 Directions", InitialiseStandard4Directions); AddThresholdItemsToContextMenu(menu); } /// /// Fills the `menu` with functions relevant to the `rootProperty`. /// public static void AddThresholdItemsToContextMenu(GenericMenu menu, string prefix = "Calculate Thresholds") { var fromVelocity = "From Velocity"; if (prefix != null) fromVelocity = prefix + "/" + fromVelocity; AddCalculateThresholdsFunction(menu, fromVelocity + "/XY", (clip, threshold) => { var velocity = clip.averageSpeed; return new Vector2(velocity.x, velocity.y); }); AddCalculateThresholdsFunction(menu, fromVelocity + "/XZ", (clip, threshold) => { var velocity = clip.averageSpeed; return new Vector2(velocity.x, velocity.z); }); AddCalculateThresholdsFunctionPerAxis(menu, prefix, "From Speed", (clip, threshold) => clip.apparentSpeed); AddCalculateThresholdsFunctionPerAxis(menu, prefix, "From Velocity X", (clip, threshold) => clip.averageSpeed.x); AddCalculateThresholdsFunctionPerAxis(menu, prefix, "From Velocity Y", (clip, threshold) => clip.averageSpeed.z); AddCalculateThresholdsFunctionPerAxis(menu, prefix, "From Velocity Z", (clip, threshold) => clip.averageSpeed.z); AddCalculateThresholdsFunctionPerAxis(menu, prefix, "From Angular Speed (Rad)", (clip, threshold) => clip.averageAngularSpeed * Mathf.Deg2Rad); AddCalculateThresholdsFunctionPerAxis(menu, prefix, "From Angular Speed (Deg)", (clip, threshold) => clip.averageAngularSpeed); } /************************************************************************************************************************/ private static void InitialiseStandard4Directions(SerializedProperty property) { var oldSpeedCount = CurrentSpeeds.arraySize; CurrentClips.arraySize = CurrentThresholds.arraySize = CurrentSpeeds.arraySize = 5; CurrentThresholds.GetArrayElementAtIndex(0).vector2Value = Vector2.zero; CurrentThresholds.GetArrayElementAtIndex(1).vector2Value = Vector2.up; CurrentThresholds.GetArrayElementAtIndex(2).vector2Value = Vector2.right; CurrentThresholds.GetArrayElementAtIndex(3).vector2Value = Vector2.down; CurrentThresholds.GetArrayElementAtIndex(4).vector2Value = Vector2.left; InitialiseSpeeds(oldSpeedCount); var type = property.FindPropertyRelative("_Type"); type.enumValueIndex = (int)MixerType.Directional; } /************************************************************************************************************************/ private static void AddCalculateThresholdsFunction(GenericMenu menu, string label, Func calculateThreshold) { AddPropertyModifierFunction(menu, label, (property) => { var count = CurrentClips.arraySize; for (int i = 0; i < count; i++) { var clip = CurrentClips.GetArrayElementAtIndex(i).objectReferenceValue as AnimationClip; if (clip == null) continue; var threshold = CurrentThresholds.GetArrayElementAtIndex(i); var value = calculateThreshold(clip, threshold.vector2Value); if (!float.IsNaN(value.x) && !float.IsNaN(value.y)) threshold.vector2Value = value; } }); } /************************************************************************************************************************/ private static void AddCalculateThresholdsFunctionPerAxis(GenericMenu menu, string prefix, string label, Func calculateThreshold) { if (prefix != null) prefix += " "; AddCalculateThresholdsFunction(menu, prefix + "X/" + label, 0, calculateThreshold); AddCalculateThresholdsFunction(menu, prefix + "Y/" + label, 1, calculateThreshold); } private static void AddCalculateThresholdsFunction(GenericMenu menu, string label, int axis, Func calculateThreshold) { AddPropertyModifierFunction(menu, label, (property) => { var count = CurrentClips.arraySize; for (int i = 0; i < count; i++) { var clip = CurrentClips.GetArrayElementAtIndex(i).objectReferenceValue as AnimationClip; if (clip == null) continue; var threshold = CurrentThresholds.GetArrayElementAtIndex(i); var value = threshold.vector2Value; var newValue = calculateThreshold(clip, value[axis]); if (!float.IsNaN(newValue)) value[axis] = newValue; threshold.vector2Value = value; } }); } /************************************************************************************************************************/ /// will add some functions to the menu. protected override bool HasThresholdContextMenu { get { return true; } } /// Adds functions to the `menu` relating to the thresholds. protected override void AddThresholdItemsToMenu(GenericMenu menu) { AddThresholdItemsToContextMenu(menu, null); } /************************************************************************************************************************/ } /************************************************************************************************************************/ #endif #endregion /************************************************************************************************************************/ } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Transition Drawer #if UNITY_EDITOR /************************************************************************************************************************/ /// [Editor-Only] Draws the Inspector GUI for a . /// /// This class would be nested inside , but the generic parameters /// cause problems in Unity 2019.3. /// [CustomPropertyDrawer(typeof(Transition<,>), true)] public class TransitionDrawer : ManualMixerState.Transition.Drawer { /************************************************************************************************************************/ /// /// The number of horizontal pixels the "Threshold" label occupies. /// private readonly float ThresholdWidth; private static float _ThresholdLabelWidth; /// /// The number of horizontal pixels the word "Threshold" label occupies when drawn with the standard /// style. /// protected static float ThresholdLabelWidth { get { if (_ThresholdLabelWidth == 0) _ThresholdLabelWidth = Editor.AnimancerGUI.CalculateWidth(EditorStyles.popup, "Threshold"); return _ThresholdLabelWidth; } } /************************************************************************************************************************/ /// /// Constructs a new using the default . /// public TransitionDrawer() : this(ThresholdLabelWidth) { } /// /// Constructs a new using a custom width for its threshold labels. /// protected TransitionDrawer(float thresholdWidth) { ThresholdWidth = thresholdWidth; } /************************************************************************************************************************/ /// /// The serialized of the /// . /// protected static SerializedProperty CurrentThresholds { get; private set; } /************************************************************************************************************************/ /// /// Called every time a `property` is drawn to find the relevant child properties and store them to be /// used in and /// . /// protected override void GatherSubProperties(SerializedProperty property) { base.GatherSubProperties(property); CurrentThresholds = property.FindPropertyRelative("_Thresholds"); var count = Math.Max(CurrentClips.arraySize, CurrentThresholds.arraySize); CurrentClips.arraySize = count; CurrentThresholds.arraySize = count; if (CurrentSpeeds.arraySize != 0) CurrentSpeeds.arraySize = count; } /************************************************************************************************************************/ /// Splits the specified `area` into separate sections. protected void SplitListRect(Rect area, out Rect animation, out Rect threshold, out Rect speed, out Rect sync) { SplitListRect(area, out animation, out speed, out sync); threshold = animation; var xMin = threshold.xMin = Math.Max( EditorGUIUtility.labelWidth + Editor.AnimancerGUI.IndentSize, threshold.xMax - ThresholdWidth); animation.xMax = xMin - Editor.AnimancerGUI.StandardSpacing; } /************************************************************************************************************************/ /// Draws the headdings of the state list. protected override void DoStateListHeaderGUI(Rect area) { Rect animationArea, thresholdArea, speedArea, syncArea; SplitListRect(area, out animationArea, out thresholdArea, out speedArea, out syncArea); DoAnimationLabelGUI(animationArea); EditorGUI.BeginProperty(thresholdArea, GUIContent.none, CurrentThresholds); var content = Editor.AnimancerGUI.TempContent("Threshold", "The parameter values at which each child state will be fully active"); if (HasThresholdContextMenu) { if (EditorGUI.DropdownButton(thresholdArea, content, FocusType.Passive)) { var menu = new GenericMenu(); AddThresholdItemsToMenu(menu); menu.ShowAsContext(); } } else { GUI.Label(thresholdArea, content); } EditorGUI.EndProperty(); DoSpeedLabelGUI(speedArea); DoSyncLabelGUI(syncArea); } /************************************************************************************************************************/ /// Draws the GUI of the state at the specified `index`. protected override void DoElementGUI(Rect area, int index, SerializedProperty clip, SerializedProperty speed) { var threshold = CurrentThresholds.GetArrayElementAtIndex(index); Rect animationArea, thresholdArea, speedArea, syncArea; SplitListRect(area, out animationArea, out thresholdArea, out speedArea, out syncArea); DoElementGUI(animationArea, speedArea, syncArea, index, clip, speed); EditorGUI.PropertyField(thresholdArea, threshold, GUIContent.none); } /************************************************************************************************************************/ /// /// Called when adding a new state to the list to ensure that any other relevant arrays have new /// elements added as well. /// protected override void OnAddElement(ReorderableList list) { var index = CurrentClips.arraySize; base.OnAddElement(list); CurrentThresholds.InsertArrayElementAtIndex(index); } /************************************************************************************************************************/ /// /// Called when removing a state from the list to ensure that any other relevant arrays have elements /// removed as well. /// protected override void OnRemoveElement(ReorderableList list) { base.OnRemoveElement(list); RemoveArrayElement(CurrentThresholds, list.index); } /************************************************************************************************************************/ /// /// Called when reordering states in the list to ensure that any other relevant arrays have their /// corresponding elements reordered as well. /// protected override void OnReorderList(ReorderableList list, int oldIndex, int newIndex) { base.OnReorderList(list, oldIndex, newIndex); CurrentThresholds.MoveArrayElement(oldIndex, newIndex); } /************************************************************************************************************************/ /// Indicates whether will add anything to the menu. protected virtual bool HasThresholdContextMenu { get { return false; } } /// Adds functions to the `menu` relating to the thresholds. protected virtual void AddThresholdItemsToMenu(GenericMenu menu) { } /************************************************************************************************************************/ } /************************************************************************************************************************/ #endif #endregion /************************************************************************************************************************/ } }