Simple Multi-image Animation¶
In these notes you will learn:
- How to use a state variable to control an animated object with multiple images.
- How to create a simple timer class.
- How to use a timer to control the speed of an animation.
Introduction¶
The use of state variables in the previous notes is a very useful technique that has many different applications. Here, we will see how to use a state variable to control the display of an animated object that has multiple images.
We will use the following 3 images of a famous plumber:
marioWalk1.png
,
marioWalk2.png
,
marioWalk3.png
. When shown quickly in
sequence, they will make a walking version of Mario.
Warning
The way we will do the animation in what follows is not necessarily the way a professional programmer would do it. It is meant only as an example of how to solve a programming problem. Our main goal is to make the code simple to write and as easy as possible to understand. We will many details, and so the approach taken here is not necessarily the way you should do animation in a more practical program.
The Main Program¶
First, let’s create the main program:
PImage img0;
PImage img1;
PImage img2;
Mario mario;
void setup() {
size(500, 500);
img0 = loadImage("marioWalk1.png");
img1 = loadImage("marioWalk2.png");
img2 = loadImage("marioWalk3.png");
mario = new Mario();
mario.x = 0;
mario.y = 50;
mario.dx = 0.5;
mario.dy = 0;
}
void draw() {
background(255);
mario.render();
mario.update();
}
There are a couple of things to notice:
The three images,
img0
,img1
, andimg2
, are loaded insetup
. We name the first variableimg0
because it turns that later on, when we have more images, starting at 0 is more convenient then starting at 1.The images only get loaded once each because
setup
is only called once. So no matter how many Marios we create, we only have to load their images once. That’s important because image loading is relatively slow (think how long it takes for video games to start up — they’re loading images, sounds, and video), and so you should never re-load an image that’s already in memory.Even though we have not yet created the
Mario
class, we used it in our main program as if it existed. This is a useful because it lets us specify how we want it to work without yet worrying about how it is implemented.
The Mario Class¶
Now lets create the Mario
class. In addition to extending the Sprite
class, we’ll use an int
variable called state
to keep track of which
image to display:
class Sprite {
float x;
float y;
float dx;
float dy;
void update() {
x += dx;
y += dy;
}
}
class Mario extends Sprite {
int state;
void render() {
pushMatrix();
// ...
popMatrix();
}
// ... update() is inherited from Sprite ...
}
The update()
function turns out to be the one inherited from Sprite
,
so we don’t need to write a new update()
in the Mario
class.
The render()
function will use an if-else-if statement to check the
state
and display the corresponding image:
void render() {
pushMatrix();
scale(3); // make Mario 3 times as big (just for fun)
if (state == 0) {
image(img0, x, y);
state = 1;
} else if (state == 1) {
image(img1, x, y);
state = 2;
} else if (state == 2) {
image(img2, x, y);
state = 0;
}
popMatrix();
}
Look at the first part of the if-else-if statement:
if (state == 0) {
image(img0, x, y);
state = 1;
}
If state
is 0, then it displays img0
at (x
, y
), and then it
sets state
to 1 to so that the next time render()
is called.
The other parts of the if-else-statement work similarly. Notice in the last
if-statement state
is set to 0, and so the pattern of state values is
this: 0, 1, 2, 0, 1, 2, 0, 1, 2, ....
We now have a runnable program that will make a Mario walk across the screen. But there’s an obvious problem: the images change so quickly that Mario appears to have the jitters. To fix this, we need is some way to control the speed at which we change the images.
So we will need to create a timer to do this for us.
Adding a Timer to the Mario Class¶
Previously, we saw how to create and use a timer class
to control the time between events. Now lets add a Timer
to the
Mario
class so we can control the speed at which images are shown:
class Mario {
// ...
Timer timer;
int imgShowMillis; // how long to show one image for
Mario() { // constructor
timer = new Timer();
imgShowMillis = 200;
}
void render() {
pushMatrix();
scale(3);
if (state == 0) {
image(img0, x, y);
if (timer.elapsedMillis() > imgShowMillis) {
state = 1;
timer.reset();
}
} else if (state == 1) {
image(img1, x, y);
if (timer.elapsedMillis() > imgShowMillis) {
state = 2;
timer.reset();
}
} else if (state == 2) {
image(img2, x, y);
if (timer.elapsedMillis() > imgShowMillis) {
state = 0;
timer.reset();
}
}
popMatrix();
}
void update() {
// ...
}
}
We’ve added two new variables:
timer
, which is of typeTimer
, stores a timer.imgShowMillis
, which is of typeint
, stores the number of milliseconds that we want each image to be displayed for.
Both are initialized inside the constructor. As we saw with the Timer
class, an object’s constructor is automatically called when you create it with
new
. It’s job is to ensure that the object is correctly initialized, which
usually means that variables get correct initial values.
The render()
function has some changes. Look at the first if-statement:
if (state == 0) {
image(img0, x, y);
if (timer.elapsedMillis() > imgShowMillis) {
state = 1;
timer.reset();
}
}
After the image is displayed, we checked to see if the number of elapsed
milliseconds is bigger than imgShowMillis
. If it’s not, we do nothing,
which means that the next time render()
is called the same image
(img0
) will be displayed again.
But if the number of elapsed milliseconds is bigger than imgShowMillis
,
then we do two things:
- Set
state
to its next value, i.e. set it to 1. - Reset the timer’s elapsed time to 0.
Adding the timer makes the animation look much nicer.
100 Random Marios¶
Lets make the following changes to our program:
Add a new
float
variable calledsize
that stores the size of Mario. Inrender()
the statementscale(3)
will be replaced byscale(size)
.Add a new
update()
function so that Mario will wrap-around the screen when he goes off an edge:void update() { x += dx; y += dy; // wrap-around the screen if necessary if (x < 0) { x = width / size - 1; } else if (x > width / size) { x = 1; } else if (y < 0) { y = height / size - 1; } else if (y > height / size) { y = 1; } }
An important detail to notice here is that we use
width / size
andheight / size
in this function instead ofwidth
andsize
. We need to divide bysize
because we calledscale(size)
inrender()
, which scales the entire coordinate system (thus giving the effect of increasing the size of Mario). However, the program window stays the same size, and so if we don’t divide bysize
Mario will keep walking for a while off-screen before wrapping around, or get stuck on edges.Create a function called
randomMario()
that returns a newMario
object with randomly set values:Mario randomMario() { Mario m = new Mario(); m.x = random(10, 490); m.y = random(10, 490); m.dx = random(-2.0, 2.0); m.dy = random(-2.0, 2.0); m.imgShowMillis = int(random(100, 400)); m.size = random(0.5, 3); return m; }
Add an
ArrayList<Mario>
object to hold 100 Marios.
The complete source code is given at the end of the notes. The program works, although you will immediately notice that Mario is always facing to the right no matter which direction he is walking!
Questions¶
- Modify the 100 Marios program so that Mario also rotates as he walks.
Source Code for 1 Mario¶
For this code to work, you must put these files in your program’s data
folder: marioWalk1.png
,
marioWalk2.png
,
marioWalk3.png
.
Here is a single .zip archive of all the Mario images:
marioWalk.zip
.
class Sprite {
float x;
float y;
float dx;
float dy;
void update() {
x += dx;
y += dy;
}
}
class Timer {
int startTime;
Timer() {
reset();
}
int elapsedMillis() {
return millis() - startTime;
}
void reset() {
startTime = millis();
}
}
PImage img0;
PImage img1;
PImage img2;
Mario mario;
void setup() {
size(500, 500);
img0 = loadImage("marioWalk1.png");
img1 = loadImage("marioWalk2.png");
img2 = loadImage("marioWalk3.png");
mario = new Mario();
mario.y = 50;
mario.dx = 1;
}
void draw() {
background(255);
mario.render();
mario.update();
}
class Mario extends Sprite {
int state;
Timer timer;
int imgShowMillis; // how long to show one image for
Mario() {
timer = new Timer();
imgShowMillis = 200;
}
void render() {
pushMatrix();
scale(3);
if (state == 0) {
image(img0, x, y);
if (timer.elapsedMillis() > imgShowMillis) {
state = 1;
timer.reset();
}
}
else if (state == 1) {
image(img1, x, y);
if (timer.elapsedMillis() > imgShowMillis) {
state = 2;
timer.reset();
}
}
else if (state == 2) {
image(img2, x, y);
if (timer.elapsedMillis() > imgShowMillis) {
state = 0;
timer.reset();
}
}
popMatrix();
}
}
Source Code for 100 Marios¶
For this code to work, you must put these files in your program’s data
folder: marioWalk1.png
,
marioWalk2.png
,
marioWalk3.png
.
class Sprite {
float x;
float y;
float dx;
float dy;
void update() {
x += dx;
y += dy;
}
}
class Timer {
int startTime;
Timer() {
reset();
}
int elapsedMillis() {
return millis() - startTime;
}
void reset() {
startTime = millis();
}
}
PImage img0;
PImage img1;
PImage img2;
final int NUM_MARIOS = 100;
ArrayList<Mario> marios;
Mario randomMario() {
Mario m = new Mario();
m.x = random(10, 490);
m.y = random(10, 490);
m.dx = random(-2.0, 2.0);
m.dy = random(-2.0, 2.0);
m.imgShowMillis = int(random(100, 400));
m.size = random(0.5, 3);
return m;
}
void setup() {
size(500, 500);
img0 = loadImage("marioWalk1.png");
img1 = loadImage("marioWalk2.png");
img2 = loadImage("marioWalk3.png");
marios = new ArrayList<Mario>();
int i = 0;
while (i < NUM_MARIOS) {
Mario m = randomMario();
marios.add(m);
++i;
}
}
void draw() {
background(255);
for (Mario m : marios) {
m.render();
m.update();
}
}
class Mario extends Sprite {
float size;
int state;
Timer timer;
int imgShowMillis; // how long to show one image for
Mario() {
timer = new Timer();
imgShowMillis = 200;
size = 1;
}
void render() {
pushMatrix();
scale(size);
if (state == 0) {
image(img0, x, y);
if (timer.elapsedMillis() > imgShowMillis) {
state = 1;
timer.reset();
}
}
else if (state == 1) {
image(img1, x, y);
if (timer.elapsedMillis() > imgShowMillis) {
state = 2;
timer.reset();
}
}
else if (state == 2) {
image(img2, x, y);
if (timer.elapsedMillis() > imgShowMillis) {
state = 0;
timer.reset();
}
}
popMatrix();
}
void update() {
x += dx;
y += dy;
// wrap-around the screen if necessary
if (x < 0) {
x = width / size - 1;
}
else if (x > width / size) {
x = 1;
}
else if (y < 0) {
y = height / size - 1;
}
else if (y > height / size) {
y = 1;
}
}
}