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 animated behavior.
Introduction¶
Suppose you want to create an animation that shows little people (paratroopers) drop from the top of the screen. They fall for a few moments, and then suddenly open their parachutes and float gently to the ground. On the ground, their parachutes disappear and they walk off the screen.
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 without a parachute.
- The “floating” state, where they fall slowly with a parachute.
- The “walking” state, where they are walk, either 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 paratroopers, we will use the following rules to decide when their state changes:
- 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();
p.randomRestart();
troopers.add(p);
++i;
}
}
void draw() {
background(255);
for(Paratrooper t : troopers) {
t.render();
t.update();
}
}
This is very similar to the code we wrote for the water particle system. The major difference will be the code we put in the
Paratrooper
class.
The Paratrooper Class¶
The Paratropper
class has this overall structure:
class Sprite {
float x;
float y;
float dx;
float dy;
void update() {
x += dx;
y += dy;
}
}
class Paratrooper extends Sprite {
// ... variables for this Paratrooper ...
void randomRestart() {
// ... 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:
drag
, which calculates the speed of afloating
paratrooper. A paratrooper floats downwards at speeddrag * dy
, and sodrag
should be between 0 and 1.chuteOpenHeight
, for the height along the y-axis for when a paratrooper’s chute opens.bodyColor
, for the color of the paratrooper’s body (which we’ll draw as a rectangle).state
, for the current state of the paratrooper:"falling"
,"floating"
, or"walking"
.
Now lets add those variables to the class, and initialize them in
randomRestart
:
class Paratrooper extends Sprite {
float drag;
float chuteOpenHeight;
color bodyColor;
String state; // "falling", "floating", or "walking"
void randomRestart() {
x = random(50, 450);
y = 0;
drag = random(0.02, 0.08);
chuteOpenHeight = random(0.25 * height, 0.75 * height);
dx = 0;
dy = 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"
, so that
paratroopers will begin falling from the top of the screen.
Now lets sketch the render()
function:
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 it.
Between pushMatrix()
and popMatrix()
we have a big if-else-if
structure that does different things depending upon the value of state
:
- When
state
is “falling”, we draw a colored rectangle. - When
state
is “floating”, we draw the colored rectangle plus a parachute on top. - When
state
is “walking”, we draw a colored rectangle.
Of course, you can make much more elaborate drawings than this if you have the time and interest.
Here’s a 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);
// body
fill(bodyColor);
noStroke();
rect(0, 0, 5, 10);
// parachute
fill(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()
because
update()
doesn’t (and shouldn’t!) change the screen coordinate system.
Let’s list the things we need to do for each state in update()
:
- When
state
is “falling”, we need update the sprite’s position with usualsuper.update()
(i.e. we call theupdate()
function inSprite``q). If ``y >= chuteOpenHeight
, then we will setstate
to “floating”. - When
state
is “floating”, we updatey
by addingdrag * dy
to it. If the paratrooper reaches the bottom of the screen (i.e.y >= height - 10
), then we do this:- Set
state
to “walking”. - Set
dx
to random value chosen from a small range. - Set
dy
to 0.
- Set
- When
state
is"walking"
, we callsuper.update()
, and then . check if the paratrooper reaches the edge of the screen (i.e. ifx < 0
orx > 500
). If it does, we do this:- Set
state
to"falling"
. - Call
randomRestart()
so that the object starts again from the top of of the screen.
- Set
Here’s a complete implementation of update()
:
void update() {
if (state.equals("falling")) {
super.update(); // call the update() function in Sprite
if (y >= chuteOpenHeight) {
state = "floating";
}
} else if (state.equals("floating")) {
x += dx;
y += drag * dy;
if (y >= height - 10) {
state = "walking";
dx = random(0.5, 2);
dy = 0;
if (random(0.0, 1.0) < 0.5) {
dx = -dx;
}
}
} else if (state.equals("walking")) {
super.update(); // call the update() function in Sprite
if (x < 0 || x > width) {
state = "falling";
randomRestart();
}
}
}
Notice this code:
if (random(0.0, 1.0) < 0.5) {
dx = -dx;
}
The expression random(0, 1.0)
returns a randomly chosen number in the
range 0 to 1, and so random(0, 1.0) < 0.5
is true 50% of the time. It is
like a coin-flip: half the time dx
will be negated, the other half 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
making state
a String
probably makes no noticeable difference.
However, for program with lots and lots of animated objects — such as a game
— it could be too slow.
To increase the speed of checking state
, there are a couple of common
alternative 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.
Programming Questions¶
Add wind to the simulation, so that the paratroopers move a little left/right as they fall.
Parachuting is a dangerous business, and so sometimes paratroopers die when they hit the ground. Add another state to the paratroopers called “dead” which causes them to appear dead at the bottom of the screen. You could render a dead paratrooper as a little tombstone, or a skull and cross bones.
Paratroopers shouldn’t always become “dead” when they hit the ground. Use the
random
function to make, say, 50% of the paratroopers die when they hit the ground.Add an airplane that continually flies across the top of the screen, dropping the paratroopers as it goes.
Source Code¶
class Sprite {
float x;
float y;
float dx;
float dy;
void update() {
x += dx;
y += dy;
}
}
class Paratrooper extends Sprite {
float drag;
float chuteOpenHeight;
color bodyColor;
String state; // "falling", "floating", or "walking"
void randomRestart() {
x = random(50, width - 50);
y = 0;
drag = random(0.02, 0.08);
chuteOpenHeight = random(0.25 * height, 0.75 * height);
dx = 0;
dy = 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);
// body
fill(bodyColor);
noStroke();
rect(0, 0, 5, 10);
// parachute
fill(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")) {
super.update(); // call the update() function in Sprite
if (y >= chuteOpenHeight) {
state = "floating";
}
} else if (state.equals("floating")) {
x += dx;
y += drag * dy;
if (y >= height - 10) {
state = "walking";
dx = random(0.5, 2);
dy = 0;
if (random(0.0, 1.0) < 0.5) {
dx = -dx;
}
}
} else if (state.equals("walking")) {
super.update(); // call the update() function in Sprite
if (x < 0 || x > width) {
state = "falling";
randomRestart();
}
}
}
} // class Paratrooper
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();
p.randomRestart();
troopers.add(p);
++i;
}
}
void draw() {
background(255);
for (Paratrooper p : troopers) {
p.render();
p.update();
}
}