04. Animating the Door

Using Unity 2021.3.33f1 and Visual Scripting 1.8.0. The project is using the 2D Core template.

Demo

See the visual script version here for comparison: 04. Animating the Door

We will use code to smoothly animate the door as it opens and closes.

Animation

Creating the illusion of movement on a screen is a matter of changing the position of objects in small steps every frame. We already see this in action with the player, where we change the position over time.

Making animations that takes a fixed amount of time from start to finish, however, requires us to do things differently.

Before adding animation to the door, let us just try to move an object from one position to another. You can add this script to a new game object. Remember to name the file AnimationExample just like the class name.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AnimationExample : MonoBehaviour
{
    public Vector3 startPosition = new Vector3(-5f, 0f, 0f);
    public Vector3 endPosition = new Vector3(5f, 0f, 0f);
    public float animationDuration = 10f;

    private float _animationTimer;

    private void Update()
    {
        // Increment the timer, this will make it act like a stopwatch counting seconds
        _animationTimer += Time.deltaTime;
        
        // Calculate the animation progress as a percentage
        float percent = _animationTimer / animationDuration;
        
        // Use the percentage to get a position in between startPosition and endPosition
        Vector3 newPosition = Vector3.Lerp(startPosition, endPosition, percent);
        
        transform.position = newPosition;
    }
}

This script will move the object from startPosition to endPosition given some animationDuration in seconds. You can try to adjust these numbers in the Inspector.

We use the _animationTimer float variable to keep track of time. It starts at zero, and is then incremented every frame in the Update() method.

In Unity, there is a special method called Vector3.Lerp(). Lerp is a compressed word (a portmanteau) of linear interpolation. In mathematics, it is about finding some value in between two extremes and this is exactly what we want! To find a position in between two other positions. This method takes three parameters:

  1. A start position.
  2. An end position.
  3. Some float value that tells how close we want to be each extreme. A value of 0.5f would give us the point halfway between the two. A value of 0f would give the start and 1f would give us the end.

To convert the time elapsed (_animationTimer) to a value between 0f and 1f we can divide it by the full duration (animationDuration):

1
float percent = _animationTimer / animationDuration;

Now we have everything we need to use the Vector3.Lerp() method and we put in the expected parameters and assign the newPosition to the Transform’s position:

1
2
3
Vector3 newPosition = Vector3.Lerp(startPosition, endPosition, percent);

transform.position = newPosition;

Adding animation variables to the door

Adding animation to the door is almost identical to what we just did!

However, there are a few things we would like to change:

  1. The animation should start from whatever position the door is currently at.
  2. The animation timer should restart whenever the door switches from being closed to open and vice versa. In other words, when the button is pressed.
  3. The animation should start and end more smoothly - what is known as easing in and out in animation. To start and end slowly and move faster in the middle.

Start by adding these public and private variables at the top of the class

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Door : MonoBehaviour
{
    // Public animation variables
    public float animationDuration = 1f;
    public AnimationCurve animationCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);
    
    // Private animation variables
    private float _animationTimer;
    private Vector3 _animationStartPosition;
    private Vector3 _animationEndPosition;
    
    // The rest of the class...
}

There is a new type of variable here: AnimationCurve. We can use this curve to modify how the movement changes over time. Clicking the curve in the Inspector lets us inspect it more closely. The curve should be read from left to right as a change over time. It begins more flat and then gradually becomes more steep towards to middle after which it again flattens out towards the top. This curve will create a nice and smooth slow start and end.

button

button

You can click the end points of the curve to reveal some handles, that allows you to tweak the curvature of the curve. I encourage you to play around with different curves to get a feel for how it affects the animation. The left point should stay and (0, 0) and the right point should stay at (1, 1).

Restarting the timer

We want the timer to be complete initially, to avoid any animation playing. We can do this in the Start() method:

1
2
3
4
5
private void Start()
{
    // Make the animation be "complete" initially
    _animationTimer = animationDuration;
}

We will then change our own HandleButtonChange() method, to better accommodate our needs.

  1. We reset the _animationTimer by setting it to zero.
  2. The Transform’s current position is saved in _animationStartPosition.
  3. The _animationEndPosition is set to either the initial position of the door or the initial position + the offset we have defined in the Inspector.
  4. We no longer change the Transform’s position here, since we want that to happen as an animation.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void HandleButtonChange(bool switchedOn)
{
    _animationTimer = 0f;
    _animationStartPosition = transform.position;
    
    if (_isOpen)
    {
        _animationEndPosition = _startPosition;
        _isOpen = false;
    }
    else
    {
        _animationEndPosition = _startPosition + offset;
        _isOpen = true;
    }
}

Creating the animation

We will create a new method called UpdateAnimation() that takes care of moving the door. This is done to help organise the code and make it easier to understand.

This method will be called from Unity’s Update() method every frame.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void Update()
{
    UpdateAnimation();
}

private void UpdateAnimation()
{
    if (_animationTimer >= animationDuration)
    {
        // This will end the method early, and not do the rest
        return;
    }
    
    _animationTimer += Time.deltaTime;

    float percent = _animationTimer / animationDuration;
    float curveValue = animationCurve.Evaluate(percent);

    Vector3 newPosition = Vector3.LerpUnclamped(_animationStartPosition, _animationEndPosition, curveValue);
    transform.position = newPosition;
}

In the beginning of the UpdateAnimation() method, we do something a little bit cryptic. We return; when the _animationTimer is greater than the animationDuration.

1
2
3
4
if (_animationTimer >= animationDuration)
{
    return;
}

When the computer runs the return; statement it exits out of the method. This means it will stop execution and go back to the Update() method and continue from there.

If we assume the _animationTimer is greater than animationDuration, the computer will process our script as follows:

  1. It will start from the Update() method at line 1.
  2. Then it will go to line 3 where it will call the UpdateAnimation() method.
  3. It will then jump to line 8 where it will check if (_animationTimer >= animationDuration).
  4. Since the above is true, it will move to line 11 where it is told to return.
  5. Then, it will jump back up to line 4 and continue where it left off. Since it is the end of Update() it will stop.

The complete script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Door : MonoBehaviour
{
    public Button button;
    public Vector3 offset;
    
    // Public animation variables
    public float animationDuration = 1f;
    public AnimationCurve animationCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);

    private Vector3 _startPosition;
    private bool _isOpen; // default value is false

    // Private animation variables
    private float _animationTimer;
    private Vector3 _animationStartPosition;
    private Vector3 _animationEndPosition;

    private void Start()
    {
        button.onButtonChange += HandleButtonChange;
        _startPosition = transform.position;
        
        // Make the animation be "complete" initially
        _animationTimer = animationDuration;
    }

    private void OnDestroy()
    {
        button.onButtonChange -= HandleButtonChange;
    }

    private void Update()
    {
        UpdateAnimation();
    }

    private void HandleButtonChange(bool switchedOn)
    {
        _animationTimer = 0f;
        _animationStartPosition = transform.position;
        
        if (_isOpen)
        {
            _animationEndPosition = _startPosition;
            _isOpen = false;
        }
        else
        {
            _animationEndPosition = _startPosition + offset;
            _isOpen = true;
        }
    }

    private void UpdateAnimation()
    {
        if (_animationTimer >= animationDuration)
        {
            // This will end the method early, and not do the rest
            return;
        }
        
        _animationTimer += Time.deltaTime;

        float percent = _animationTimer / animationDuration;
        float curveValue = animationCurve.Evaluate(percent);

        Vector3 newPosition = Vector3.LerpUnclamped(_animationStartPosition, _animationEndPosition, curveValue);
        transform.position = newPosition;
    }
}