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

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