public static void RemoveChild(AnimancerState state, IList <AnimancerState> states) { var index = state.Index; if (index < 0) { throw new InvalidOperationException( "Tried to remove a child state that did not actually have a Index assigned"); } if (index > states.Count) { throw new IndexOutOfRangeException( "state.Index (" + state.Index + ") is outside the collection of states (count " + states.Count + ")"); } if (states[state.Index] != state) { throw new InvalidOperationException( "Tried to remove a child state that was not actually connected to its port on " + state.Parent + ":" + "\n Port: " + state.Index + "\n Connected Child: " + states[state.Index] + "\n Disconnecting Child: " + state); } }
/// <summary> /// Stops all other animations, plays the `state`, and returns it. /// <para></para> /// The animation will continue playing from its current <see cref="AnimancerState.Time"/>. /// If you wish to force it back to the start, you can simply set the `state`s time to 0. /// </summary> public AnimancerState Play(AnimancerState state) { Validate.Root(state, Root); if (TargetWeight != 1) { Weight = 1; } AddChild(state); CurrentState = state; state.Play(); var count = States.Count; for (int i = 0; i < count; i++) { var otherState = States[i]; if (otherState != state) { otherState.Stop(); } } return(state); }
/************************************************************************************************************************/ /// <summary> /// Sets the <see cref="CurrentState"/> and <see cref="CurrentEvent"/> then invokes the <see cref="callback"/>. /// </summary> /// <remarks>This method catches and logs any exception thrown by the <see cref="callback"/>.</remarks> /// <exception cref="NullReferenceException">The <see cref="callback"/> is null.</exception> public void Invoke(AnimancerState state) { #if UNITY_ASSERTIONS if (IsNullOrDummy(callback)) { OptionalWarning.UselessEvent.Log( $"An {nameof(AnimancerEvent)} that does nothing was invoked." + " Most likely it was not configured correctly." + " Unused events should be removed to avoid wasting performance checking and invoking them.", state?.Root?.Component); } #endif var previousState = _CurrentState; var previousEvent = _CurrentEvent; _CurrentState = state; _CurrentEvent = this; try { callback(); } catch (Exception exception) { Debug.LogException(exception, state?.Root?.Component as Object); } _CurrentState = previousState; _CurrentEvent = previousEvent; }
/************************************************************************************************************************/ /// <summary> /// Manipulates the other parameters according to the `mode`. /// </summary> private void EvaluateFadeMode(FadeMode mode, ref AnimancerState state, ref float fadeDuration, out bool isFixedDuration) { switch (mode) { case FadeMode.FixedSpeed: fadeDuration *= Mathf.Abs(1 - state.Weight); isFixedDuration = false; break; case FadeMode.FixedDuration: isFixedDuration = true; break; case FadeMode.FromStart: { var previousState = state; state = GetOrCreateWeightlessState(state); if (previousState != state) { var previousLayer = previousState.Layer; if (previousLayer != this && previousLayer.CurrentState == previousState) { previousLayer.StartFade(0, fadeDuration); } } isFixedDuration = false; break; } case FadeMode.NormalizedSpeed: fadeDuration *= Mathf.Abs(1 - state.Weight) * state.Length; isFixedDuration = false; break; case FadeMode.NormalizedDuration: fadeDuration *= state.Length; isFixedDuration = true; break; case FadeMode.NormalizedFromStart: { var previousState = state; state = GetOrCreateWeightlessState(state); fadeDuration *= state.Length; if (previousState != state) { var previousLayer = previousState.Layer; if (previousLayer != this && previousLayer.CurrentState == previousState) { previousLayer.StartFade(0, fadeDuration); } } isFixedDuration = false; break; } default: throw new ArgumentException("Invalid FadeMode: " + mode, "mode"); } }
public static void AssertCanRemoveChild(AnimancerState state, IList <AnimancerState> states) { #if UNITY_ASSERTIONS var index = state.Index; if (index < 0) { throw new InvalidOperationException( $"Cannot remove a child state that did not have an {nameof(state.Index)} assigned"); } if (index > states.Count) { throw new IndexOutOfRangeException( $"{nameof(AnimancerState)}.{nameof(state.Index)} ({state.Index})" + $" is outside the collection of states (count {states.Count})"); } if (states[state.Index] != state) { throw new InvalidOperationException( $"Cannot remove a child state that was not actually connected to its port on {state.Parent}:" + $"\n• Port: {state.Index}" + $"\n• Connected Child: {AnimancerUtilities.ToStringOrNull(states[state.Index])}" + $"\n• Disconnecting Child: {AnimancerUtilities.ToStringOrNull(state)}"); } #endif }
/************************************************************************************************************************/ /// <summary> /// If the <see cref="AnimancerState.Clip"/> and <see cref="AnimancerNode.Weight"/> match the /// <see cref="AnimationEvent"/>, this method invokes the <see cref="AnimancerEvent.Sequence.OnEnd"/> callback /// and returns true. /// </summary> private static bool TryInvokeOnEndEvent(AnimancerState state, AnimationEvent animationEvent) { if (state.Weight != animationEvent.animatorClipInfo.weight || state.Clip != animationEvent.animatorClipInfo.clip || !state.HasEvents) { return(false); } var endEvent = state.Events.endEvent; if (endEvent.callback != null) { Debug.Assert(CurrentEvent == null, $"Recursive call to {nameof(TryInvokeOnEndEvent)} detected"); try { CurrentEvent = animationEvent; endEvent.Invoke(state); } finally { CurrentEvent = null; } } return(true); }
public static void AssertCanRemoveChild(AnimancerState state, IList <AnimancerState> states) { #if UNITY_ASSERTIONS var index = state.Index; if (index < 0) { throw new InvalidOperationException( "Cannot remove a child state that did not have an Index assigned"); } if (index > states.Count) { throw new IndexOutOfRangeException( "AnimancerState.Index (" + state.Index + ") is outside the collection of states (count " + states.Count + ")"); } if (states[state.Index] != state) { throw new InvalidOperationException( "Cannot remove a child state that was not actually connected to its port on " + state.Parent + ":" + "\n Port: " + state.Index + "\n Connected Child: " + AnimancerUtilities.ToStringOrNull(states[state.Index]) + "\n Disconnecting Child: " + AnimancerUtilities.ToStringOrNull(state)); } #endif }
/************************************************************************************************************************/ /// <summary>Disconnects the `state` from this layer at its <see cref="AnimancerNode.Index"/>.</summary> protected internal override void OnRemoveChild(AnimancerState state) { var index = state.Index; Validate.RemoveChild(state, States); if (_Playable.GetInput(index).IsValid()) { Root._Graph.Disconnect(_Playable, index); } // Swap the last state into the place of the one that was just removed. var lastPort = States.Count - 1; if (index < lastPort) { state = States[lastPort]; state.DisconnectFromGraph(); States[index] = state; state.Index = index; if (KeepChildrenConnected || state.Weight != 0) { state.ConnectToGraph(); } } States.RemoveAt(lastPort); _Playable.SetInputCount(lastPort); }
/************************************************************************************************************************/ /// <summary> /// Throws an <see cref="ArgumentException"/> if the <see cref="AnimancerState.Parent"/> is not this /// <see cref="AnimancerLayer"/>. /// </summary> /// <exception cref="ArgumentException"/> public void ValidateState(AnimancerState state) { if (state.Parent != this) { throw new ArgumentException("AnimancerState.Parent mismatch:" + " you are attempting to use a state in an AnimancerLayer that isn't it's parent."); } }
public static void Parent(AnimancerState state, AnimancerNode parent) { if (state.Parent != parent) { throw new ArgumentException("AnimancerState.Parent mismatch:" + " you are attempting to use a state in an AnimancerLayer that is not it's parent."); } }
/// <summary> /// Sets the <see cref="Source"/> and <see cref="Callback"/>. /// </summary> public void Set(AnimancerState source, Action <AnimationEvent> callback) { Source = source; Callback = callback; #if UNITY_EDITOR ValidateSourceHasCorrectEvent(); #endif }
/// <summary> /// Returns the value of the <see cref="Curve"/> at the current <see cref="AnimancerState.Time"/>. /// </summary> public float Evaluate(AnimancerState state) { if (state == null) { return(0); } return(Evaluate(state.Time % state.Length)); }
/// <summary>[Internal] Removes the `state` from this dictionary (the opposite of <see cref="Register"/>).</summary> internal void Unregister(AnimancerState state) { var key = state._Key; if (key != null) { States.Remove(key); } }
/// <summary>[Internal] Removes the `state` from this dictionary (the opposite of <see cref="Register"/>).</summary> internal void Unregister(AnimancerState state) { if (state._Key == null) { return; } States.Remove(state._Key); state._Key = null; }
/// <summary> /// If a state is registered with the `key`, this method outputs it as the `state` and returns true. Otherwise /// `state` is set to null and this method returns false. /// </summary> public bool TryGet(object key, out AnimancerState state) { if (key == null) { state = null; return(false); } return(States.TryGetValue(key, out state)); }
/************************************************************************************************************************/ /// <summary> /// Calls <see cref="GetKey"/> then passes the key to /// <see cref="TryGet(object, out AnimancerState)"/> and returns the result. /// </summary> public bool TryGet(AnimationClip clip, out AnimancerState state) { if (clip == null) { state = null; return(false); } return(TryGet(Root.GetKey(clip), out state)); }
/// <summary> /// Passes the <see cref="IHasKey.Key"/> into <see cref="TryGet(object, out AnimancerState)"/> /// and returns the result. /// </summary> public bool TryGet(IHasKey hasKey, out AnimancerState state) { if (hasKey == null) { state = null; return(false); } return(TryGet(hasKey.Key, out state)); }
/************************************************************************************************************************/ /// <summary> /// Initialises this mixer with the specified number of ports which can be filled individually by <see cref="CreateState"/>. /// </summary> 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]; }
/************************************************************************************************************************/ /// <summary>[<see cref="ITransition"/>] /// Called by <see cref="AnimancerPlayable.Play(ITransition)"/> to set the <see cref="BaseState"/> /// and apply any other modifications to the `state`. /// </summary> /// <remarks> /// This method also clears the <see cref="State"/> if necessary, so it will re-cast the /// <see cref="BaseState"/> when it gets accessed again. /// </remarks> public virtual void Apply(AnimancerState state) { state.Events = _Events; BaseState = state; if (_State != state) { _State = null; } }
/************************************************************************************************************************/ /// <summary> /// Constructs a new <see cref="AnimationEventReceiver"/> and sets the <see cref="Source"/> and /// <see cref="Callback"/>. /// </summary> public AnimationEventReceiver(AnimancerState source, Action <AnimationEvent> callback) { _Source = source; _SourceID = source != null ? source.Layer.CommandCount : -1; Callback = callback; #if UNITY_EDITOR FunctionName = null; ValidateSourceHasCorrectEvent(); #endif }
/// <summary> /// Starts fading in the 'state' from the start over the course of the 'fadeDuration' while fading out all /// others in this layer. Returns the 'state'. /// <para></para> /// If the 'state' isn't currently at 0 <see cref="AnimancerState.Weight"/>, this method will actually fade it /// to 0 along with the others and create and return a new state with the same clip to fade to 1. This ensures /// that calling this method will always fade out from all current states and fade in from the start of the /// desired animation. States created for this purpose are cached so they can be reused in the future. /// <para></para> /// Calling this method repeatedly on subsequent frames will probably have undesirable effects; you most likely /// want to use <see cref="CrossFade"/> instead. /// <para></para> /// If this layer currently has 0 <see cref="Weight"/>, this method will instead start fading in the layer /// itself and simply <see cref="Play"/> the 'state'. /// <para></para> /// Animancer Lite only allows the default 'fadeDuration' (0.3 seconds) in a runtime build. /// </summary> /// <remarks> /// This can be useful when you want to repeat an action while the previous animation is still fading out. /// For example, if you play an 'Attack' animation, it ends and starts fading back to 'Idle', and while it is /// doing so you want to start another 'Attack'. The previous 'Attack' can't simply snap back to the start, so /// you can use this method to create a second 'Attack' state to fade in while the old one fades out. /// </remarks> public AnimancerState CrossFadeFromStart(AnimancerState state, float fadeDuration = AnimancerPlayable.DefaultFadeDuration) { if (Weight == 0) { return(Play(state)); } if (Weight != 0 && state.Weight != 0) { var clip = state.Clip; if (clip == null) { Debug.LogWarning("CrossFadeFromStart was called on a state which has no clip: " + state, state.MainObject); } else { var layerIndex = state.LayerIndex; // Get the default state registered with the clip. state = Root.GetOrCreateState(clip, clip, layerIndex); #if UNITY_EDITOR int depth = 1; #endif // If that state isn't at 0 weight, get or create another state registered using the previous state as a key. // Keep going through states in this manner until you find one at 0 weight. while (state.Weight != 0) { // Explicitly cast the state to an object to avoid the overload that warns about using a state as a key. state = Root.GetOrCreateState((object)state, clip, layerIndex); #if UNITY_EDITOR if (depth++ == maxCrossFadeFromStartDepth) { throw new ArgumentOutOfRangeException("depth", "CrossFadeFromStart has created " + maxCrossFadeFromStartDepth + " or more states for a single clip." + " This is most likely a result of calling the method repeatedly on consecutive frames." + " You probably just want to use CrossFade instead, but you can increase" + " AnimancerLayer.MaxCrossFadeFromStartDepth if necessary."); } #endif } } } // Reset the time in case it wasn't already reset when the weight was previously set to 0. state.Time = 0; // Now that we have a state with 0 weight, start fading it in. return(CrossFade(state, fadeDuration)); }
/// <summary> /// If the `state` is not currently at 0 <see cref="AnimancerNode.Weight"/>, this method finds a copy of it /// which is at 0 or creates a new one. /// </summary> /// <exception cref="InvalidOperationException">Thrown if the <see cref="AnimancerState.Clip"/> is null.</exception> /// <exception cref="ArgumentOutOfRangeException"> /// Thrown if more states have been created for this <see cref="AnimancerState.Clip"/> than the /// <see cref="maxStateDepth"/> allows. /// </exception> public AnimancerState GetOrCreateWeightlessState(AnimancerState state) { if (state.Weight != 0) { var clip = state.Clip; if (clip == null) { throw new InvalidOperationException( "GetOrCreateWeightlessState was called on a state which has no clip: " + state); // We could probably support any state type by giving them a Clone method, but that would take a // lot of work for something that might never get used. } else { // Get the default state registered with the clip. if (state.Key as Object != clip) { state = Root.States.GetOrCreate(clip, clip); } #if UNITY_EDITOR int depth = 0; #endif // If that state is not at 0 weight, get or create another state registered using the previous state as a key. // Keep going through states in this manner until you find one at 0 weight. while (state.Weight != 0) { // Explicitly cast the state to an object to avoid the overload that warns about using a state as a key. state = Root.States.GetOrCreate((object)state, clip); #if UNITY_EDITOR if (++depth == maxStateDepth) { throw new ArgumentOutOfRangeException("depth", "GetOrCreateWeightlessState has created " + maxStateDepth + " or more states for a single clip." + " This is most likely a result of calling the method repeatedly on consecutive frames." + " You probably just want to use FadeMode.FixedSpeed instead, but you can increase" + " AnimancerLayer.maxStateDepth if necessary."); } #endif } } } // Make sure it is on this layer and at time 0. AddChild(state); state.Time = 0; return(state); }
/************************************************************************************************************************/ /// <summary> /// Adds a new port and uses <see cref="AnimancerState.SetParent"/> to connect the `state` to it. /// </summary> public void AddChild(AnimancerState state) { if (state.Parent == this) { return; } var index = States.Count; States.Add(null); _Playable.SetInputCount(index + 1); state.SetParent(this, index); }
/// <summary> /// Gather the current details of the <see cref="AnimancerNode.Root"/> and register this /// <see cref="CustomFade"/> to be updated by it so that it can replace the regular fade behaviour. /// </summary> protected void Apply(AnimancerState state) { #if UNITY_ASSERTIONS Debug.Assert(state != null, "State is null."); Debug.Assert(state.IsValid, "State is not valid."); Debug.Assert(state.IsPlaying, "State is not playing."); Debug.Assert(state.LayerIndex >= 0, "State is not connected to a layer."); Debug.Assert(state.TargetWeight > 0, "State is not fading in."); var animancer = state.Root; Debug.Assert(animancer != null, $"{nameof(state)}.{nameof(state.Root)} is null."); #if UNITY_EDITOR if (WarningType.CustomFadeBounds.IsEnabled()) { if (CalculateWeight(0) != 0) { WarningType.CustomFadeBounds.Log("CalculateWeight(0) != 0.", animancer.Component); } if (CalculateWeight(1) != 1) { WarningType.CustomFadeBounds.Log("CalculateWeight(1) != 1.", animancer.Component); } } #endif #endif _FadeSpeed = state.FadeSpeed; if (_FadeSpeed == 0) { return; } _Time = 0; _TargetState = new StateWeight(state); _TargetLayer = state.Layer; _CommandCount = _TargetLayer.CommandCount; OtherStates.Clear(); for (int i = _TargetLayer.ChildCount - 1; i >= 0; i--) { var other = _TargetLayer.GetChild(i); other.FadeSpeed = 0; if (other != state && other.Weight != 0) { OtherStates.Add(new StateWeight(other)); } } state.Root.RequireUpdate(this); }
/// <summary> /// Starts fading in the `state` over the course of the `fadeDuration` while fading out all others in this /// layer. Returns the `state`. /// <para></para> /// If the `state` was already playing and fading in with less time remaining than the `fadeDuration`, this /// method will allow it to complete the existing fade rather than starting a slower one. /// <para></para> /// If the layer currently has 0 <see cref="AnimancerNode.Weight"/>, this method will fade in the layer itself /// and simply <see cref="AnimancerState.Play"/> the `state`. /// <para></para> /// Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in a runtime build. /// </summary> public AnimancerState Play(AnimancerState state, float fadeDuration, FadeMode mode = FadeMode.FixedSpeed) { Validate.Root(state, Root); if (fadeDuration <= 0 || // With no fade duration, Play immediately. (Index == 0 && Weight == 0)) // First animation on Layer 0 snap Weight to 1. { return(Play(state)); } bool isFixedDuration; EvaluateFadeMode(mode, ref state, ref fadeDuration, out isFixedDuration); StartFade(1, fadeDuration); if (Weight == 0) { return(Play(state)); } AddChild(state); CurrentState = state; // If the state is already playing or will finish fading in faster than this new fade, // continue the existing fade but still pretend it was restarted. if (state.IsPlaying && state.TargetWeight == 1 && (state.Weight == 1 || state.FadeSpeed * fadeDuration > Math.Abs(1 - state.Weight))) { OnStartFade(); } // Otherwise fade in the target state and fade out all others. else { state.IsPlaying = true; state.StartFade(1, fadeDuration); var count = States.Count; for (int i = 0; i < count; i++) { var otherState = States[i]; if (otherState != state) { otherState.StartFade(0, fadeDuration); } } } return(state); }
/************************************************************************************************************************/ /// <summary> /// Initialises the <see cref="Mixer"/> with two ports and connects two states to them for the specified clips /// at the specified thresholds (default 0 and 1). /// </summary> public void Initialise(AnimationClip clip0, AnimationClip clip1, float threshold0 = 0, float threshold1 = 1) { _Playable.SetInputCount(2); States = new AnimancerState[2]; new ClipState(this, 0, clip0); new ClipState(this, 1, clip1); SetThresholds(new float[] { threshold0, threshold1, }); }
/************************************************************************************************************************/ /// <summary> /// Adds a new port and uses <see cref="AnimancerState.SetParent"/> to connect the `state` to it. /// </summary> public void AddChild(AnimancerState state) { if (state.Parent == this) { return; } state.SetRoot(Root); var index = States.Count; States.Add(null);// OnAddChild will assign the state. _Playable.SetInputCount(index + 1); state.SetParent(this, index); }
/************************************************************************************************************************/ /// <summary>[Internal] /// Registers the `state` in this dictionary so the `key` can be used to get it later on using /// any of the lookup methods such as <see cref="this[object]"/>. /// </summary> /// <remarks>The `key` can be <c>null</c>.</remarks> internal void Register(object key, AnimancerState state) { if (key != null) { #if UNITY_ASSERTIONS if (state.Root != Root) { throw new ArgumentException( $"{nameof(StateDictionary)} cannot register a state with a different {nameof(Root)}: " + state); } #endif States.Add(key, state); } state._Key = key; }
/************************************************************************************************************************/ /// <summary> /// Called by <see cref="AnimancerPlayable.Play(ITransition)"/> to apply the <see cref="Speed"/> /// and <see cref="NormalizedStartTime"/>. /// </summary> public override void Apply(AnimancerState state) { base.Apply(state); if (!float.IsNaN(_Speed)) { state.Speed = _Speed; } if (!float.IsNaN(_NormalizedStartTime)) { state.NormalizedTime = _NormalizedStartTime; } else if (state.Weight == 0) { state.NormalizedTime = AnimancerEvent.Sequence.GetDefaultNormalizedStartTime(_Speed); } }
/************************************************************************************************************************/ /// <summary> /// Gathers the current details of the <see cref="AnimancerNode.Root"/> and register this /// <see cref="CustomFade"/> to be updated by it so that it can replace the regular fade behaviour. /// </summary> protected void Apply(AnimancerState state) { AnimancerUtilities.Assert(state.Parent != null, "Node is not connected to a layer."); Apply((AnimancerNode)state); var parent = state.Parent; for (int i = parent.ChildCount - 1; i >= 0; i--) { var other = parent.GetChild(i); if (other != state && other.FadeSpeed != 0) { other.FadeSpeed = 0; FadeOutNodes.Add(new NodeWeight(other)); } } }