More Complex Animations with State Variables¶
In these notes you will learn how to:
- Use a state variable to make an object do different things.
- Use if-else-if statements in render/update functions to create complex behavior.
Introduction¶
Suppose you want to create an animation that shows little people (paratroopers) drop from the top of the screen for a few moments, and then, near the middle, suddenly open their parachutes and float gently to the ground. On the ground, their parachutes disappear and theu walk left or right along the bottom of the screen until they disappear through one of the edges.
What’s interesting about this example is that the paratroopers have three distinct states that we will need to keep track of:
- The “falling” state, where they fall quickly downwards without a parachute.
- The “floating” state, where they fall slowly downwards with a parachute.
- The “walking” state, where they are walking, either towards the left or the right, along the bottom of the screen.
A paratroopers appearance and behavior depends on its state. We’ll use if-
statements inside the render()
and update()
functions to decide what
to do based upon the current state.
We will also need to manage state transitions. For our paratrooper, we have the following state transitions:
- falling \(\rightarrow\) floating: When the paratrooper is falling, it should change to floating after it has fallen some randomly- chosen distance
- floating \(\rightarrow\) walking: When the paratrooper is floating, it should change to walking when it reaches the ground.
- walking \(\rightarrow\) falling: When the paratrooper is walking, it should change to falling after it has reached either the left edge or right edge of the screen.
When we change the state of the paratrooper, we may also perform other actions. For instance, when the state changes from “walking” to “falling”, we will move the paratrooper to the top of the screen and drop them again as if they were brand new.
The Main Program¶
Before we dive into the details of the paratrooper, lets look at the main program that controls them:
ArrayList<Paratrooper> troopers; // initially null
void setup() {
size(500, 500);
// make a new, initially empty, ArrayList of paratroopers
troopers = new ArrayList<Paratrooper>();
// add some paratroopers
int i = 0;
while (i < 25) {
Paratrooper p = new Paratrooper();
troopers.add(p);
++i;
}
}
void draw() {
background(255);
for(Paratrooper t : troopers) {
t.render();
t.update();
}
}
We define a variable of type ArrayList<Paratrooper>
, and then in setup
assign a new (initially empty) ArrayList<Paratrooper>
object to it. Then
we add some paratroopers to trooper
using a while loop. Inside draw
,
after making the screen white, we then call render
and update
on each
Paratrooper
object in trooper
.
The Paratrooper Class¶
The Paratropper
class has this overall structure:
class Paratrooper {
// ... variables for this Paratrooper ...
Paratrooper() { // constructor
// ... initializes variables ...
}
void render() {
// ... drawing code ...
}
void update() {
// ... updates the variables of the paratrooper object ...
}
} // class Paratrooper
Lets start by defining the variables. We want:
x
andy
, to store the position of the paratrooper.fallingSpeed
, to store the speed at which a “falling” paratrooper falls.walkingSpeed
, to store the speed at which a “walking” paratrooper walks.drag
, which will be used to calculate the speed of afloating
paratrooper. A paratrooper will float downwards at speeddrag * fallingSpeed
, and sodrag
should be some value between 0 and 1.chuteOpenHeight
, to store the height along the y-axis for when a paratrooper’s chute should open up.bodyColor
, to store the color of the paratrooper’s body (which we’ll draw as a rectangle).state
, to store the current state of the paratrooper. The possible values ofstate
are the strings"falling"
,"floating"
, and"walking"
.
Of course, you could add other variables if we wanted to make a more detailed paratrooper.
Now lets add those variables to the class, and initialize them in the constructor:
class Paratrooper {
float x, y;
float fallingSpeed;
float walkingSpeed;
float drag;
float chuteOpenHeight;
color bodyColor;
String state; // "falling", "floating", or "walking"
Paratrooper() {
x = random(50, 450);
y = 0;
drag = random(0.02, 0.08);
chuteOpenHeight = random(50, 350);
fallingSpeed = random(3.0, 6.0);
bodyColor = color(random(256), random(256), random(256));
state = "falling";
}
void render() {
// ...
}
void update() {
// ...
}
} // class Paratrooper
Notice that we initialize state
to the value "falling"
, which means
that paratroopers will start out falling from the top of the screen.
Now lets create the render()
function. It will have this general
structure:
void render() {
pushMatrix();
if (state.equals("falling")) {
// ...
} else if (state.equals("floating")) {
// ...
} else if (state.equals("walking")) {
// ...
}
popMatrix();
}
As usual, we call pushMatrix()
at the start of render()
to save the
current coordinate system. The call popMatrix()
at the end restores this
saved coordinate system.
Between pushMatrix()
and popMatrix()
we have a big if-else-if
structure that does different things depending upon the value of state
.
Lets list the different things we want to draw:
- When
state
is “falling”, we’ll draw just a colored rectangle. - When
state
is “floating”, we’ll draw the colored rectangle plus a parachute on top. - When
state
is “walking”, we’ll draw just a colored rectangle.
Of course, you can make much more elaborate drawings than this if you have the time and interest.
Here’s the full implementation of render()
:
void render() {
pushMatrix();
if (state.equals("falling")) {
translate(x, y);
fill(bodyColor);
noStroke();
rect(0, 0, 5, 10);
} else if (state.equals("floating")) {
translate(x, y);
fill(bodyColor);
noStroke();
rect(0, 0, 5, 10); // body
fill(200, 200, 200);
stroke(0);
arc(2, -5, 20, 20, PI, 2*PI);
line(0, 0, -8, -5);
line(5, 0, 11, -5);
} else if (state.equals("walking")) {
translate(x, y);
fill(bodyColor);
noStroke();
rect(0, 0, 5, 10);
}
popMatrix();
}
The only tricky thing here is how arc
is used to draw a half-circle that
looks like a parachute.
The update()
function has a similar overall structure:
void update() {
if (state.equals("falling")) {
// ...
} else if (state.equals("floating")) {
// ...
} else if (state.equals("walking")) {
// ...
}
}
There’s no need to call pushMatrix()
and popMatrix()
for update()
because it will not (should not!) be changing the screen coordinate system.
Let’s list the things we need to do for each state in update()
:
- When
state
is “falling”, we need to incrementy
byfallingSpeed
. Also, we need to check ify >= chuteOpenHeight
. If it is, then we will setstate
to “floating”. - When
state
is “floating”, we incrementy
bydrag * fallingSpeed
. Also, we check to see ify >= 490
, i.e. if the paratrooper has reached the bottom of the screen. Ify >= 490
is true, then we do this:- Set
state
to “walking”. - Assign a value to
walkingSpeed
chosen at random from a small range of numbers.
- Set
- When
state
is walking, we incrementx
(noty
!) bywalkingSpeed
. Also, we check to see if the paratrooper has reached an edge of the screen, i.e. ifx < 0
orx > 500
. If so, then we do this:- Set
state
to “falling”. - Set
x
to be a random number in the range 50 to 450. - Set
y
to be 0 (so it will start falling from the top of the screen).
- Set
Here’s the entire implementation of update()
:
void update() {
if (state.equals("falling")) {
y += fallingSpeed;
if (y >= chuteOpenHeight) {
state = "floating";
}
} else if (state.equals("floating")) {
y += drag * fallingSpeed;
if (y >= 490) {
state = "walking";
walkingSpeed = random(0.5, 2);
if (random(0.0, 1.0) < 0.5) {
walkingSpeed = -walkingSpeed;
}
}
} else if (state.equals("walking")) {
x += walkingSpeed;
if (x < 0 || x > width) {
state = "falling";
x = random(50, 450);
y = 0;
}
}
}
Take a look at this code:
if (random(0.0, 1.0) < 0.5) {
walkingSpeed = -walkingSpeed;
}
The expression random(0, 1.0)
returns a randomly chosen number in the
range 0 to 1, and to the express random(0, 1.0) < 0.5
is true
approximately 50% of the time. It is like a coin-flip: half the time
walkingSpeed
will negated, while the rest of the time it won’t.
A Note on Efficiency¶
We’ve implemented state
as a String
variable so that it is easy to
keep track of the current state. However, testing if two strings are equal
usually requires more work than checking if, say, two int
values are
equal. If you only have a few animated objects on the screen at once, then
using String
for state
probably doesn’t make any noticeable
difference. However, for program with lots and lots of animated objects, it
could be a problem.
To increase the speed of checking state
, there are a couple of common
approaches:
Make
state
of typeint
, and use numbers to represent states. For example, “falling” could be 1, “floating” 2, and “walking” 3. Programmers often define variables to remember what number each state is, e.g.:final int FALLING = 1; final int FLOATING = 2; final int WALKING = 3;
The keyword
final
means that these variables cannot be changed later in the program — they are constants.We can then write code like this:
void update() { if (state == FALLING) { // ... } else if (state == FLOATING) { // ... } else if (state == WALKING) { // ... } }
==
is usually more efficient thenequals
, so this could be somewhat faster code.Use an enum. An
enum
is essentially a built-in version of the previous idea that is safe, readable, and efficient.
Questions¶
- Add another state to the paratroopers called “waiting” which causes them to wait for a short, random amount of time after they land on the ground. After they finish waiting, they should start walking.
- Instead of having the paratroopers just appear falling on the screen, add an airplane that continually flies across the top of the screen, dropping the paratroopers as it goes.
Source Code¶
ArrayList<Paratrooper> troopers;
void setup() {
size(500, 500);
troopers = new ArrayList<Paratrooper>();
int i = 0;
while (i < 25) {
troopers.add(new Paratrooper());
++i;
}
}
void draw() {
background(255);
for(Paratrooper t : troopers) {
t.render();
t.update();
}
}
class Paratrooper {
float x, y;
float fallingSpeed;
float walkingSpeed;
float drag;
float chuteOpenHeight;
color bodyColor;
String state; // falling", "floating", or "walking"
Paratrooper() {
x = random(50, 450);
y = 0;
drag = random(0.02, 0.08);
chuteOpenHeight = random(50, 350);
fallingSpeed = random(3.0, 6.0);
bodyColor = color(random(256), random(256), random(256));
state = "falling";
}
void render() {
pushMatrix();
if (state.equals("falling")) {
translate(x, y);
fill(bodyColor);
noStroke();
rect(0, 0, 5, 10);
} else if (state.equals("floating")) {
translate(x, y);
fill(bodyColor);
noStroke();
rect(0, 0, 5, 10); // body
fill(200, 200, 200);
stroke(0);
arc(2, -5, 20, 20, PI, 2*PI);
line(0, 0, -8, -5);
line(5, 0, 11, -5);
} else if (state.equals("walking")) {
translate(x, y);
fill(bodyColor);
noStroke();
rect(0, 0, 5, 10);
}
popMatrix();
}
void update() {
if (state.equals("falling")) {
y += fallingSpeed;
if (y >= chuteOpenHeight) {
state = "floating";
}
} else if (state.equals("floating")) {
y += drag * fallingSpeed;
if (y >= 490) {
state = "walking";
walkingSpeed = random(0.5, 2);
if (random(0.0, 1.0) < 0.5) {
walkingSpeed = -walkingSpeed;
}
}
} else if (state.equals("walking")) {
x += walkingSpeed;
if (x < 0 || x > width) {
state = "falling";
randomRestart();
}
}
}
} // class Paratrooper