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, and img2, are loaded in setup. We name the first variable img0 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 type Timer, stores a timer.
  • imgShowMillis, which is of type int, 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 called size that stores the size of Mario. In render() the statement scale(3) will be replaced by scale(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 and height / size in this function instead of width and size. We need to divide by size because we called scale(size) in render(), 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 by size 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 new Mario 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

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