32. 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.

32.1. 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 in images of the current image to display.
  • playRate stores the speed of the animation. It is how many frames must pass (i.e. calls to update() before currentImage is changed. The higher the value of playRate, the slower the animation will go.
  • imageSwitchTimer keeps track of when to change an image. It’s initially set to playRate, and every time update() is called imageSwitchTimer is decremented by 1. When it hits 0, currentImage is changed.

32.2. 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 sprite sheet.

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.

32.3. 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;
}

32.4. 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”).

32.5. 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.

32.6. 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();
}