Skip to content

Instantly share code, notes, and snippets.

@senox13
Last active April 20, 2023 20:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save senox13/8f29c9969e66620694ff2cc02b0b30a1 to your computer and use it in GitHub Desktop.
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
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
}
}
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