// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik // using System; using System.Collections; using System.Collections.Generic; using System.Text; using UnityEngine; namespace Animancer { /// A dictionary of s mapped to their . /// /// Documentation: /// /// States /// /// https://kybernetik.com.au/animancer/api/Animancer/AnimancerStateDictionary public class AnimancerStateDictionary : IAnimationClipCollection, IEnumerable { /************************************************************************************************************************/ /// The at the root of the graph. private readonly AnimancerGraph Graph; /************************************************************************************************************************/ /// mapped to . private readonly Dictionary States = new(); /************************************************************************************************************************/ /// [Internal] Creates a new . internal AnimancerStateDictionary(AnimancerGraph graph) => Graph = graph; /************************************************************************************************************************/ /// The number of states that have been registered with a . public int Count => States.Count; /************************************************************************************************************************/ /// public void AppendDescriptionOrOrphans( StringBuilder text, string separator = "\n") { string stateSeparator = null; foreach (var state in States.Values) { if (state.Parent != null) continue; if (stateSeparator is null) { text.Append(separator) .Append("Orphan States:"); separator += Strings.Indent; stateSeparator = separator + Strings.Indent; } text.Append(separator); state.AppendDescription(text, stateSeparator); } } /************************************************************************************************************************/ #region Create /************************************************************************************************************************/ /// Creates and returns a new to play the `clip`. /// /// To create a state on a specific layer, use animancer.Layers[x].CreateState(clip) instead. /// /// is used to determine the . /// public ClipState Create(AnimationClip clip) => Create(Graph.GetKey(clip), clip); /// /// Creates and returns a new to play the `clip` and registers it with the `key`. /// /// /// To create a state on a specific layer, use animancer.Layers[x].CreateState(key, clip) instead. /// public ClipState Create(object key, AnimationClip clip) { var state = new ClipState(clip); state.SetGraph(Graph); state._Key = key; Register(state); return state; } /************************************************************************************************************************/ /// Calls for each of the specified clips. public void CreateIfNew(AnimationClip clip0, AnimationClip clip1) { GetOrCreate(clip0); GetOrCreate(clip1); } /// Calls for each of the specified clips. public void CreateIfNew(AnimationClip clip0, AnimationClip clip1, AnimationClip clip2) { GetOrCreate(clip0); GetOrCreate(clip1); GetOrCreate(clip2); } /// Calls for each of the specified clips. public void CreateIfNew(AnimationClip clip0, AnimationClip clip1, AnimationClip clip2, AnimationClip clip3) { GetOrCreate(clip0); GetOrCreate(clip1); GetOrCreate(clip2); GetOrCreate(clip3); } /// Calls for each of the specified `clips`. public void CreateIfNew(params AnimationClip[] clips) { if (clips == null) return; var count = clips.Length; for (int i = 0; i < count; i++) { var clip = clips[i]; if (clip != null) GetOrCreate(clip); } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Access /************************************************************************************************************************/ /// /// The on layer 0. /// /// Specifically, this is the state that was most recently started using any of the Play methods on that layer. /// States controlled individually via methods in the itself will not register in /// this property. /// public AnimancerState Current => Graph.Layers[0].CurrentState; /************************************************************************************************************************/ /// Calls then returns the state registered with that key. /// The key is null. /// No state is registered with the key. public AnimancerState this[AnimationClip clip] => States[Graph.GetKey(clip)]; /// Returns the state registered with the . /// The `key` is null. /// No state is registered with the `key`. public AnimancerState this[IHasKey hasKey] => States[hasKey.Key]; /// Returns the state registered with the `key`. /// The `key` is null. /// No state is registered with the `key`. public AnimancerState this[object key] => States[key]; /************************************************************************************************************************/ /// /// Calls then passes the key to /// and returns the result. /// public bool TryGet(AnimationClip clip, out AnimancerState state) { if (clip == null) { state = null; return false; } return TryGet(Graph.GetKey(clip), out state); } /// /// Passes the into /// and returns the result. /// public bool TryGet(IHasKey hasKey, out AnimancerState state) { if (hasKey == null) { state = null; return false; } return TryGet(hasKey.Key, out state); } /// /// If a `state` is registered with the `key`, this method outputs it and returns true. Otherwise the /// `state` is set to null and this method returns false. /// public bool TryGet(object key, out AnimancerState state) { if (key == null) { state = null; return false; } return States.TryGetValue(key, out state); } /************************************************************************************************************************/ /// /// Calls and returns the state registered with that key /// or creates one if it doesn't exist. /// /// /// If the state already exists but has the wrong , the `allowSetClip` /// parameter determines what will happen. False causes it to throw an while /// true allows it to change the . Note that the change is somewhat costly to /// performance so use with caution. /// /// public AnimancerState GetOrCreate(AnimationClip clip, bool allowSetClip = false) => GetOrCreate(Graph.GetKey(clip), clip, allowSetClip); /// /// Returns the state registered with the `transition`s if there is one. /// Otherwise this method uses to create a new one /// and registers it with that key before returning it. /// public AnimancerState GetOrCreate(ITransition transition) { var key = transition.Key; if (!TryGet(key, out var state)) { state = transition.CreateState(); state._Key = key; state.SetGraph(Graph); } return state; } /// /// Returns the state which registered with the `key` or creates one if it doesn't exist. /// /// If the state already exists but has the wrong , the `allowSetClip` /// parameter determines what will happen. False causes it to throw an /// while true allows it to change the . /// Note that the change is somewhat costly to performance so use with caution. /// /// /// See also: public AnimancerState GetOrCreate(object key, AnimationClip clip, bool allowSetClip = false) { if (TryGet(key, out var state)) { // If a state exists with the 'key' but has the wrong clip, either change it or complain. if (!ReferenceEquals(state.Clip, clip)) { if (allowSetClip) { state.Clip = clip; } else { throw new ArgumentException(GetClipMismatchError(key, state.Clip, clip)); } } } else { state = Create(key, clip); } return state; } /************************************************************************************************************************/ /// Returns an error message explaining that a state already exists with the specified `key`. public static string GetClipMismatchError(object key, AnimationClip oldClip, AnimationClip newClip) => $"A state already exists using the specified '{nameof(key)}', but has a different {nameof(AnimationClip)}:" + $"\n• Key: {key}" + $"\n• Old Clip: {oldClip}" + $"\n• New Clip: {newClip}"; /************************************************************************************************************************/ /// [Internal] /// Registers the `state` in this dictionary so the can be used to get it /// later on using any of the lookup methods such as or /// . /// /// Does nothing if the is null. internal void Register(AnimancerState state) { var key = state._Key; if (key != null) { #if UNITY_ASSERTIONS if (state.Graph != Graph) throw new ArgumentException( $"{nameof(AnimancerStateDictionary)} cannot register a state with a different {nameof(Graph)}: " + state); #endif States.Add(key, state); } } /// [Internal] Removes the `state` from this dictionary (the opposite of ). internal void Unregister(AnimancerState state) { var key = state._Key; if (key != null) States.Remove(key); } /************************************************************************************************************************/ #region Enumeration /************************************************************************************************************************/ // IEnumerable for 'foreach' statements. /************************************************************************************************************************/ /// Returns an enumerator that will iterate through all registered states. public Dictionary.ValueCollection.Enumerator GetEnumerator() => States.Values.GetEnumerator(); /// IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /************************************************************************************************************************/ /// [] /// Adds all the animations of states with a to the `clips`. /// public void GatherAnimationClips(ICollection clips) { foreach (var state in States.Values) clips.GatherFromSource(state); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Destroy /************************************************************************************************************************/ /// /// Calls on the state associated with the `clip` (if any). /// Returns true if the state existed. /// public bool Destroy(AnimationClip clip) { if (clip == null) return false; return Destroy(Graph.GetKey(clip)); } /// /// Calls on the state associated with the /// (if any). Returns true if the state existed. /// public bool Destroy(IHasKey hasKey) { if (hasKey == null) return false; return Destroy(hasKey.Key); } /// /// Calls on the state associated with the `key` (if any). /// Returns true if the state existed. /// public bool Destroy(object key) { if (!TryGet(key, out var state)) return false; state.Destroy(); return true; } /************************************************************************************************************************/ /// Calls on each of the `clips`. public void DestroyAll(IList clips) { if (clips == null) return; for (int i = clips.Count - 1; i >= 0; i--) Destroy(clips[i]); } /// Calls on each of the `clips`. public void DestroyAll(IEnumerable clips) { if (clips == null) return; foreach (var clip in clips) Destroy(clip); } /************************************************************************************************************************/ /// /// Calls on all states gathered by /// . /// public void DestroyAll(IAnimationClipSource source) { if (source == null) return; var clips = ListPool.Acquire(); source.GetAnimationClips(clips); DestroyAll(clips); ListPool.Release(clips); } /// /// Calls on all states gathered by /// . /// public void DestroyAll(IAnimationClipCollection source) { if (source == null) return; var clips = SetPool.Acquire(); source.GatherAnimationClips(clips); DestroyAll(clips); SetPool.Release(clips); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Key Error Methods #if UNITY_EDITOR /************************************************************************************************************************/ // These are overloads of other methods that take a System.Object key to ensure the user doesn't try to use an // AnimancerState as a key, since the whole point of a key is to identify a state in the first place. /************************************************************************************************************************/ /// [Warning] /// You should not use an as a key. /// The whole point of a key is to identify a state in the first place. /// [Obsolete("You should not use an AnimancerState as a key. The whole point of a key is to identify a state in the first place.", true)] public AnimancerState this[AnimancerState key] => key; /// [Warning] /// You should not use an as a key. /// The whole point of a key is to identify a state in the first place. /// [Obsolete("You should not use an AnimancerState as a key. The whole point of a key is to identify a state in the first place.", true)] public bool TryGet(AnimancerState key, out AnimancerState state) { state = key; return true; } /// [Warning] /// You should not use an as a key. /// The whole point of a key is to identify a state in the first place. /// [Obsolete("You should not use an AnimancerState as a key. The whole point of a key is to identify a state in the first place.", true)] public AnimancerState GetOrCreate(AnimancerState key, AnimationClip clip) => key; /// [Warning] /// You should not use an as a key. /// Just call . /// [Obsolete("You should not use an AnimancerState as a key. Just call AnimancerState.Destroy.", true)] public bool Destroy(AnimancerState key) { key.Destroy(); return true; } /************************************************************************************************************************/ #endif #endregion /************************************************************************************************************************/ } }