Easy parametric animations in #Unity3d with code and curves

iOS forces you to use pretty animations: if you don’t tell it otherwise, it will use “ease-in/ease-out” curves to make things feel … smoother, less jerky.

Unity is made for programmers; changes you make to position, size, rotation, etc all happen instantaneously. By default, they won’t even animate, will teleport instead.

So … moving from “prototype with no animation” to “prototype that won’t hurt people’s eyes, and I can share”, what are some tips and gotchas?

Mecanim

Personal opinion, but … most of us are using it to get cheap bipedal walking animations. Which is fine, but a very small percent of the many use-cases for “animation during a game”.

So, I’m ignoring it here.

Playing an animation correctly

First off: don’t use Animation.Play( “anim-name” ) – this method is great for prototyping, but it throws-away the timing info you need later

You can workaround this by putting source-code into every single one of your Animations. Yeah. That works, but .. it’s as safe and maintainable as it sounds. For very complex anims, you’ll have to do that anyway, and it’s a great solution – but for small games it’s overkill.

Always load the AnimationClip

Animation anim = myObject.GetComponentInChildren<Animation>();
if( anim == null )
	anim = myObject.AddComponent<Animation>();

AnimationClip clip = anim.GetClip( animationName );
anim.clip = clip;

Now you know how long the anim takes – it’s “clip.length”

Applying Animations to GameObjects

Design flaw in Unity that we’re stuck with:

Animations OVERWRITE the object state, instead of MODIFYING it

i.e. a jumping animation that moves up and down … will reset your character to the origin, jump them up and down, and then teleport your character back to where they were standing. World’s biggest WTF in engine-design.

Official workaround:

  1. Create an empty GameObject.
  2. Parent your object within the new one.
  3. Apply the animation to the original object (the child).
  4. Re-write all your game-input, controllers, AI, etc so that it moves the new, empty, PARENT object.

Parametric Animation

That’s not quite enough.

In a game, almost all animation is parametric. It is very rare to find something that SHOULDN’T be tweakable via simple config file, NOR influenced by the player’s or AI’s actions.

Unity doesn’t directly support parametric animation; but they provide API calls and features that make it very easy to add yourself. If you know what they are…

Example: Jump with variable length

First, split your animation into two parts:

  1. Parametric part (in this case: how much ground-distance does the jump cover?)
    • You implement this as an AnimationClip, following Unity’s standard process / official docs.
  2. Static part (in this case: how high they jump, the curve of up and down movement (should look like acceleration: moves quickly at first, slower as it nears the peak, then fast again as it falls))
    • You implement this in code, using a co-routine. See next paragraph..

First, you write it without a co-routine, to test it works:

public void PlayJump( GameObject myObject, Vector3 jumpDistance )
{
	myObject.transform.position = myObject.transform.position + jumpDistance;
}

Works, right? Great. But it teleports you from start to finish.

Then you convert it to a co-routine, causing Unity to smoothly animate it. W00t! Here, you use Unity’s provided method for smoothly moving something over time: Vector3.Lerp() (google it if you don’t know what a “lerp” is, it’s used in almost every game ever written).

public void PlayJump( GameObject myObject, Vector3 jumpDistance )
{
	StartCoroutine( CoMove( myObject, myObject.transform.position + jumpDistance );
}

private IEnumerator CoMove( GameObject myObject, Vector3 finalPosition )
{
	float elapsedTime = 0, transitionTime = 1f /* spend 1 seconds moving */;
	Vector3 savedOriginalPosition = myObject.transform.position;

	while( elapsedTime < transitionTime )
	{
	myObject.transform.position = Vector3.Lerp( savedOriginalPosition, finalPosition, (elapsedTime/transitionTime) );
	elapsedTime += Time.deltaTime;
	yield return new WaitForEndOfFrame();
	}
}

Your jump should now combine a Unity Animation and a parametric / controllable distance.

But … you want the jump distance and jump animation to be synchronized. For that, you need the clip-length we grabbed earlier:


public void AnimateAndPlayJump( GameObject myObject, Vector3 jumpDistance )
{
	Animation anim = myObject.GetComponentInChildren<Animation>();
	if( anim == null )
		anim = myObject.AddComponent<Animation>();
	
	AnimationClip clip = anim.GetClip( animationName );
	anim.clip = clip;
	
	// now you know how long the anim takes - it's "clip.length"
	
	anim.Play(); // uses the new .clip
	
	PlayJump( myObject, jumpDistance, clip.length );
}

public void PlayJump( GameObject myObject, Vector3 jumpDistance, float duration )
{
	StartCoroutine( CoMove( myObject, myObject.transform.position + jumpDistance, duration );
}

private IEnumerator CoMove( GameObject myObject, Vector3 finalPosition, float duration )
{
	float elapsedTime = 0;
	Vector3 savedOriginalPosition = myObject.transform.position;

	while( elapsedTime < duration )
	{
	myObject.transform.position = Vector3.Lerp( savedOriginalPosition, finalPosition, (elapsedTime/transitionTime) );
	elapsedTime += Time.deltaTime;
	yield return new WaitForEndOfFrame();
	}
}

Now it’s perfect.

Then you try it in-game, and it’s about 10% slower than you intended. Easy, speed up the ani– oh, wait. Unity’s animation editor DOES NOT ALLOW you to speed up / slow down your animationclips. Ouch. Burn!

Controlling animation speed

You can change the speed of an animation itself, but it’s a bit non-obvious how to do it. You unintuitively iterate through the AnimationStates, and set their speed to the INVERSE of the ratio of speeds. Wat? Read the code:

public void AnimateAndPlayJump( GameObject myObject, Vector3 jumpDistance, float animationDuration = 0f )
{
	Animation anim = myObject.GetComponentInChildren<Animation>();
	if( anim == null )
		anim = myObject.AddComponent<Animation>();
	
	AnimationClip clip = anim.GetClip( animationName );
	anim.clip = clip;
	
	if( animationDuration > 0f )
	{
		/** Modify the animation-clip, for this animation only */
		foreach( AnimationState s in anim )
		{
					s.speed = transitTimeSeconds/overrideAnimationDuration;
		}
	}
	// now you know how long the anim takes - it's animationDuration
	
	anim.Play(); // uses the new .clip
	
	PlayJump( myObject, jumpDistance, animationDuration > 0f ? animationDuration : clip.length );
}

Very nice. Fully controllable animations, integrated with gameplay. Aw, yiss!

Editable curves for your Parametric animations

There’s a problem with the above: you make a beautiful jump, which arcs nicely through space, using Unity’s Curves in the animation window, but … the distance itself is constantly increasing. You can’t have the character squat, get ready, then jump – they will skid along the ground while squatting!

There are many obvious code solutions; they’re mostly wrong.

Instead, there’s a 1-line change + new variable (of type AnimationCurve). IF you do it this way, Unity gives you a powerful graphical editor for the PARAMETRIC part of your animation – IMHO it’s easier to use than the Animation window, which is a little tragic, but useful.

private IEnumerator CoMove( GameObject myObject, Vector3 finalPosition, float duration, AnimationCurve timingCurve = null )
{
	float elapsedTime = 0;
	Vector3 savedOriginalPosition = myObject.transform.position;

	if( timingCurve == null )
	{
		timingCurve = AnimationCurve.Linear( 0, 0, 1f, 1f ); // identical to a Lerp
	}

	while( elapsedTime < duration )
	{
	myObject.transform.position = Vector3.Lerp( savedOriginalPosition, finalPosition, timingCurve.Evaluate( (elapsedTime/transitionTime) ) );
	elapsedTime += Time.deltaTime;
	yield return new WaitForEndOfFrame();
	}
}

This. Is. Awesome.

Animation and Physics

Read Sampsa’s Excellent short post on this topic, with do’s and don’ts, and explanations.

Also: run the webplayer demo he included there! It’s a wonderfully quick way to see for yourself what these (undocumented) core features in Unity actually mean in practice.

Net changes: enable Kinematic while Animating

Depending on your situation, you may want to use some or all of Sampsa’s improvements. But at the very least, you’ll want to switch to Kinematic during your animation (so that Unity doesn’t do something dumb, like override the animation with a falling-due-to-gravity).

NB: if you want the animation to play *while falling*, you’ll have some fun times. You can’t re-use the official Parenting trick, because you’re only allowed one set of physics / RigidBodies within a single hierarchy of parent/child/grandchild/etc. Bummer.

public void PlayJump( GameObject myObject, Vector3 jumpDistance, float duration )
{
	/** c.f. Sampsa's article, but generally you want to do this while you're animating */
	myObject.GetComponent<PlayingCard>().isKinematic = true;

	StartCoroutine( CoMove( myObject, myObject.transform.position + jumpDistance, duration );
}

private IEnumerator CoMove( GameObject myObject, Vector3 finalPosition, float duration, AnimationCurve timingCurve = null )
{
	... // rest of method here

	/** re-enable physics so it can be knocked around, stood on, etc */
	myObject.GetComponent<PlayingCard>().isKinematic = false;
}

…but you PROBABLY want to enable “animatePhysics” too (c.f. Sampsa’s demo).

Leave a Reply

Your email address will not be published. Required fields are marked *