(Extra) Animated Sprites¶
So far all the sprites we’ve used are single-image sprites. That’s
because our Sprite
class stores only one image:
class Sprite {
PImage img; // Sprite stores a single image
// ...
} // class Sprite
If we want our sprite image to change while it is moving, then we’ll to need
store one image for each frame of animation we want. So in this note we’ll
create a new class called AnimatedSprite
that stores and displays multiple
images for a sprite.
Storing Images in an ArrayList¶
The main idea is that instead of storing a single image in a PImage
variable, we will now store multiple images in an ArrayList<PImage>
:
class AnimatedSprite {
ArrayList<PImage> images;
int currentImage; // index of the image that will be displayed
int playRate; // speed at which to change currentImage
int imageSwitchTimer; // get a new image whenever this is 0
// ... rest of the code essentially the same as the Sprite class ...
}
We also added three new variables:
currentImage
is the index inimages
of the current image to display.playRate
stores the speed of the animation. It is how many frames must pass (i.e. calls toupdate()
beforecurrentImage
is changed. The higher the value ofplayRate
, the slower the animation will go.imageSwitchTimer
keeps track of when to change an image. It’s initially set toplayRate
, and every timeupdate()
is calledimageSwitchTimer
is decremented by 1. When it hits 0,currentImage
is changed.
Initializing an Animated Sprite¶
Here is the constructor for AnimatedSprite
:
AnimatedSprite(int init_playRate) { // constructor
images = new ArrayList<PImage>();
playRate = init_playRate;
imageSwitchTimer = playRate;
currentImage = 0;
visible = false;
bouncing = false;
boundingBoxVisible = false;
}
The only parameter is an initial play rate so that it knows how frequently to change the image.
We’ll add images with addImage
:
void addImage(String fname) {
PImage img = loadImage(fname);
images.add(img);
}
This is called once for each image we want to add to the sprite.
Note
In practice, it’s common for multiple sprites to be stored on a single image file known as a sprite sheet. For example, here’s the sprite sheet (from http://sdb.drshnaps.com/display.php?object=8608) for the images in these notes:
A big advantage of this approach is that there’s only a single image file
for the sprite. This saves memory, and can also take less time to load
since it requires only one call to loadImage
. Another possible benefit
is that it can be easier for the artist creating the sprites to save them
all side-by-side in a single image instead of saving them in multiple
files.
Accessing the individual sprites is a little bit trickier since you must
index into the image pixels using the pixels[]
array that is part of
every PImage
. We won’t go into the details of that here.
Rendering an Animated Sprite¶
Rendering an AnimatedSprite
is almost the same as rendering a single-image
sprite. The only difference is that we assign the image we want rendered to
img
at the beginning of render()
:
void render() {
PImage img = images.get(currentImage);
if (visible) {
pushMatrix();
// move and rotate the coordinate system
translate(x + img.height / 2, y + img.height / 2);
rotate(angle);
// draw the image
image(img, -img.width / 2, -img.height / 2);
popMatrix();
}
if (boundingBoxVisible) {
pushStyle();
noFill();
stroke(255, 0, 0);
rect(x, y, img.width, img.height);
popStyle();
}
}
We also make the same change to pointInBoundingBox
:
boolean pointInBoundingBox(float a, float b) {
PImage img = images.get(currentImage);
if (a > x && a < x + img.width && b > y && b < y + img.height)
return true;
else
return false;
}
Updating an Animated Sprite¶
To update an AnimatedSprite
, we do everything we did for a regular
sprite, plus check to see if we need to change currentImage
:
void update() {
x += dx;
y += dy;
angle += spinRate;
if (bouncing) checkEdgeCollisions();
// change the image if necessary
--imageSwitchTimer;
if (imageSwitchTimer <= 0) {
imageSwitchTimer = playRate;
currentImage += 1;
if (currentImage >= images.size()) { // wrap-around to first image if necessary
currentImage = 0;
}
}
}
imageSwitchTimer
is decremented by 1 on every call to
update()
. When it hits 0 (or less), we re-initialize it and then
make currentImage
refer to the next image in images
. If
currentImage
is incremented to a value past the end of images
,
then we set it to 0 (i.e make it “wrap around”).
A Sample Program¶
Here’s a program that makes an animated Mario walk across the screen:
AnimatedSprite mario;
void setup() {
size(500, 500);
// initialize the mario sprite
mario = new AnimatedSprite(10);
mario.x = 25;
mario.y = 25;
mario.dx = 2;
mario.visible = true;
mario.bouncing = true;
mario.addImage("marioWalk1.png");
mario.addImage("marioWalk2.png");
mario.addImage("marioWalk3.png");
mario.addImage("marioWalk4.png");
mario.addImage("marioWalk5.png");
mario.addImage("marioWalk6.png");
mario.addImage("marioWalk7.png");
mario.addImage("marioWalk8.png");
}
void draw() {
background(255);
mario.render();
mario.update();
}
It uses these image files: marioWalk1.png
, marioWalk2.png
, marioWalk3.png
, marioWalk4.png
, marioWalk5.png
, marioWalk6.png
, marioWalk7.png
, and marioWalk8.png
.
Complete Code¶
class AnimatedSprite {
ArrayList<PImage> images;
int currentImage; // index of the image that will be displayed
int playRate; // speed at which to change currentImage
int imageSwitchTimer; // get a new image whenever this is 0
float x, y;
float dx, dy;
float angle;
float spinRate;
boolean visible;
boolean bouncing;
boolean boundingBoxVisible;
AnimatedSprite(int init_playRate) { // constructor
images = new ArrayList<PImage>();
playRate = init_playRate;
imageSwitchTimer = playRate;
currentImage = 0;
visible = false;
bouncing = false;
boundingBoxVisible = false;
}
void addImage(String fname) {
PImage img = loadImage(fname);
images.add(img);
}
void render() {
PImage img = images.get(currentImage);
if (visible) {
pushMatrix();
// move and rotate the coordinate system
translate(x + img.height / 2, y + img.height / 2);
rotate(angle);
// draw the image
image(img, -img.width / 2, -img.height / 2);
popMatrix();
}
if (boundingBoxVisible) {
pushStyle();
noFill();
stroke(255, 0, 0);
rect(x, y, img.width, img.height);
popStyle();
}
}
void update() {
x += dx;
y += dy;
angle += spinRate;
if (bouncing) checkEdgeCollisions();
// change the image if necessary
--imageSwitchTimer;
if (imageSwitchTimer <= 0) {
imageSwitchTimer = playRate;
currentImage += 1;
if (currentImage >= images.size()) { // wrap-around to first image if necessary
currentImage = 0;
}
}
}
void checkEdgeCollisions() {
if (y < 0) dy *= -1; // hit the top edge?
if (y >= height) dy *= -1; // hit the bottom edge?
if (x < 0) dx *= -1; // hit the left edge?
if (x >= width) dx *= -1; // hit the right edge?
}
// Returns true when point (a, b) is inside this sprite's
// bounding box, and false otherwise.
boolean pointInBoundingBox(float a, float b) {
PImage img = images.get(currentImage);
if (a > x && a < x + img.width && b > y && b < y + img.height)
return true;
else
return false;
}
} // class AnimatedSprite
///////////////////////////////////////////////////////////////////
AnimatedSprite mario;
void setup() {
size(500, 500);
// initialize the mario sprite
mario = new AnimatedSprite(10);
mario.x = 25;
mario.y = 25;
mario.dx = 2;
mario.visible = true;
mario.bouncing = true;
mario.addImage("marioWalk1.png");
mario.addImage("marioWalk2.png");
mario.addImage("marioWalk3.png");
mario.addImage("marioWalk4.png");
mario.addImage("marioWalk5.png");
mario.addImage("marioWalk6.png");
mario.addImage("marioWalk7.png");
mario.addImage("marioWalk8.png");
}
void draw() {
background(255);
mario.render();
mario.update();
}