Multi-image Animation Using an ArrayList¶
Introduction¶
We saw in the previous note how to use a state variable and an if-else-if- statement to animate a multi-image sprite. It used only a few images, and could not be modified to work with many different images without a lot of tedious changes being made to the code.
So in this note we will modify the Mario
class to work with any number of
imags. It will use an ArrayList
to store its images instead of individual
variables. The basic idea of how the images are displayed one after the other
remains the same, but the implementation will change. The resulting code is a
little more complex, but it will let us make animations that work with any
number of images.
Using an ArrayList¶
In the first version of the Mario animation program, we used three images that were stored in global variables like this:
PImage img0;
PImage img1;
PImage img2;
// ...
void setup() {
size(500, 500);
img0 = loadImage("marioWalk1.png");
img1 = loadImage("marioWalk2.png");
img2 = loadImage("marioWalk3.png");
// ...
}
The problem with this is that if we have more than 3 images, we will need to
write many lines of nearly identical code. So lets replace this with an
ArrayList
:
ArrayList<PImage> images;
void setup() {
size(500, 500);
images = new ArrayList<PImage>();
images.add(loadImage("marioWalk1.png"));
images.add(loadImage("marioWalk2.png"));
images.add(loadImage("marioWalk3.png"));
// ...
}
Inside the Mario
class, we also need replace references to img0
,
img1
, and img2
with images.get(0)
, images.get(1)
, and
images.get(2)
. For example:
if (state == 0) {
image(images.get(0), x, y);
if (timer.elapsedMillis() > imgShowMillis) {
state = 1;
timer.reset();
}
}
With these changes, the program runs as before. However, the code inside the
Mario
render()
function still assumes there are exactly three images.
So we need to modify that to work with any number of images.
Modifying the render() Function¶
Here is the current version of the Mario
render()
function. We want to
modify it so that it works with any number of images, not just 3:
void render() {
pushMatrix();
scale(size);
if (state == 0) {
image(images.get(0), x, y);
if (timer.elapsedMillis() > imgShowMillis) {
state = 1;
timer.reset();
}
} else if (state == 1) {
image(images.get(1), x, y);
if (timer.elapsedMillis() > imgShowMillis) {
state = 2;
timer.reset();
}
} else if (state == 2) {
image(images.get(2), x, y);
if (timer.elapsedMillis() > imgShowMillis) {
state = 0;
timer.reset();
}
}
popMatrix();
}
Notice that when we call images.get
, the image we want is always the same
as the value of state
. This was intentional! What it means is that we
don’t actually need the if-statement. Instead, we replace all this code with
something that looks like this (although this is not quite right yet):
image(images.get(state), x, y);
if (timer.elapsedMillis() > imgShowMillis) {
state += 1; // this statement is not quite right!
timer.reset();
}
This almost works. The problem is the statement state += 1
quite correct.
When i
is 3, we set state
to 0, not 4. A simple way to fix this is
to add an if-statement that checks for this one case, i.e.:
image(images.get(state), x, y);
if (timer.elapsedMillis() > imgShowMillis) {
state += 1; // not quite right!
if (state == 3) state = 0; // fix state if necessary
timer.reset();
}
This works! The program runs the same as before. But now the source code is improved: we’ve replaced a large and repetitive if-else-if structure with a much shorter piece of code that does the same thing.
Note
Another clever way to fix this problem is to write this:
state = (i + 1) % 3;
No extra if-statement is needed.
The expression (i + 1) % 3
returns the remainder of dividing
i + 1
by 3. For example, if i
is 2, then (i + 1) % 3
is
the same as 3 % 3
which is 0. The expression 3 % 3
returns
the remainder of dividing 3 into 3. The remainder is 0, because 3
goes into 3 one time, with 0 left over.
%
is called the remainder, or mod (short for
modulus) operator.
One more change is needed to make the class work with any number of images.
The statement if (state == 3) state = 0
assumes there are only 3 images,
and so we need to change it to if (state == images.size()) state = 0
. The
expression images.size()
returns the number of PImage
objects in
images
.
Running the program again, we see the same as before. This means our changes are likely correct.
Using More Images¶
We’ve now generalized the Mario
class so that it will work with any number
of images. Lets test that by adding a few more:
void setup() {
size(500, 500);
images = new ArrayList<PImage>();
images.add(loadImage("marioWalk1.png"));
images.add(loadImage("marioWalk2.png"));
images.add(loadImage("marioWalk3.png"));
images.add(loadImage("marioWalk4.png"));
images.add(loadImage("marioWalk5.png"));
images.add(loadImage("marioWalk6.png"));
images.add(loadImage("marioWalk7.png"));
images.add(loadImage("marioWalk8.png"));
// ...
}
This works! No matter how many images you add to the images
ArrayList
,
the Mario
objects will display them all.
Questions¶
In
setup()
, the images are added like this:images.add(loadImage("marioWalk1.png")); images.add(loadImage("marioWalk2.png")); images.add(loadImage("marioWalk3.png")); images.add(loadImage("marioWalk4.png")); images.add(loadImage("marioWalk5.png")); images.add(loadImage("marioWalk6.png")); images.add(loadImage("marioWalk7.png")); images.add(loadImage("marioWalk8.png"));
Replace these lines of code with a while-loop where
images.add
is only called once inside the while-loop body.
Source Code¶
For this code to work, you must put these files in your program’s data
folder: marioWalk1.png
,
marioWalk2.png
,
marioWalk3.png
,
marioWalk4.png
,
marioWalk5.png
,
marioWalk6.png
,
marioWalk7.png
,
marioWalk8.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();
}
}
ArrayList<PImage> images;
final int NUM_MARIOS = 10;
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);
images = new ArrayList<PImage>();
images.add(loadImage("marioWalk1.png"));
images.add(loadImage("marioWalk2.png"));
images.add(loadImage("marioWalk3.png"));
images.add(loadImage("marioWalk4.png"));
images.add(loadImage("marioWalk5.png"));
images.add(loadImage("marioWalk6.png"));
images.add(loadImage("marioWalk7.png"));
images.add(loadImage("marioWalk8.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;
}
void render() {
pushMatrix();
scale(size);
image(images.get(state), x, y);
if (timer.elapsedMillis() > imgShowMillis) {
state += 1;
if (state == images.size()) state = 0; // fix state if necessary
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;
}
}
}