Last active
April 20, 2023 20:29
-
-
Save senox13/8f29c9969e66620694ff2cc02b0b30a1 to your computer and use it in GitHub Desktop.
Quick demo of a proxy class based workaround for the lack of support in Unity for playables with generic parameters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections.Generic; | |
using UnityEngine; | |
using UnityEngine.Animations; | |
using UnityEngine.Playables; | |
[RequireComponent(typeof(Animator))] | |
public sealed class StateMixerPlayableDemo: MonoBehaviour, IAnimationClipSource{ | |
/* | |
* Fields | |
*/ | |
//Serialized fields | |
[SerializeField] | |
[Tooltip("This clip is expected to be set to loop. It will be played upon scene start and every time the below clip completes")] | |
private AnimationClip loopedClip = null; | |
[SerializeField] | |
[Tooltip("This clip is expected to not be set to loop. It will be played, once, every time space is pressed")] | |
private AnimationClip oneTimeClip = null; | |
//Unserialized fields | |
private PlayableGraph graph; | |
private StateMixerPlayable.Proxy<State> mixer; | |
/* | |
* IAnimationClipSource implementation | |
*/ | |
//For in-editor convenience | |
public void GetAnimationClips(List<AnimationClip> results){ | |
if(loopedClip != null) | |
results.Add(loopedClip); | |
if(oneTimeClip != null) | |
results.Add(oneTimeClip); | |
} | |
/* | |
* MonoBehaviour message methods | |
*/ | |
private void Awake(){ | |
//Create graph | |
graph = PlayableGraph.Create("Custom playable test"); | |
graph.SetTimeUpdateMode(DirectorUpdateMode.GameTime); | |
//Create ScriptPlayable for state mixer and | |
ScriptPlayable<StateMixerPlayable> mixerPlayable = ScriptPlayable<StateMixerPlayable>.Create(graph); | |
//Initialize proxy for playable behaviour with our State enum as the generic parameter | |
mixer = mixerPlayable.GetBehaviour().Init<State>(graph, mixerPlayable); | |
//Add our states and their associated clips | |
mixer.AddState(State.LOOPING, loopedClip); | |
mixer.AddState(State.SINGLE, oneTimeClip); | |
//Add a transition between clips | |
mixer.AddStateTransition(State.SINGLE, (int)State.LOOPING); | |
//Some callbacks, just for demonstration | |
mixer.AddOnBegin(State.LOOPING, () => Debug.Log("Entering UPDOWN")); | |
mixer.AddOnBegin(State.SINGLE, () => Debug.Log("Entering LEFTRIGHT")); | |
mixer.AddOnComplete(State.LOOPING, () => Debug.Log("Exiting UPDOWN")); | |
mixer.AddOnComplete(State.SINGLE, () => Debug.Log("Exiting LEFTRIGHT")); | |
//Build graph from previously registered states | |
mixer.Build(); | |
//Set an intiial state | |
mixer.SetState(State.LOOPING); | |
//Create and bind output, then play graph | |
AnimationPlayableOutput animOutput = AnimationPlayableOutput.Create(graph, "Player", GetComponent<Animator>()); | |
animOutput.SetSourcePlayable(mixerPlayable, 0); | |
graph.Play(); | |
} | |
private void Update(){ | |
if(Input.GetKeyDown(KeyCode.Space)){ | |
if(mixer.ActiveState == State.LOOPING){ | |
mixer.SetState(State.SINGLE); | |
} | |
} | |
} | |
private void OnDestroy(){ | |
if(graph.IsValid()){ | |
graph.Destroy(); | |
} | |
} | |
/* | |
* Nested types | |
*/ | |
private enum State{ | |
LOOPING, | |
SINGLE | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using UnityEngine; | |
using UnityEngine.Animations; | |
using UnityEngine.Playables; | |
public sealed class StateMixerPlayable : PlayableBehaviour{ | |
/* | |
* Fields | |
*/ | |
private ProxyBase proxy; | |
/* | |
* Public methods | |
*/ | |
public Proxy<E> Init<E>(PlayableGraph graphIn, Playable ownerIn){ | |
if(proxy != null) | |
throw new InvalidOperationException("Cannot init StateMixerPlayable more than once"); | |
Proxy<E> genericProxy = new Proxy<E>(graphIn, ownerIn); | |
proxy = genericProxy; | |
return genericProxy; | |
} | |
/* | |
* PlayableBehaviour override methods | |
*/ | |
public override void PrepareFrame(Playable playable, FrameData info){ | |
if(proxy == null) | |
throw new InvalidOperationException("StateMixerPlayable used before calling Init"); | |
proxy.ProcessFrame(); | |
} | |
/* | |
* Nested types | |
*/ | |
public abstract class ProxyBase{ | |
public abstract void ProcessFrame(); | |
} | |
public sealed class Proxy<E> : ProxyBase{ | |
/* | |
* Fields | |
*/ | |
private bool hasBeenBuilt; | |
private PlayableGraph graph; | |
private Playable owner; | |
private AnimationMixerPlayable mixer = AnimationMixerPlayable.Null; | |
private readonly List<E> states = new List<E>(); | |
private readonly Dictionary<E, E> fallbackStates = new Dictionary<E, E>(); | |
private readonly Dictionary<E, AnimationClipPlayable> clips = new Dictionary<E, AnimationClipPlayable>(); | |
private readonly Dictionary<E, E> stateTransitions = new Dictionary<E, E>(); | |
private readonly Dictionary<E, float> weightOverrides = new Dictionary<E, float>(); | |
private readonly Dictionary<E, Action> onBeginCallbacks = new Dictionary<E, Action>(); | |
private readonly Dictionary<E, Action> onCompleteCallbacks = new Dictionary<E, Action>(); | |
/* | |
* Properties | |
*/ | |
public E ActiveState {get;private set;} | |
/* | |
* Constructor | |
*/ | |
internal Proxy(PlayableGraph graphIn, Playable ownerIn){ | |
graph = graphIn; | |
owner = ownerIn; | |
} | |
/* | |
* Private methods | |
*/ | |
private int GetIndex(E state){ | |
return states.IndexOf(state); | |
} | |
private AnimationClipPlayable GetPlayable(E state){ | |
return clips[state]; | |
} | |
/* | |
* Public methods | |
*/ | |
public void AddState(E newState, AnimationClip stateClip, float speed = 1f){ | |
if(states.Contains(newState)) | |
throw new InvalidOperationException("Tried to add existing state to StateMixerPlayable"); | |
if(fallbackStates.ContainsKey(newState)){ | |
fallbackStates.Remove(newState); | |
} | |
states.Add(newState); | |
AnimationClipPlayable newClip = AnimationClipPlayable.Create(graph, stateClip); | |
if(!stateClip.isLooping){ | |
newClip.SetDuration(stateClip.length); | |
} | |
newClip.SetSpeed(speed); | |
newClip.Pause(); | |
clips.Add(newState, newClip); | |
} | |
public void AddStateTransition(E from, E to){ | |
stateTransitions.Add(from, to); | |
} | |
public void AddFallback(E state, E fallback){ | |
fallbackStates.Add(state, fallback); | |
} | |
public void AddOnBegin(E state, Action callback){ | |
onBeginCallbacks[state] = callback; | |
} | |
public void AddOnComplete(E state, Action callback){ | |
onCompleteCallbacks[state] = callback; | |
} | |
public void SetState(E newState){ | |
if(fallbackStates.TryGetValue(newState, out E fallback)){ | |
newState = fallback; | |
} | |
//Update weight of old state | |
mixer.SetInputWeight(GetIndex(ActiveState), 0f); | |
//Pause and reset old state's clip | |
AnimationClipPlayable oldClip = GetPlayable(ActiveState); | |
oldClip.Pause(); | |
oldClip.SetTime(0); | |
oldClip.SetTime(0); | |
oldClip.SetDone(false); | |
//Update state and weights and play new state's clip | |
ActiveState = newState; | |
float newWeight = 1f; | |
if(weightOverrides.TryGetValue(ActiveState, out float overrideWeight)){ | |
newWeight = overrideWeight; | |
} | |
mixer.SetInputWeight(GetIndex(ActiveState), newWeight); | |
GetPlayable(ActiveState).Play(); | |
if(onBeginCallbacks.TryGetValue(ActiveState, out Action callback)){ | |
callback.Invoke(); | |
} | |
} | |
public void OverrideWeight(E state, float weight){ | |
weightOverrides[state] = weight; | |
} | |
public void Build(){ | |
if(hasBeenBuilt) | |
throw new InvalidOperationException("Cannot build StateMixerPlayable more than once"); | |
hasBeenBuilt = true; | |
owner.SetInputCount(1); | |
mixer = AnimationMixerPlayable.Create(graph, states.Count); | |
graph.Connect(mixer, 0, owner, 0); | |
owner.SetInputWeight(0, 1f); | |
for (int i = 0; i < states.Count; i++){ | |
mixer.ConnectInput(i, clips[states[i]], 0); | |
} | |
} | |
/* | |
* Proxy override methods | |
*/ | |
public override void ProcessFrame(){ | |
if(!hasBeenBuilt) | |
throw new InvalidOperationException("StateMixerPlayable used before calling Build"); | |
//Check for state transition | |
if(GetPlayable(ActiveState).IsDone() && stateTransitions.TryGetValue(ActiveState, out E nextState)){ | |
if(onCompleteCallbacks.TryGetValue(ActiveState, out Action callback)){ | |
callback.Invoke(); | |
} | |
SetState(nextState); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment