31. A Simple Sprite Class

In these notes you will learn:

  • What a sprite is, and why they are useful in animation.
  • How to write a simple yet generic sprite class.

31.1. Introduction

You may have noticed a pattern in the classes we’ve created for animated objects. They all start out like this:

class MyCoolAnimatedThing {
   float x, y;    // position
   float dx, dy;  // velocity

   void render() {
      // ... drawing code ..
   }

   void update() {
     // ... updating code
   }

}

What we will do in this note is create a general-purpose class called Sprite that contains these basic features. This will make it easy to create animated objects like those we’ve been making in this course.

In computer animation, a sprite is any animated object. Typically, a sprite consists of one or more pre-made images. Here we’ll create a simple yet useful class for single-image sprites.

31.2. A Sprite Class

We want to write a class called Sprite that represents animated objects:

class Sprite {

   // ...

} // class Sprite

What should go in this class? For concreteness, lets use a bouncing image as the model to follow. To get an image to bounce around the screen, we need a few things:

  • An image. Processing uses the PImage class to store and manipulate images, and the loadImage function to read them from disk.
  • The position of the image on the screen. Recall that in Processing, images are placed by specifying the location of their upper-left corner.
  • The velocity of the image.
  • Whether or not the image is visible.

So lets add variables for each of these pieces of information:

class Sprite {
  PImage img;
  float x, y;
  float dx, dy;
  boolean visible;

  // ...
}

Next lets add a constructor:

class Sprite {
  PImage img;
  float x, y;
  float dx, dy;
  boolean visible;

  Sprite(String fname) {
    img = loadImage(fname);
    visible = false;  // starts invisible
  }

  // ...
}

The constructor requires the name of an image file. If the file does not exist, then loadImage crashes the program. In other words, we can’t create a Sprite unless we give it a valid image.

Next, lets add render() and update() functions. Lets not yet worry about the ball bouncing off walls yet:

class Sprite {

  // ...

  void render() {
    if (visible) image(img, x, y);
  }

  void update() {
    x += dx;
    y += dy;
  }
}

The render() function draws the image on screen at (x, y). If visible is false, then nothing is drawn. The update() function is also quite straightforward, adding the velocity to the current position.

Here is the complete class:

class Sprite {
  PImage img;
  float x, y;
  float dx, dy;
  boolean visible;

  Sprite(String fname) {
    img = loadImage(fname);
    visible = false;  // starts invisible
  }

  void render() {
    if (visible) image(img, x, y);
  }

  void update() {
    x += dx;
    y += dy;
  }
} // class Sprite

31.3. Using Sprite

Here’s how you can use Sprite:

Sprite s;

void setup() {
  size(500, 500);

  s = new Sprite("burger.gif");
  s.dx = 1;
  s.dy = 3;
  s.visible = true;
}

void draw() {
  background(255);

  s.render();
  s.update();
}

The setup() function creates a Sprite object based on the image file burger.gif (which contains a small cartoon hamburger). It sets the position of the sprite to be the upper-left corner of the screen, and then sets the velocity so that the burger moves diagonally towards the bottom right. Finally, we set visible to true so that something will actually be drawn on the screen.

31.4. Making it Bounce

Now lets make the image in our sprite bounce. We need to add edge-detection code, but the question is where do we add it? One possibility is to put it in the update() function of the Sprite class. But the problem with that is that sometimes we may want a sprite that doesn’t bounce.

There are a number of ways of providing both bouncing and non-bouncing sprites. The technique we’ll use here is to add a new boolean variable to Sprite called bouncing that keeps track of whether or not the sprite should bounce off the screen edges.

Note

One of the most common ways to solve this problem in an object-oriented language like Processing is to use inheritance. Inheritance is essentially a technique that lets you create a new class by basing it on an existing class. The new class inherits all the variables and functions of the old one, plus you can add new variables and functions, or even re-define existing functions.

Inheritance is a useful technique, but it introduces more technical details than we have time to cover in this course.

Here is the modified Sprite class:

class Sprite {
  PImage img;
  float x, y;
  float dx, dy;
  boolean visible;
  boolean bouncing;

  Sprite(String fname) {
    img = loadImage(fname);
    visible = false;
    bouncing = false;
  }

  void render() {
    if (visible) image(img, x, y);
  }

  void update() {
    x += dx;
    y += dy;
    if (bouncing) checkEdgeCollisions();
  }

  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?
  }
}

The changes are:

  • The boolean variable bouncing has been added. It is initially given the value false, meaning the sprite won’t bounce.

  • The update function has one new line:

    void update() {
      x += dx;
      y += dy;
      if (bouncing) checkEdgeCollisions();
    }
    

    Every time update it is called it checks to see if the ball is bouncing, and, if so, runs the edge collision detection code.

  • The function checkEdgeCollisions has been added:

    void checkEdgeCollisions() {
      if (y <= 0) dy = -dy;      // hit the top edge?
      if (y >= height) dy = -dy; // hit the bottom edge?
      if (x <= 0) dx = -dx;      // hit the left edge?
      if (x >= width) dx = -dx;  // hit the right edge?
    }
    

    The variables width and height are pre-defined Processing variables that return the width and height of the screen.

We’ve seen the code for making balls bounce before, and so we only do the bare minimum here. You can improve it in a number of ways.

To test this new code, you need only add one line to the previous test program to turn bouncing on:

Sprite s;

void setup() {
  size(500, 500);

  s = new Sprite("burger.gif");
  s.dx = 1;
  s.dy = 3;
  s.visible = true;
  s.bouncing = true;
}

void draw() {
  background(255);

  s.render();
  s.update();
}

31.5. Showing the Bounding Box

It’s often useful to visually display a sprite’s bounding box, and so lets add that as an option to Sprite. Similar to what we did to add bouncing, we will use a boolean variable to keep track of whether or not the bounding box should be displayed:

class Sprite {
  // ... same variables as before ...
  boolean boundingBoxVisible;

  Sprite(String fname) {
    // ... same as before ...
    boundingBoxVisible = false;
  }

  void render() {
    if (visible) image(img, x, y);
    if (boundingBoxVisible) {
      pushStyle();
      noFill();
      stroke(255, 0, 0);
      rect(x, y, img.width, img.height);
      popStyle();
    }
  }

  // ... same as before ...
} // class Sprite

We’ve used two new functions here, pushStyle and popStyle. The pushStyle function saves the current style settings, e.g. the fill and stroke settings. The popStyle function restores the style settings to be the way they were to just before the most recent call to pushStyle. Together, these two functions let us set the stroke and fill to be what we want for the bounding box without over-writing any style settings that other parts of the program may have set.

Note

As you might guess by their names, pushStyle and popStyle are similar to pushMatrix and popMatrix: pushMatrix and popMatrix save and restore the coordinate system settings, while pushStyle and popStyle save and restore the style settings. Both pairs of functions are used frequently in Processing programs.

To turn on the sprite’s bounding box set boundingBoxVisible to true, e.g.:

void setup() {
  size(500, 500);

  s = new Sprite("burger.gif");
  s.dx = 1;
  s.dy = 3;
  s.visible = true;
  s.bouncing = true;
  s.boundingBoxVisible = true;
}

31.6. Hit Detection

Lets add a function to Sprite that makes it easy to test if a given point is inside the sprite’s bounding box:

class Sprite {
  // ...  same as before ...

  // Returns true when point (a, b) is inside this sprite's
  // bounding box, and false otherwise.
  boolean pointInBoundingBox(float a, float b) {
    if (a > x && a < x + img.width &&  b > y && b < y + img.height)
      return true;
    else
      return false;
  }

} // class Sprite

This is essentially the same as the pointInRect function we saw in the hit detection notes, but it is more convenient to call since it needs only two parameters.

We can use it like this:

Sprite s;

void setup() {
  size(500, 500);

  s = new Sprite("burger.gif");
  s.dx = 1;
  s.dy = 3;
  s.visible = true;
  s.bouncing = true;
  s.boundingBoxVisible = true;
}

void draw() {
  background(255);

  s.render();
  s.update();
}

void mousePressed() {
  if (s.pointInBoundingBox(mouseX, mouseY)) {
    println("burger hit!");
  }
}

31.7. Rotating Around the Center

Finally, lets add some code that allows the sprite to rotate around its center point. We’ll need a variable to keep track of the objects angle, and another variable to store the rate at which it is spinning:

class Sprite {
  // ... same as before ...

  float angle;
  float spinRate;

  // ... same as before ...

  void render() {
    if (visible) {
      pushMatrix();

      // move and rotate the coordinate system
      translate(x + img.width / 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();
  }

  // ... same as before ...

} // class Sprite

Note a couple of things:

  • Rotating requires moving the origin of the coordinate system where we want the rotation to be centered, and then doing the rotation. Since we are modifying the coordinate system, we need to use pushMatrix and popMatrix to save and restore it to that way it was before the rotation.
  • The bounding box does not get rotated in our Sprite class. This is because the bounding box is used as the hit box, and our hit detection function does not take angles into account (it assumes the sides of the box are axis-aligned, i.e. parallel to the x-axis and y-axis).

Note

In practice, it would probably be more useful to allow the use of the Sprite class to specify the point they want the sprite to rotate around. However, to keep things simple our sprite will always rotate around the center.

31.8. One More Constructor

Finally, lets add one more constructor to the Sprite class:

class Sprite {
  // ...

  Sprite(PImage init_img) {
    img = init_img;
    visible = false;
    bouncing = false;
    boundingBoxVisible = false;
  }

  Sprite(String fname) {
    img = loadImage(fname);
    visible = false;
    bouncing = false;
    boundingBoxVisible = false;
  }

  // ...
}

A class can have as many different constructors as it needs, so long as each constructor takes different variables as input. Here, the new constructor we’ve just added takes a PImage object called init_img as input. This now lets us write code where multiple Sprite objects share the same image. This is a very good thing: sharing images cuts down on the memory needed by the program, and also shortens the time it takes to load images from disk.

Here’s a sample of how this new constructor is meant to be used:

ArrayList<Sprite> burgers;

void setup() {
   size(500, 500);

   burgers = new ArrayList<Sprite>();
   PImage img = loadImage("burger.gif");

   for(int i = 0; i < 100; ++i) {
      Sprite s = new Sprite(img);
      burgers.add(s):
   }
}

This code creates an ArrayList of 100 sprites that all share the same PImage. The file burger.gif is only loaded one time into memory.

31.9. Programming Questions

  1. Modify the edge-checking code in checkEdgeCollisions so that no part of the sprite is ever drawn off the screen, no matter its speed.

  2. Add a function called setUpperLeftCorner(x, y) that sets the upper-left corner of a sprite’s image to be (x, y):

    // cat is sprite that has already been defined
    cat.setUpperLeftCorner(250, 250);
    
  3. Add a function called setCenter(x, y) that sets the center-point of a sprite’s image to be (x, y):

    // cat is sprite that has already been defined
    cat.setCenter(250, 250);
    

    Note that you don’t need to add any new variables to implement this function.

  4. Add a new boolean variable called upperLeftVisible that is initially false. When it is set to true, a small (e.g.) red dot is drawn over the upper-left corner of the sprite’s image.

  5. Add a new boolean variable called centerVisible that is initially false. When it is set to true, a small (e.g.) red dot is drawn over the center point of the sprite’s image.

  6. Add a new boolean variable called followMouse that is initially false. When it is set to true, the sprite’s center point is drawn at (mouseX, mouseY).

31.10. The Complete Sprite Class

class Sprite {
  PImage img;
  float x, y;
  float dx, dy;
  float angle;
  float spinRate;

  boolean visible;
  boolean bouncing;
  boolean boundingBoxVisible;

  Sprite(PImage init_img) {
    img = init_img;
    visible = false;
    bouncing = false;
    boundingBoxVisible = false;
  }

  Sprite(String fname) {
    img = loadImage(fname);
    visible = false;
    bouncing = false;
    boundingBoxVisible = false;
  }

  void render() {
    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();
  }

  void checkEdgeCollisions() {
    if (y < 0) dy = -dy;       // hit the top edge?
    if (y >= height) dy = -dy; // hit the bottom edge?
    if (x < 0) dx = -dx;       // hit the left edge?
    if (x >= width) dx = -dx;  // 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) {
    if (a > x && a < x + img.width &&  b > y && b < y + img.height)
      return true;
    else
      return false;
  }
} // class Sprite