30. 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 generic sprite class, that can be reused.

30.1. Introduction

You may have noticed a pattern in the classes we’ve created for animated objects. They all have the following structure:

class MyFlaberTasticAnimatedObject {
    float x, y;
    float dx, dy;

    MyFlaberTasticAnimatedObjet() {
        // ... Initialization code ...
    }

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

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

Every time we start writing a new program, we write these features. 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 the ones we’ve been making.

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 class for single-image sprites.

30.2. A Sprite Class

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

class Sprite {
    // ..

}

What should go in this class? For concreteness, let’s 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 now we can add variables to represent each of these pieces of information:

class Sprite {
    PImage img;
    float x, y;      // image position
    float dx, dy;    // image velocity

    boolean visible;
}

In order to be able to create an image, we need to write a constructor:

class Sprite {
    PImage img;
    float x, y;      // image position
    float dx, dy;    // image velocity

    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, let’s add render() and update() functions. We’ll worry about making the image bounce off the wall in a moment:

class Sprite {
    // ...

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

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

The render() function draws the image on the screen (x, y), but only if visible is true. The update() function is also quite straightforward, adding the velocity to the current position.

Here is the complete class:

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

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

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

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

30.3. Using the Sprite

Here’s how you can use Sprite:

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

    s = new Sprite("burger.gif");
    s.dx = 3;
    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.

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 as we have done so far. But the problem is that sometimes we might want a sprite that doesn’t bounce, or that bounces differently than what we want now.

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 which we will call bouncing. This variable will keep 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 of the variables and functions of the old one, plus we can add new variables and functions, or even re-define existing ones.

Inheritance is a useful technique, but it introduces more technical difficulties 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 = -dy;
            y = 0;
        }

        if (y + img.height >= height - 1) {
            dy = -dy;
            y = height - 1 - img.height;
        }

        if (x <= 0) {
            dx = -dx;
            x = 0;
        }

        if (y >= width - 1) {
            dx = -dx;
            x = width - 1 - img.width;
        }
    }
}

The changes are:

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

  • The update() function has one new line:

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

    Every time update 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 to detect edge collisions and deal with them. Notice that to avoid interpenetration, we use the image’s width and height, which are stored in img.width and img.height, respectively.

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

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

    s = new Sprite(500, 500);
    s.dx = 3;
    s.dy = 3;
    s.visible = true;
    s.bouncing = true;
}

void draw() {
    background(255);

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

30.4. Showing the Bounding Box

It’s often useful to visually display a sprite’s bounding box, so we’ll 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 ...
    }

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

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

We’ve used two new functions here, pushStyle() and popStyle(). The former saves the current style settings, e.g. the fill and stroke settings. The latter restores the style settings to be the way they were 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, 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 bouncingBoxVisible 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;
}

30.5. Hit Detection

Another property we might want in a Sprite, is a function 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) {
        return (x <= a && a <= x + img.width
                && y <= b && b <= y + img.height);
    }
}

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!");
    }
}

Note

In practice, the hit box for a sprite may not be the same as the sprite’s bounding box. So a more general sprite class would allow you to specify the position and dimensions of a hit box (or multiple hit boxes) that can be placed anywhere relative to the sprite.

Here we want to keep things simple, and so we make the bounding box the hit box.

30.6. Rotating Around the Centre

Let’s add some code that allows the sprite to rotate around it’s centre point. We’ll need a variable to keep track of the object’s 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.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();
        }
    }
}

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 rotate around its centre.

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

30.8. 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).

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