The first thing I do in any new game is get the player character moving and animating. In Unity there are three main options for animation:

  • Using the legacy animation component

    Simple and direct playable of individual animations, but precludes humanoid animation retargetting.

  • Using the animator component using animation controllers

    Fine explicit control of animations and transitions built using in editor visual state machine.

  • Using the animator component with a playble graph

    Allows you to build state machines programmically at run time, which can be changed on the fly.

For an ARPG style game like Seekers, the primary use case is very simple direct "play this clip". I don't particularly enjoy using the editor to build visual state machines, and needing a different number of clips per character would mean either many making state machines or one very large machine, however I may wish to use humanoid animation sets across multiple characters. So I decided to use a playable graph with the animator component to try to recreate the simplicity of use of the legacy animation component.

Initial Setup

The unity manual's playable examples page provides all the information to needed to set up a playables graph to blend between animation clips (scroll down to the "Blending an AnimationClip and AnimatorController" example). It's relatively easy to expand this example to support an arbitrary number of animation clips.

using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;

[RequireComponent(typeof(Animator))]
public class AnimationController : MonoBehaviour
{
	const string ANIMATION = "Animation";

	PlayableGraph _playableGraph;
	PlayableOutput _playableOutput;
	AnimationMixerPlayable _mixerPlayable;
	AnimationClipPlayable[] _clipPlayables;

	void OnDestroy()
	{
		_playableGraph.Destroy();
	}

	public void Init(AnimationClip[] clips)
	{
		_playableGraph = PlayableGraph.Create();
		_playableGraph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);
		_playableOutput = AnimationPlayableOutput.Create(_playableGraph, ANIMATION, GetComponent());

		_mixerPlayable = AnimationMixerPlayable.Create(_playableGraph, clips.Length);

		_playableOutput.SetSourcePlayable(_mixerPlayable);

		_clipPlayables = new AnimationClipPlayable[clips.Length];
		for (int i = 0, l = _clipPlayables.Length; i < l; i++)
		{
			_clipPlayables[i] = AnimationClipPlayable.Create(_playableGraph, clips[i]);
			_playableGraph.Connect(_clipPlayables[i], 0, _mixerPlayable, i);
		}

		_playableGraph.Play();
	}

	public bool IsComplete(int index)
	{
		return _clipPlayables[index].GetTime() > _clipPlayables[index].GetAnimationClip().length;
	}

	public float GetNormalizedTime(int index)
	{
		return (float)_clipPlayables[index].GetTime() / _clipPlayables[index].GetAnimationClip().length;
	}

	public void SetTime(int index, float time)
	{
		_clipPlayables[index].SetTime(time);
	}

	public void SetWeight(int index, float weight)
	{
		_mixerPlayable.SetInputWeight(index, weight);
	}

	public void SetSpeed(int index, float speed)
	{
		_clipPlayables[index].SetSpeed(speed);
	}

	public void SetDuration(int index, float duration)
	{
		SetSpeed(index, _clipPlayables[index].GetAnimationClip().length / duration);
	}
}

It's worth noting this controller doesn't internally ensure that total weight of all clips adds up to one, that's left to the controlling code. Here's what that looks like when hooked up to a test script that swaps between an clip idle and another one of our array of clips.

Single Animation Playback

The animations in this set all start and finish near the idle pose, so this looks pretty good without even without any transition blending!

Movement

So far so straight forward, so onto the fun bit, movement animation. As Seekers is intended to be multi-platform I'm going with the classic ARPG style where you can only run forwards and change your facing direction. It's quite difficult to control independent facing and movement directions with a gamepad or touch screen! This simplifies our problem to playing a forward moving animations such that it looks appropriate for the speed at which a character is moving.

To start with we're going to need to know what speed of movement our animations correspond to. If you were authoring your own animations you'd hopefully already know, but as I'm using pre-bought assets (alas I'm not an animator) I need to determine this.

I found the best way to do this is by eye, with a couple of test scripts, one which can adjustably scroll a material, and another to adjust the time scale of playback. Then simply drop the model into a test scene, get the desired clip playing, and adjust the scrolling material under the character until it looks about right.

Determining Clip Travel Speed

Whilst we're gathering meta-data about our animations let's figure out the times in the animation that each foot hits the ground, so we have the option of playing some sound effects. The easiest way to do this is probably to select the clip in the project view and use the animation preview section in the bottom of the inspector window, dragging the animation position until the foot strikes the floor and noting the normalised playback time.

As I want movement speed to be easily tweak-able, and potentially changeable, I'm going to need some method of adjusting clips to match the speed a character is actually moving at. Two approaches occur to me; dynamically changing the playback speed of the current clip or blending between two clips of different movement speeds.

Approach 1: Adjust Playback Speed

Now adjusting playback speed itself isn't complex, pretty much just calling something like SetSpeed(movementClipIndex, currentSpeed / movementClipTravelSpeed) on the animation controller code above and if you've only got one clip it's pretty much just picking the idle clip or the movement clip if you have a non-zero speed. The interesting bit is if you want to use multiple movement clips with different speeds, e.g. walk, run, sprint.

The approach I took was to add even more meta-data for the movement clips, defining the ranges at which a clip applies and then picking the clip where current speed fell within the defined range, or the closest otherwise. I also made it so it would prioritise the current clip if it was still in the current clips speed range, paired with overlapping speed ranges this prevents slight variations in input causing the movement animation from flicking back and forth.

Motion: Scaling Playback Speed

There's a few observations to be made on this approach, having tried both with a pair of clips for walk and run, as well as using a single run movement animation:

  • Walking clips between 0 and the clip's travel speed look pretty good.
  • Running clips at very low speeds look pretty bad, more like 'slow motion' than natural movement.
  • Running clips played at higher speeds look more and more comical as speed increases.
  • The transitions between different movement clips are jarring.

The last point is made worse by the fact that in this set of animations the walk animation is more upright than both the idle and run clip so the character stands up straighter briefly when accelerating, which looks very unnatural.

Approach 2: Blending Movement Clips

If you're only using a single movement clip, then this is pretty straight forward, the code would look something like this:

float blendWeight = Mathf.Min(1.0f, speed / movementClipTravelSpeed);
SetWeight(movementClipIndex, blendWeight);
SetWeight(idleClipIndex, 1 - blendWeight);

As with the previous method the interesting bit is when you have multiple movement clips. If the animations being used were made with blending in mind; the movement clips will have the same number of run cycles, the same footfall times and be the same duration. All three of these things need to be true for you to be able to simply blend between the different movement clips based on current movement speed. Alas this is not the case with the animation sets I'm using, so we're also going to need to change the playback speed so they last for the same duration, and offset the playback so the footfalls are approximately at the same time.

Having decided the desired playback duration and armed with the normalised footfall times meta-data we can use something like the following to set the necessary offsets and playback speeds.

[SerializedField] MovementBlendSettings _moveBlendSettings; // Contains desired clip length and desired left footfall time
MovementBlendValues[] _movementBlendValues; // Store for calculated offsets and playback speeds

public void CalculateBlendValues()
{
  // Calculate playback speeds and offsets for clip blending
  _movementBlendValues = new MovementBlendValues[_moveClips.Length;]
  for (int i = 0, l = _moveClips.Length; i < l; i++)
  {
    _movementBlendValues[i] = new MovementBlendValues();
    int cycleCount = _moveClips[i].LeftFootfallNormalizedTimes.Length;
    _movementBlendValues[i].PlaybackSpeed = _moveClips[i].Clip.length / (cycleCount * _moveBlendSettings.DesiredClipLength);
    _movementBlendValues[i].Offset = _moveClips[i].LeftFootfallNormalizedTimes[0] - _moveBlendSettings.NormalizedLeftFootfallTime;
    if (_movementBlendValues[i].Offset < 0)
    {
      _movementBlendValues[i].Offset = 1 - _movementBlendValues[i].Offset;
    }
  }
}

Having calculated the necessary offsets and playback speeds we can then blend between clips as in the simple psuedocode at the start of this section, so long as we pick the correct clips to blend between and we adjust the travel speed for the new playback speed.

In this approach we only need to set the playback speed of the playables on initialisation, however once in idle (i.e. not moving) the clip playables current time should be set back to their calculated offsets so that as when the character sets off it always starts from the 'start' of the animation clip. This is because even when the weight of a playable is set to 0 in a mixer, it's execution time will still increase as the mixer plays, on the plus side this keeps all the offset-ed movement clips in sync whilst moving, even if not all have a non-zero blend weight at any given time.

Motion: Blending Clips

Observations on this approach:

  • The character seamlessly transitions between different speeds.
  • When using on one running movement clip, setting off looks far better than adjusted play back speeds.
  • Only allows you to 'match' movement speeds up-to the travel speed (adjusted for playback speed) of your fastest travelling movement clip.
  • Running clips continue to look natural even when movement speed is far higher than the clips travel speed.
  • The method of changing the play back speeds is only really suitable for slightly different clip durations per run cycle, where the difference in travel speed is caused primarily by stride length.

Further Improvements

As I am only going to be using idle and a running clip for my main characters (due to the clips in the animation set I have), it makes sense for me to go with the second approach to movement as described above. However if I were to want the comical overspeed run, or were to animate a character with suitable walk animations, I would probably want to create a hybrid approach, which would could use either approach when moving between no movement and the slowest clip (combined with more classical transition style blend in), and when dealing with overspeed from the fastest clip, but retaining the blending for transitions between clips.

You can look at the source code here. This is very much only first pass at an character animation controller, there's plenty more I'm planning to add and polish up, but it's enough to start prototyping with, and looks pretty good with the animation set I'm using.

Something sorely lacking from this controller is transition blends which is pretty much a requirement for animation sets where animations don't start and end to the same idle pose or if you need to transition mid-playback to another animation. My plan is to incorporate this by separating the movement clips into a separate sub-mixer on the graph and then I'll be able to write a relatively simple transition blender which will only need to transition channels on the source mixer of the graph between 0 and 1, whilst allowing me to continue to blend movement clips to match the character's movement speed.

The next Seekers systems post will probably be about the ability system, that's the configuration driven timeline that allows me to specify a large variety of character and enemy abilities without having to write bespoke code each time. I expect that post to be more conceptual and I'll be posting even less code as it involves a lot of code!

I hope you found this post interesting, I took a little longer to put together than I expected but was nice opportunity to review some code from early in the project. I'm going to go back to focusing on creating the minimum viable version of Seekers for a while now!