// Animancer // Copyright 2020 Kybernetik // using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Playables; using UnityEngine.Animations; #if UNITY_EDITOR using UnityEditor; using UnityEditorInternal; #endif namespace Animancer { /// [Pro-Only] /// An which blends multiple child states. Unlike other mixers, this class does not /// perform any automatic weight calculations, it simple allows you to control the weight of all states manually. /// /// This mixer type is similar to the Direct Blend Type in Mecanim Blend Trees. /// public class ManualMixerState : MixerState { /************************************************************************************************************************/ #region Properties /************************************************************************************************************************/ /// The states managed by this mixer. public AnimancerState[] States { get; protected set; } /// The number of input ports in the . public override int PortCount { get { return States.Length; } } /************************************************************************************************************************/ /// Returns the array. public override IList ChildStates { get { return States; } } /************************************************************************************************************************/ /// /// The weighted average of each child state according to their /// . /// protected override float NewTime { get { var totalWeight = 0f; var normalizedTime = 0f; var length = 0f; var count = States.Length; while (--count >= 0) { var state = States[count]; if (state != null) { var weight = state.Weight; totalWeight += weight; normalizedTime += state.NormalizedTime * weight; length += state.Length * weight; } } if (totalWeight == 0) return 0; totalWeight = 1f / totalWeight; return normalizedTime * totalWeight * length * totalWeight; } set { var count = States.Length; if (value == 0) goto ZeroTime; var length = Length; if (length == 0) goto ZeroTime; value /= length;// Normalize. while (--count >= 0) { var state = States[count]; if (state != null) state.NormalizedTime = value; } return; // If the value is 0, we can set the child times slightly more efficiently. ZeroTime: while (--count >= 0) { var state = States[count]; if (state != null) state.Time = 0; } } } /************************************************************************************************************************/ /// /// The weighted average of each child state according to their /// . /// public override float Length { get { var length = 0f; var childWeight = CalculateTotalChildWeight(); if (childWeight == 0) return 0; childWeight = 1f / childWeight; var count = States.Length; while (--count >= 0) { var state = States[count]; if (state != null) length += state.Length * state.Weight * childWeight; } return length; } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Initialisation /************************************************************************************************************************/ /// /// Constructs a new without connecting it to the . /// protected ManualMixerState(AnimancerPlayable root) : base(root) { } /// /// Constructs a new and connects it to the `layer`. /// public ManualMixerState(AnimancerLayer layer) : base(layer) { } /// /// Constructs a new and connects it to the `parent` at the specified /// `index`. /// public ManualMixerState(AnimancerNode parent, int index) : base(parent, index) { } /************************************************************************************************************************/ /// /// Initialises this mixer with the specified number of ports which can be filled individually by . /// public virtual void Initialise(int portCount) { if (portCount <= 1) Debug.LogWarning(GetType() + " is being initialised with capacity <= 1. The purpose of a mixer is to mix multiple clips."); _Playable.SetInputCount(portCount); States = new AnimancerState[portCount]; } /************************************************************************************************************************/ /// /// Initialises this mixer with one state per clip. /// public void Initialise(params AnimationClip[] clips) { int count = clips.Length; _Playable.SetInputCount(count); if (count <= 1) Debug.LogWarning(GetType() + " is being initialised without multiple clips. The purpose of a mixer is to mix multiple clips."); States = new AnimancerState[count]; for (int i = 0; i < count; i++) { var clip = clips[i]; if (clip != null) new ClipState(this, i, clip); } } /************************************************************************************************************************/ /// /// Creates and returns a new to play the `clip` with this /// as its parent. /// public override ClipState CreateState(int index, AnimationClip clip) { var oldState = States[index]; if (oldState != null) { // If the old state has the specified `clip`, return it. if (oldState.Clip == clip) { return oldState as ClipState; } else// Otherwise destroy and replace it. { oldState.Destroy(); } } return base.CreateState(index, clip); } /************************************************************************************************************************/ /// Connects the `state` to this mixer at its . protected internal override void OnAddChild(AnimancerState state) { OnAddChild(States, state); } /// Disconnects the `state` from this mixer at its . protected internal override void OnRemoveChild(AnimancerState state) { Validate.RemoveChild(state, States); States[state.Index] = null; state.DisconnectFromGraph(); } /************************************************************************************************************************/ /// /// Destroys the of this mixer and its . /// public override void Destroy() { DestroyChildren(); base.Destroy(); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Child States /************************************************************************************************************************/ /// /// Calculates the sum of the of all child states. /// public float CalculateTotalChildWeight() { if (States == null) return 0; var total = 0f; var count = States.Length; while (--count >= 0) { var state = States[count]; if (state != null) total += state.Weight; } return total; } /************************************************************************************************************************/ /// /// Destroys all connected to this mixer. This operation cannot be undone. /// public void DestroyChildren() { if (States == null) return; var count = States.Length; while (--count >= 0) { var state = States[count]; if (state != null) state.Destroy(); } States = null; } /************************************************************************************************************************/ /// /// Does nothing. Manual mixers do not automatically recalculate their weights. /// public override void RecalculateWeights() { } /************************************************************************************************************************/ /// /// Sets the weight of all states after the `previousIndex` to 0. /// protected void DisableRemainingStates(int previousIndex) { var count = States.Length; while (++previousIndex < count) { var state = States[previousIndex]; if (state == null) continue; state.Weight = 0; } } /************************************************************************************************************************/ /// /// Returns the state at the specified `index` if it is not null, otherwise increments the index and checks /// again. Returns null if no state is found by the end of the array. /// protected AnimancerState GetNextState(ref int index) { while (index < States.Length) { var state = States[index]; if (state != null) return state; index++; } return null; } /************************************************************************************************************************/ /// /// Divides the weight of all states by the `totalWeight` so that they all add up to 1. /// protected void NormalizeWeights(float totalWeight) { if (totalWeight == 1) return; totalWeight = 1f / totalWeight; int count = States.Length; for (int i = 0; i < count; i++) { var state = States[i]; if (state == null) continue; state.Weight *= totalWeight; } } /************************************************************************************************************************/ /// Gets a user-friendly key to identify the `state` in the Inspector. public override string GetDisplayKey(AnimancerState state) { return string.Concat("[", state.Index.ToString(), "]"); } /************************************************************************************************************************/ #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 new class Transition : AnimancerState.Transition, IAnimationClipCollection where TMixer : ManualMixerState { /************************************************************************************************************************/ [SerializeField, HideInInspector] private AnimationClip[] _Clips; /// [] /// The to use for each state in the mixer. /// public AnimationClip[] Clips { get { return _Clips; } set { _Clips = value; } } [SerializeField, HideInInspector] private float[] _Speeds; /// [] /// The to use for each state in the mixer. /// /// If the size of this array doesn't match the , it will be ignored. /// public float[] Speeds { get { return _Speeds; } set { _Speeds = value; } } /************************************************************************************************************************/ [SerializeField, HideInInspector] private bool[] _SynchroniseChildren; /// [] /// The flags for each state in the mixer. /// /// The array can be null or empty. Any elements not in the array will be treated as true. /// public bool[] SynchroniseChildren { get { return _SynchroniseChildren; } set { _SynchroniseChildren = value; } } /************************************************************************************************************************/ /// [] /// Returns true is any of the are looping. /// public override bool IsLooping { get { var count = _Clips.Length; for (int i = 0; i < count; i++) { var clip = _Clips[i]; if (clip == null) continue; if (clip.isLooping) return true; } return false; } } /// [] /// The maximum amount of time the animation is expected to take (in seconds). /// public override float MaximumDuration { get { if (_Clips == null) return 0; var duration = 0f; var hasSpeeds = _Speeds != null && _Speeds.Length == _Clips.Length; for (int i = 0; i < _Clips.Length; i++) { var clip = _Clips[i]; if (clip == null) continue; var length = clip.length; if (hasSpeeds) length *= _Speeds[i]; if (duration < length) duration = length; } return duration; } } /************************************************************************************************************************/ /// /// Initialises the immediately after it is created. /// public virtual void InitialiseState() { State.Initialise(_Clips); if (_Speeds != null && _Speeds.Length == _Clips.Length) { for (int i = 0; i < State.States.Length; i++) { State.States[i].Speed = _Speeds[i]; } } State.SynchroniseChildren = _SynchroniseChildren; } /************************************************************************************************************************/ /// Adds the to the collection. void IAnimationClipCollection.GatherAnimationClips(ICollection clips) { clips.Gather(_Clips); } /************************************************************************************************************************/ #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); Transition.Drawer.AddItemsToContextMenu(menu, property); } /************************************************************************************************************************/ #endif /************************************************************************************************************************/ } /************************************************************************************************************************/ /// /// A serializable which can create a 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. /// [Serializable] public class Transition : Transition { /************************************************************************************************************************/ /// /// Creates and returns a new connected to the `layer`. /// /// This method also assigns it as the . /// public override ManualMixerState CreateState(AnimancerLayer layer) { State = new ManualMixerState(layer); InitialiseState(); return State; } /************************************************************************************************************************/ #region Drawer #if UNITY_EDITOR /************************************************************************************************************************/ /// [Editor-Only] Draws the Inspector GUI for a . [CustomPropertyDrawer(typeof(Transition), true)] public class Drawer : Editor.TransitionDrawer { /************************************************************************************************************************/ /// /// The property this drawer is currently drawing. /// /// Normally each property has its own drawer, but arrays share a single drawer for all elements. /// public static SerializedProperty CurrentProperty { get; private set; } /// The field. public static SerializedProperty CurrentClips { get; private set; } /// The field. public static SerializedProperty CurrentSpeeds { get; private set; } /// The field. public static SerializedProperty CurrentSynchroniseChildren { get; private set; } private readonly Dictionary PropertyPathToStates = new Dictionary(); /// /// Gather the details of the `property`. /// /// This method gets called by every and call since /// Unity uses the same instance for each element in a collection, so it /// needs to gather the details associated with the current property. /// protected virtual ReorderableList GatherDetails(SerializedProperty property) { InitialiseMode(property); GatherSubProperties(property); var propertyPath = property.propertyPath; ReorderableList states; if (!PropertyPathToStates.TryGetValue(propertyPath, out states)) { states = new ReorderableList(CurrentClips.serializedObject, CurrentClips) { drawHeaderCallback = DoStateListHeaderGUI, elementHeightCallback = GetElementHeight, drawElementCallback = DoElementGUI, onAddCallback = OnAddElement, onRemoveCallback = OnRemoveElement, #if UNITY_2018_1_OR_NEWER onReorderCallbackWithDetails = OnReorderList, #else onReorderCallback = OnReorderList, onSelectCallback = OnListSelectionChanged, #endif }; PropertyPathToStates.Add(propertyPath, states); } return states; } /************************************************************************************************************************/ /// /// Called every time a `property` is drawn to find the relevant child properties and store them to be /// used in and . /// protected virtual void GatherSubProperties(SerializedProperty property) { GatherSubPropertiesStatic(property); } /// /// Called every time a `property` is drawn to find the relevant child properties and store them to be /// used in and . /// public static void GatherSubPropertiesStatic(SerializedProperty property) { CurrentProperty = property; CurrentClips = property.FindPropertyRelative("_Clips"); CurrentSpeeds = property.FindPropertyRelative("_Speeds"); CurrentSynchroniseChildren = property.FindPropertyRelative("_SynchroniseChildren"); if (CurrentSpeeds.arraySize != 0) CurrentSpeeds.arraySize = CurrentClips.arraySize; } /************************************************************************************************************************/ /// /// Calculates the number of vertical pixels the `property` will occupy when it is drawn. /// public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { var height = EditorGUI.GetPropertyHeight(property, label); if (property.isExpanded) { var states = GatherDetails(property); height += Editor.AnimancerGUI.StandardSpacing + states.GetHeight(); } return height; } /************************************************************************************************************************/ /// /// Draws the root `property` GUI and calls /// for each of its children. /// public override void OnGUI(Rect area, SerializedProperty property, GUIContent label) { var originalProperty = property.Copy(); base.OnGUI(area, property, label); if (!originalProperty.isExpanded) return; var states = GatherDetails(originalProperty); var indentLevel = EditorGUI.indentLevel; area.yMin = area.yMax - states.GetHeight(); EditorGUI.indentLevel++; area = EditorGUI.IndentedRect(area); EditorGUI.indentLevel = 0; states.DoList(area); EditorGUI.indentLevel = indentLevel; TryCollapseSpeeds(); } /************************************************************************************************************************/ /// Splits the specified `area` into separate sections. protected void SplitListRect(Rect area, out Rect animation, out Rect speed, out Rect sync) { var spacing = Editor.AnimancerGUI.StandardSpacing; area.width += 2; sync = Editor.AnimancerGUI.StealFromRight(ref area, Editor.AnimancerGUI.ToggleWidth - spacing, spacing); speed = Editor.AnimancerGUI.StealFromRight(ref area, 55, spacing); animation = area; } /************************************************************************************************************************/ /// Draws the headdings of the state list. protected virtual void DoStateListHeaderGUI(Rect area) { Rect animationArea, speedArea, syncArea; SplitListRect(area, out animationArea, out speedArea, out syncArea); DoAnimationLabelGUI(animationArea); DoSpeedLabelGUI(speedArea); DoSyncLabelGUI(syncArea); } /************************************************************************************************************************/ /// Draws an "Animation" label. protected static void DoAnimationLabelGUI(Rect area) { EditorGUI.BeginProperty(area, GUIContent.none, CurrentClips); GUI.Label(area, Editor.AnimancerGUI.TempContent("Animation", "The animations that will be used for each child state")); EditorGUI.EndProperty(); } /// Draws a "Speed" label. protected static void DoSpeedLabelGUI(Rect area) { EditorGUI.BeginProperty(area, GUIContent.none, CurrentSpeeds); GUI.Label(area, Editor.AnimancerGUI.TempContent("Speed", "Determines how fast each child state plays (Default = 1)")); EditorGUI.EndProperty(); } private static float _SyncLabelWidth; /// Draws a "Sync" label. protected static void DoSyncLabelGUI(Rect area) { const string Text = "Sync"; if (_SyncLabelWidth == 0) _SyncLabelWidth = Editor.AnimancerGUI.CalculateLabelWidth(Text); if (area.width < _SyncLabelWidth) area.xMin = area.xMax - _SyncLabelWidth; EditorGUI.BeginProperty(area, GUIContent.none, CurrentSynchroniseChildren); GUI.Label(area, Editor.AnimancerGUI.TempContent(Text, "Determines which child states have their normalized times constantly synchronised")); EditorGUI.EndProperty(); } /************************************************************************************************************************/ /// Calculates the height of the state at the specified `index`. protected virtual float GetElementHeight(int index) { return Editor.AnimancerGUI.LineHeight; } /************************************************************************************************************************/ /// Draws the GUI of the state at the specified `index`. private void DoElementGUI(Rect area, int index, bool isActive, bool isFocused) { if (index < 0 || index > CurrentClips.arraySize) return; var clip = CurrentClips.GetArrayElementAtIndex(index); var speed = CurrentSpeeds.arraySize > 0 ? CurrentSpeeds.GetArrayElementAtIndex(index) : null; DoElementGUI(area, index, clip, speed); } /************************************************************************************************************************/ /// Draws the GUI of the state at the specified `index`. protected virtual void DoElementGUI(Rect area, int index, SerializedProperty clip, SerializedProperty speed) { Rect animationArea, speedArea, syncArea; SplitListRect(area, out animationArea, out speedArea, out syncArea); DoElementGUI(animationArea, speedArea, syncArea, index, clip, speed); } /// Draws the GUI of the state at the specified `index`. protected void DoElementGUI(Rect animationArea, Rect speedArea, Rect syncArea, int index, SerializedProperty clip, SerializedProperty speed) { EditorGUI.PropertyField(animationArea, clip, GUIContent.none); if (speed != null) { EditorGUI.PropertyField(speedArea, speed, GUIContent.none); } else { EditorGUI.BeginProperty(speedArea, GUIContent.none, CurrentSpeeds); var value = EditorGUI.FloatField(speedArea, 1); if (value != 1) { CurrentSpeeds.InsertArrayElementAtIndex(0); CurrentSpeeds.GetArrayElementAtIndex(0).floatValue = 1; CurrentSpeeds.arraySize = CurrentClips.arraySize; CurrentSpeeds.GetArrayElementAtIndex(index).floatValue = value; } EditorGUI.EndProperty(); } DoSyncToggleGUI(syncArea, index); } /************************************************************************************************************************/ /// /// Draws a toggle to enable or disable for the child at /// the specified `index`. /// protected void DoSyncToggleGUI(Rect area, int index) { var syncFlagCount = CurrentSynchroniseChildren.arraySize; var property = CurrentSynchroniseChildren; var enabled = true; if (index < syncFlagCount) { property = property.GetArrayElementAtIndex(index); enabled = property.boolValue; } EditorGUI.BeginChangeCheck(); EditorGUI.BeginProperty(area, GUIContent.none, property); enabled = GUI.Toggle(area, enabled, GUIContent.none); EditorGUI.EndProperty(); if (EditorGUI.EndChangeCheck()) { if (index < syncFlagCount) { property.boolValue = enabled; for (int i = syncFlagCount - 1; i >= 0; i--) { if (CurrentSynchroniseChildren.GetArrayElementAtIndex(i).boolValue) syncFlagCount = i; else break; } CurrentSynchroniseChildren.arraySize = syncFlagCount; } else { property.arraySize = index + 1; for (int i = syncFlagCount; i < index; i++) { property.GetArrayElementAtIndex(i).boolValue = true; } property.GetArrayElementAtIndex(index).boolValue = enabled; } } } /************************************************************************************************************************/ /// /// Called when adding a new state to the list to ensure that any other relevant arrays have new /// elements added as well. /// protected virtual void OnAddElement(ReorderableList list) { var index = CurrentClips.arraySize; CurrentClips.InsertArrayElementAtIndex(index); if (CurrentSpeeds.arraySize > 0) CurrentSpeeds.InsertArrayElementAtIndex(index); } /************************************************************************************************************************/ /// /// Called when removing a state from the list to ensure that any other relevant arrays have elements /// removed as well. /// protected virtual void OnRemoveElement(ReorderableList list) { var index = list.index; RemoveArrayElement(CurrentClips, index); if (CurrentSpeeds.arraySize > 0) RemoveArrayElement(CurrentSpeeds, index); } /// /// Removes the specified array element from the `property`. /// /// If the element is not at its default value, the first call to /// will only reset it, so this method will /// call it again if necessary to ensure that it actually gets removed. /// protected static void RemoveArrayElement(SerializedProperty property, int index) { var count = property.arraySize; property.DeleteArrayElementAtIndex(index); if (property.arraySize == count) property.DeleteArrayElementAtIndex(index); } /************************************************************************************************************************/ /// /// Called when reordering states in the list to ensure that any other relevant arrays have their /// corresponding elements reordered as well. /// protected virtual void OnReorderList(ReorderableList list, int oldIndex, int newIndex) { CurrentSpeeds.MoveArrayElement(oldIndex, newIndex); } #if !UNITY_2018_1_OR_NEWER private int _SelectedIndex; private void OnListSelectionChanged(ReorderableList list) { _SelectedIndex = list.index; } private void OnReorderList(ReorderableList list) { OnReorderList(list, _SelectedIndex, list.index); } #endif /************************************************************************************************************************/ #region Speeds /************************************************************************************************************************/ /// /// Initialises every element in the array from the `start` to the end of /// the array to contain a value of 1. /// public static void InitialiseSpeeds(int start) { var count = CurrentSpeeds.arraySize; while (start < count) CurrentSpeeds.GetArrayElementAtIndex(start++).floatValue = 1; } /************************************************************************************************************************/ /// /// If every element in the array is 1, this method sets the array size to 0. /// public static void TryCollapseSpeeds() { var speedCount = CurrentSpeeds.arraySize; if (speedCount <= 0) return; for (int i = 0; i < speedCount; i++) { if (CurrentSpeeds.GetArrayElementAtIndex(i).floatValue != 1) return; } CurrentSpeeds.arraySize = 0; } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Context Menu /************************************************************************************************************************/ /// [Editor-Only] Adds context menu functions for this transition. public static void AddItemsToContextMenu(GenericMenu menu, SerializedProperty property) { GatherSubPropertiesStatic(property); AddPropertyModifierFunction(menu, "Reset Speeds", (_) => CurrentSpeeds.arraySize = 0); AddPropertyModifierFunction(menu, "Normalize Durations", NormalizeDurations); } /************************************************************************************************************************/ /// /// Recalculates the depending on the of /// their animations so that they all take the same amount of time to play fully. /// private static void NormalizeDurations(SerializedProperty property) { var speedCount = CurrentSpeeds.arraySize; var lengths = new float[CurrentClips.arraySize]; if (lengths.Length <= 1) return; int nonZeroLengths = 0; float totalLength = 0; float totalSpeed = 0; for (int i = 0; i < lengths.Length; i++) { var clip = CurrentClips.GetArrayElementAtIndex(i).objectReferenceValue as AnimationClip; if (clip != null && clip.length > 0) { nonZeroLengths++; totalLength += clip.length; lengths[i] = clip.length; if (speedCount > 0) totalSpeed += CurrentSpeeds.GetArrayElementAtIndex(i).floatValue; } } if (nonZeroLengths == 0) return; var averageLength = totalLength / nonZeroLengths; var averageSpeed = speedCount > 0 ? totalSpeed / nonZeroLengths : 1; CurrentSpeeds.arraySize = lengths.Length; InitialiseSpeeds(speedCount); for (int i = 0; i < lengths.Length; i++) { if (lengths[i] == 0) continue; CurrentSpeeds.GetArrayElementAtIndex(i).floatValue = averageSpeed * lengths[i] / averageLength; } TryCollapseSpeeds(); } /************************************************************************************************************************/ /// /// Adds a menu function that will call then perform the specified /// `action`. /// protected static void AddPropertyModifierFunction(GenericMenu menu, string label, Action action) { Editor.Serialization.AddPropertyModifierFunction(menu, CurrentProperty, label, (property) => { GatherSubPropertiesStatic(property); action(property); }); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } /************************************************************************************************************************/ #endif #endregion /************************************************************************************************************************/ } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } }