(Extra) 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.
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.
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 theloadImage
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
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.
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
variablebouncing
has been added. It is initially given the valuefalse
, 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
andheight
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();
}
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;
}
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!");
}
}
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.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();
}
// ... 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
andpopMatrix
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.
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.
Programming Questions¶
Modify the edge-checking code in
checkEdgeCollisions
so that no part of the sprite is ever drawn off the screen, no matter its speed.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);
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.
Add a new
boolean
variable calledupperLeftVisible
that is initiallyfalse
. When it is set totrue
, a small (e.g.) red dot is drawn over the upper-left corner of the sprite’s image.Add a new
boolean
variable calledcenterVisible
that is initiallyfalse
. When it is set totrue
, a small (e.g.) red dot is drawn over the center point of the sprite’s image.Add a new
boolean
variable calledfollowMouse
that is initiallyfalse
. When it is set totrue
, the sprite’s center point is drawn at (mouseX
,mouseY
).
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