More Complex Sprites¶
In these notes you will learn:
- A general approach for modelling animated objects using object-oriented programming (OOP).
- How to write a class that extends
Sprite
. - The definition of base class, inherting class, subclass, and super class.
- How to use the
super
variable.
A Better Sprite Class¶
In previous notes we introduced the Sprite
class to
help simplify creating animated objects:
class Sprite {
float x;
float y;
float dx;
float dy;
}
With it we could write code like this:
color ballColor = color(255, 165, 0);
Sprite ball = new Sprite();
float radius;
void setup() {
size(500, 500);
smooth();
ball.x = 100;
ball.y = 100;
ball.dx = 1;
ball.dy = 2;
radius = 25;
}
void draw() {
background(255);
noStroke();
fill(ballColor);
ellipse(ball.x, ball.y, 2 * radius, 2 * radius);
ball.x += ball.dx;
ball.y += ball.dy;
// hit the left edge?
if (ball.x - radius <= 0) {
ball.dx = -ball.dx;
ball.x = radius;
}
// hit the right edge?
if (ball.x + radius >= 499) {
ball.dx = -ball.dx;
ball.x = 499 - radius;
}
// hit the top edge?
if (ball.y - radius <= 0) {
ball.dy = -ball.dy;
radius += 5;
ball.y = radius;
}
// hit the bottom edge?
if (ball.y + radius >= 499) {
ball.dy = -ball.dy;
ball.y = 499 - radius;
}
}
Using the Sprite
class makes it easier to declare x
, y
, dx
,
and dy
, and also makes the source code a little easier to read.
But we still have to do a lot of typing. For example, every animated objected
needs these two lines like this in draw()
:
ball.x += ball.dx;
ball.y += ball.dy;
These lines move the location of the ball, and so every animated object needs them. When you have multiple animated objects, these lines soon get tedious to type and start to clutter your program (making it harder to read).
So what we can do is add a function in the Sprite
class that does the
update for us:
class Sprite {
float x;
float y;
float dx;
float dy;
void update() {
x += dx;
y += dy;
}
}
Then we can replace the two +=
statements in draw()
with one call to
update()
:
// ... setup same as before ...
void draw() {
background(white);
// ...
// ball.x += ball.dx;
// ball.y += ball.dy;
ball.update();
// ...
}
This is a small but useful change. It saves us from writing two messy lines of code and makes our program a little shorter and easier to read.
An Animated Ball¶
We’ve seen an animated ball many times in this course, and now we want to create an object-oriented version of such a ball. For many programs, this is probably the best way to create animated objects.
The first step is to create a class for the animated object:
class Ball {
// ...
}
The name of class should describe the thing it is modelling: a class that
animates the sun should be called Sun
, a class that animates a bird should
be called Bird
, and so on.
Because Ball
is an animated object, we need to add variables for its
position and velocity:
class Ball {
float x;
float y;
float dx;
float dy;
// ...
}
While this works, there is a better way.
Inheritance¶
The Sprite
class we’ve been using already has x
, y
, dx
, and
dy
(plus update()
):
class Sprite {
float x;
float y;
float dx;
float dy;
void update() {
x += dx;
y += dy;
}
}
Instead of adding these four variables (and update()
) all again by hand to
Ball
, we can use inheritance like this:
class Ball extends Sprite {
// ...
}
Ball
inherits all the variables and functions from Sprite
. We say that
Sprite
is the base class, and Sprite
is the inheriting class.
Some programmers use different terminology: they say Ball
is a
subclass of Sprite
in this case, and, conversely, that Sprite
is a
superclass of ``
Now we can write code like this:
Ball b = new Ball();
void setup() {
size(500, 500);
b.x = 250;
b.y = 250;
b.dx = -1.6;
b.dy = 1.1;
}
Ball
behaves exactly the same as if we had written it like this:
class Ball {
float x;
float y;
float dx;
float dy;
void update() {
x += dx;
y += dy;
}
}
Obviously, using extends
is much simpler than typing all this out, and so
that it was we will do from now on for every animated class we create.
Improving Ball¶
Every ball has its own radius and fill color, and so that means we should add
a radius
and fillColor
variable to Ball
:
class Ball extends Sprite {
float radius;
color fillColor;
}
Next, lets add a function for drawing the Ball
. To avoid confusion with
the main draw()
function, we’ll call it render()
:
class Ball extends Sprite {
float radius;
color fillColor;
void render() {
noStroke();
fill(fillColor);
ellipse(x, y, 2 * radius, 2 * radius);
}
}
Now we can program a moving ball:
Ball b = new Ball();
void setup() {
size(500, 500);
// initialize the ball
b.x = 250;
b.y = 250;
b.dx = -1.6;
b.dy = 1.1;
b.radius = 25;
b.fillColor = color(255, 0, 0);
}
void draw() {
background(255);
b.render();
b.update();
}
This code is pretty simple: the details of how, exactly, the ball is rendered
and updated are hidden in the Sprite
and Ball
class.
Enabling the ball to bounce off the edges requires adding the usual set of if- statements:
Ball b = new Ball();
void setup() {
size(500, 500);
b.x = 250;
b.y = 250;
b.dx = -1.6;
b.dy = 1.1;
b.radius = 25;
b.fillColor = color(255, 0, 0);
}
void draw() {
background(255);
b.render();
b.update();
// hit left edge?
if (b.x < 0) {
b.x = 0;
b.dx = -b.dx;
}
// hit right edge?
if (b.x > 499) {
b.x = 499;
b.dx = -b.dx;
}
// hit top edge?
if (b.y < 0) {
b.y = 0;
b.dy = -b.dy;
}
// hit bottom edge?
if (b.y > 499) {
b.y = 499;
b.dy = -b.dy;
}
}
Calling a super Function¶
A better place to put the edge-detection if-statements is in the update()
for Ball
:
class Ball extends Sprite {
// ...
void render() {
// ...
}
void update() {
super.update(); // calls the update() function from Sprite
// hit left edge?
if (x < 0) {
x = 0;
dx = -dx;
}
// hit right edge?
if (x > 499) {
x = 499;
dx = -dx;
}
// hit top edge?
if (y < 0) {
y = 0;
dy = -dy;
}
// hit bottom edge?
if (y > 499) {
y = 499;
dy = -dy;
}
}
}
With this change our main code becomes short and sweet again:
Ball b = new Ball();
void setup() {
size(500, 500);
b.x = 250;
b.y = 250;
b.dx = -1.6;
b.dy = 1.1;
b.radius = 25;
b.fillColor = color(255, 0, 0);
}
void draw() {
background(255);
b.render();
b.update();
}
An important detail in the update()
function for Ball
is the very
first line:
void update() {
super.update();
// ...
}
Processing automatically adds a special variable called super
to every
object you create. It is how you can access functions defined in the “super
class”, i.e. the Sprite
class, that happen to have the same name as a
function in Ball
. Inside Ball
, the statement super.update()
calls
the update()
in Sprite
, while the statement update()
(without
super
) calls the update()
function Ball
.
Questions¶
Briefly describe what inheritance is in Processing, and how it works. Use an example in your answer.
Consider the following code fragment:
class Monster extends Sprite { // ... }
- How many different classes are named in this code?
- Which class is the base class?
- Which class is the inheriting class?
- Which class is the subclass?
- Which class is the super class?
What is the purpose of using
super
in a class? Give an example of how to use it.