Skip to content

Instantly share code, notes, and snippets.

@adammyhre
Created July 14, 2024 02:01
Show Gist options
  • Save adammyhre/71327c363bdb4f282b94f95dfe39ff13 to your computer and use it in GitHub Desktop.
Save adammyhre/71327c363bdb4f282b94f95dfe39ff13 to your computer and use it in GitHub Desktop.
AnimationSystem with Audio and Awaitables
using System;
using UnityEngine.Animations;
using UnityEngine.Playables;
using UnityEngine;
[Serializable]
public class AnimationConfig {
public Animator animator;
public AnimationClip idleClip;
public AnimationClip walkClip;
}
[Serializable]
public class AudioConfig {
public AudioSource audioSource;
public AudioClip[] footstepSounds;
}
public class AnimationSystem {
PlayableGraph playableGraph;
AnimationMixerPlayable topLevelMixer;
AnimationMixerPlayable locomotionMixer;
AnimationClipPlayable oneShotPlayable;
ScriptPlayable<FootstepPlayableBehaviour> footstepPlayable;
Awaitable blendInOut;
public AnimationSystem(AnimationConfig animationConfig, AudioConfig audioConfig) {
playableGraph = PlayableGraph.Create("AnimationSystem");
SetupAnimations(animationConfig);
SetupFootsteps(audioConfig);
playableGraph.Play();
}
void SetupAnimations(AnimationConfig animationConfig) {
var playableOutput = AnimationPlayableOutput.Create(playableGraph, "Animation", animationConfig.animator);
topLevelMixer = AnimationMixerPlayable.Create(playableGraph, 2);
playableOutput.SetSourcePlayable(topLevelMixer);
locomotionMixer = AnimationMixerPlayable.Create(playableGraph, 2);
topLevelMixer.ConnectInput(0, locomotionMixer, 0);
playableGraph.GetRootPlayable(0).SetInputWeight(0, 1f);
var idlePlayable = AnimationClipPlayable.Create(playableGraph, animationConfig.idleClip);
var walkPlayable = AnimationClipPlayable.Create(playableGraph, animationConfig.walkClip);
idlePlayable.GetAnimationClip().wrapMode = WrapMode.Loop;
walkPlayable.GetAnimationClip().wrapMode = WrapMode.Loop;
locomotionMixer.ConnectInput(0, idlePlayable, 0);
locomotionMixer.ConnectInput(1, walkPlayable, 0);
}
void SetupFootsteps(AudioConfig audioConfig) {
var scriptOutput = ScriptPlayableOutput.Create(playableGraph, "Footsteps");
footstepPlayable = ScriptPlayable<FootstepPlayableBehaviour>.Create(playableGraph);
var behaviour = footstepPlayable.GetBehaviour();
behaviour.footstepSounds = audioConfig.footstepSounds;
behaviour.audioSource = new ExposedReference<AudioSource> { defaultValue = audioConfig.audioSource };
scriptOutput.SetSourcePlayable(footstepPlayable);
}
public void OnFootstep() => footstepPlayable.GetBehaviour().PlayFootstepSound();
public void UpdateLocomotion(Vector3 velocity, float maxSpeed) {
var weight = Mathf.InverseLerp(0f, maxSpeed, velocity.magnitude);
locomotionMixer.SetInputWeight(0, 1f - weight);
locomotionMixer.SetInputWeight(1, weight);
}
public void PlayOneShot(AnimationClip oneShotClip) {
if (oneShotPlayable.IsValid() && oneShotPlayable.GetAnimationClip() == oneShotClip) return;
InterruptOneShot();
oneShotPlayable = AnimationClipPlayable.Create(playableGraph, oneShotClip);
topLevelMixer.ConnectInput(1, oneShotPlayable, 0);
topLevelMixer.SetInputWeight(1, 1f);
var blendDuration = Mathf.Clamp(oneShotClip.length * 0.1f, 0.1f, oneShotClip.length * 0.5f);
blendInOut = BlendInOut(blendDuration, oneShotClip.length - blendDuration);
}
async Awaitable BlendInOut(float blendDuration, float delay) {
await Blend(blendDuration, blendTime => {
var weight = Mathf.Lerp(1f, 0f, blendTime);
topLevelMixer.SetInputWeight(0, weight);
topLevelMixer.SetInputWeight(1, 1f - weight);
});
await Awaitable.WaitForSecondsAsync(delay);
await Blend(blendDuration, blendTime => {
var weight = Mathf.Lerp(0f, 1f, blendTime);
topLevelMixer.SetInputWeight(0, weight);
topLevelMixer.SetInputWeight(1, 1f - weight);
});
DisconnectOneShot();
}
static async Awaitable Blend(float duration, Action<float> blendCallback) {
var blendTime = 0f;
while (blendTime < 1f) {
blendTime += Time.deltaTime / duration;
blendCallback(blendTime);
await Awaitable.EndOfFrameAsync();
}
blendCallback(1f);
}
void InterruptOneShot() {
blendInOut?.Cancel();
topLevelMixer.SetInputWeight(0, 1f);
topLevelMixer.SetInputWeight(1, 0f);
if (oneShotPlayable.IsValid()) {
DisconnectOneShot();
}
}
void DisconnectOneShot() {
topLevelMixer.DisconnectInput(1);
playableGraph.DestroyPlayable(oneShotPlayable);
}
public void Destroy() {
if (playableGraph.IsValid()) {
playableGraph.Destroy();
}
}
}
using UnityEngine;
using UnityEngine.Audio;
using UnityEngine.Playables;
public class FootstepPlayableBehaviour : PlayableBehaviour {
public AudioClip[] footstepSounds;
public ExposedReference<AudioSource> audioSource;
AudioSource resolvedAudioSource;
PlayableGraph playableGraph;
AudioMixerPlayable audioMixerPlayable;
AudioClipPlayable currentClipPlayable;
public override void OnGraphStart(Playable playable) {
resolvedAudioSource = audioSource.Resolve(playable.GetGraph().GetResolver());
playableGraph = playable.GetGraph();
audioMixerPlayable = AudioMixerPlayable.Create(playableGraph, 1);
var audioOutput = AudioPlayableOutput.Create(playableGraph, "FootstepAudio", resolvedAudioSource);
audioOutput.SetSourcePlayable(audioMixerPlayable);
}
public void PlayFootstepSound() {
if (footstepSounds == null || footstepSounds.Length == 0 || resolvedAudioSource == null) return;
var selectedClip = footstepSounds[Random.Range(0, footstepSounds.Length)];
var newClipPlayable = AudioClipPlayable.Create(playableGraph, selectedClip, false);
if (currentClipPlayable.IsValid()) {
audioMixerPlayable.DisconnectInput(0);
playableGraph.DestroyPlayable(currentClipPlayable);
}
playableGraph.Connect(newClipPlayable, 0, audioMixerPlayable, 0);
audioMixerPlayable.SetInputWeight(0, 1f);
currentClipPlayable = newClipPlayable;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment