04. Animating the Door
Using Unity 2021.3.33f1 and Visual Scripting 1.8.0. The project is using the 2D Core template.
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:
- A start position.
- An end position.
- 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 of0f
would give the start and1f
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:
- The animation should start from whatever position the door is currently at.
- 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.
- 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.
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.
- We reset the
_animationTimer
by setting it to zero. - The Transform’s current position is saved in
_animationStartPosition
. - The
_animationEndPosition
is set to either the initial position of the door or the initial position + the offset we have defined in the Inspector. - 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:
- It will start from the
Update()
method at line1
. - Then it will go to line
3
where it will call theUpdateAnimation()
method. - It will then jump to line
8
where it will checkif (_animationTimer >= animationDuration)
. - Since the above is
true
, it will move to line11
where it is told toreturn
. - Then, it will jump back up to line
4
and continue where it left off. Since it is the end ofUpdate()
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;
}
}