Animation
The lab handout uses Super Mario Bros as assets to demonstrate certain Unity features and functionalities. You are free to follow along and submit it for checkoff, OR you can also create an entirely new project to demonstrate the requested feature(s).
The requirement(s) for the lab checkoff can be found here.
You can continue from where you left off in the previous Lab. Note that you need to finish the previous lab before starting on this one. In this lab we will upgrade our game by adding animation, sound effect, camera movement, and obstacles (leveraging on Unity's Physics2D engine) in the game.
Mario's Animation
Mario’s animation can be broken down into five main states:
- Idle state, when he’s not moving at all
- Running state, when he’s moving left or right
- Skidding state, when he switches direction while running and brake too hard
- Jumping state, when he’s off the ground
- Death state, when he hits the enemy
The Mario sprite given in the starter asset already contain the corresponding sprite that’s suitable for each state.
To begin animating a GameObject, we need these things:
- An
Animator
element attached to the GameObject, - An Animator
Controller
(need to create it in the Project Window underAssets
), - and several Animation Clips to be managed by the controller.
Animation Controller
Open the Animation Window (Window >> Animation >> Animation), then click on Mario. You will be then prompted to create an Animator for Mario, along with an animation clip. When you click create
, both are created by default. You can then begin recording Mario's changes on each frame/time on the dopesheet. First, create the folders: Assets/Animation/Mario
to contain all your Mario animation. Then, here's how to create a running Mario animation:
Animation Clips
Now create three more animation clips for idle, skidding, and jumping:
Each GameObject that you want to animate should have one Animator (just one). Each Animator is responsible over several animation clips that you can create. Always create new animation clip from the Dopesheet when focusing on current GameObject with Animator attached. If you create it straight from the project inspector, then it won't be automatically associated with the animation controller.
Animator State Machine
If you press Play
right now, you should see that your Mario immediately goes to play mario-run
animation clip. We do not want that. We want to have the following animation depending on Mario's state:
- If Mario's moving (have velocity), then we play
mario-run
clip on a loop - If Mario's off the ground, then we play
mario-jump
clip - If we change Mario's running direction from right to left, we want it to play
mario-skid
clip - Otherwise, Mario stays at
mario-idle
clip
To enable correct transition conditions, we need to create parameters
. These parameters will be used to trigger transition between each animation clip (motion). Create these three parameters on Animator Window:
onGround
of type boolxSpeed
of type floatonSkid
of type trigger (a boolean parameter that is reset by the controller when consumed by a transition)
Then add the following inside PlayerMovement.cs
:
// for animation
public Animator marioAnimator;
void Start(){
// ...
// update animator state
marioAnimator.SetBool("onGround", onGroundState);
}
void Update()
{
if (Input.GetKeyDown("a") && faceRightState)
{
faceRightState = false;
marioSprite.flipX = true;
if (marioBody.velocity.x > 0.1f)
marioAnimator.SetTrigger("onSkid");
}
if (Input.GetKeyDown("d") && !faceRightState)
{
faceRightState = true;
marioSprite.flipX = false;
if (marioBody.velocity.x < -0.1f)
marioAnimator.SetTrigger("onSkid");
}
marioAnimator.SetFloat("xSpeed", Mathf.Abs(marioBody.velocity.x));
}
void OnCollisionEnter2D(Collision2D col)
{
if (col.gameObject.CompareTag("Ground") && !onGroundState)
{
onGroundState = true;
// update animator state
marioAnimator.SetBool("onGround", onGroundState);
}
}
void FixedUpdate(){
// ...
if (Input.GetKeyDown("space") && onGroundState)
{
marioBody.AddForce(Vector2.up * upSpeed, ForceMode2D.Impulse);
onGroundState = false;
// update animator state
marioAnimator.SetBool("onGround", onGroundState);
}
}
Transition Time
Let's gradually test it by setting Mario's running animation first. Pay attention on when we untick exit time and setting the transition duration to 0:
Transition duration: The duration of the transition itself, in normalized time or seconds depending on the Fixed Duration mode, relative to the current state’s duration. This is visualized in the transition graph as the portion between the two blue markers.
If Has Exit Time is checked, this value represents the exact time at which the transition can take effect. This is represented in normalized time (for example, an exit time of 0.75 means that on the first frame where 75% of the animation has played, the Exit Time condition is true). On the next frame, the condition is false.
Exit time
Now complete the rest of the state animation state machine. It will definitely take a bit of time to setup the right exit time. We want most transition to happen immediately, but the transition between skidding state and running state should have some exit time. What we want is for the entire skidding state to complete (all frames played) before transitioning to the running state. The transition itself takes no time.
Read more documentation on transition properties here.
Here's a sped up recording to help you out. Pause it at certain key frames if needed. The key is to always check your output frequently.
Animation Event
We can create animation events on animation clips, of which we can subscribe a callback from a script attached to the GameObject where that animator is added to, as long as the signature matches (void
return type, and accepting either of the parameters: Float
, Int
, String
, or Object
).
As stated in the documentation, make sure that any GameObject which uses this animation in its animator has a corresponding script attached that contains a function with a matching event name. If you wish to call other functions in other script, you need to create a custom animation event tool script. You will learn more about this in Week 3.
For instance, let's say we want to play a sound effect whenever Mario jumps. First, create the following global variable and function in PlayerMovement.cs
:
// for audio
public AudioSource marioAudio;
void PlayJumpSound()
{
// play jump sound
marioAudio.PlayOneShot(marioAudio.clip);
}
Then:
- Create AudioSource component at Mario GameObject, and load the
smb_jump_small
AudioClip. Ensure that you disable Play on Awake property. - Then link this AudioSource component to
marioAudio
on the script from the inspector - Open
mario_jump
animation clip, and create an event at timestamp0:00
as shown in the recording below - Ensure that
mario-jump
Animation clip Loop Time property is unticked
You should hear the jumping sound effect exactly ONCE each time Mario jumps.
Death Animation
Now add death animation and sound effects. This is slightly more complicated because we want Mario to:
- Show the death sprite when colliding with Goomba
- Apply impulse force upwards
- Play death sound effect (
smb_death.mp3
), download here - Then show Game Over scene
- Restart everything when restart button is pressed
Here's the overview of the end product:
First, create a mario_die
animation with 4 samples, simply changing the sprite.
- Add
gameRestart
Trigger parameter to Mario's animator - Remove "Has Exit Time", we want Mario to go back to
idle
state immediately when the game is restarted - Add transition between
mario_die
tomario_idle
- and add the
gameRestart
condition to this newly created transition

Also, make sure to turn off Loop Time
in mario_die
animation clip. This is because we don't want the clip to loop and just play it once.

Then head to PlayerMovement.cs
and edit the OnTriggerEnter2D
and ResetGame
, while adding these two functions:
public AudioClip marioDeath;
public float deathImpulse = 15;
// state
[System.NonSerialized]
public bool alive = true;
void PlayDeathImpulse()
{
marioBody.AddForce(Vector2.up * deathImpulse, ForceMode2D.Impulse);
}
void GameOverScene()
{
// stop time
Time.timeScale = 0.0f;
// set gameover scene
gameManager.GameOver(); // replace this with whichever way you triggered the game over screen for Checkoff 1
}
void OnTriggerEnter2D(Collider2D other)
{
if (other.gameObject.CompareTag("Enemy") && alive)
{
Debug.Log("Collided with goomba!");
// play death animation
marioAnimator.Play("mario-die");
marioAudio.PlayOneShot(marioDeath);
alive = false;
}
}
public void ResetGame()
{
// reset position
marioBody.transform.position = new Vector3(-5.33f, -4.69f, 0.0f);
// ... other instruction
// reset animation
marioAnimator.SetTrigger("gameRestart");
alive = true;
}
The idea is not to immediately stop time when Mario collides with Goomba but to play the animation first for about 1 second before stopping time, to give enough time for the Physics engine to simulate the effect of deathImpulse
. We also have the state alive
to prevent collision with Goomba to be re-triggered. Then create two events in mario_die
animation, one to trigger PlayDeathImpulse
and the other to trigger GameOverScene
. Hook it up to the respective functions in PlayerMovement.cs
. Also, do not forget to link up the AudioClip
(MarioDeath
) in the Inspector:
Also notice how although alive
is a public state, we do not see it serialized in the inspector due to [System.NonSerialized]
attribute.
Disable Control when not alive
The final thing that you need to do is to disable Mario's movement when he is dead. Modify PlayerMovement.cs
FixedUpdate
:
void FixedUpdate()
{
if (alive)
{
float moveHorizontal = Input.GetAxisRaw("Horizontal");
// other code
}
}
Our game starts to become a little messier. We have states everywhere: player's status (alive or dead), score, game state (stopped or restarted), etc. We should have sort of GameManager
that's supposed to manage the game but many other scripts that sort of manages itself (like PlayerMovement.cs
). We will refactor our game to have a better architecture next week.
Fix gameRestart Bug
When the restart button is pressed while Mario is NOT in mario-die
state in the animator, we will inadvertently set gameRestart
trigger in the Animator, disallowing mario-die
clip to play the next time he collides with Goomba. What we want is to consume gameRestart
trigger in mario-idle
just in case a player restarts the game while Mario isn't dead.
- Create a transition from mario-idle to mario-idle
- Remove HasExitTime
- Add transition condition as
gameRestart
The following clip demonstrates both the bug and the fix: