// 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
/************************************************************************************************************************/
}
}