An Exploding Hamburger¶
We’ve already seen how to make explosions, and to display hamburgers using a sprite, and so here we combine the two into a single program that makes a bouncing hamburger explodes when the user clicks anywhere with the mouse:
Explosion e; // initially null
Sprite burger; // initially null
void setup() {
size(500, 500);
smooth();
burger = new Sprite("burger.gif");
burger.visible = true;
burger.bouncing = true;
burger.dx = 1;
burger.dy = 1.5;
}
void draw() {
background(255);
if (e != null) { // != is "not equal to"
e.render();
e.update();
}
burger.render();
burger.update();
}
void mousePressed() {
if (burger.visible) {
burger.visible = false;
e = new Explosion(100, burger.x, burger.y);
}
else {
burger.visible = true;
burger.x = mouseX;
burger.y = mouseY;
e = null;
}
}
To use this program, click anywhere on the screen with the mouse.
An Exploding Burger Class¶
If we want more than one exploding burger, or if we want to use it in more than one program, then its a good idea to create a class that lets us make exploding hamburgers.
We’ll create a class called Burger
that stores both a sprite and
an explosion:
PImage BURGER_IMAGE;
void setup() {
// ...
BURGER_IMAGE = loadImage("burger.gif");
// ...
}
class Burger {
Sprite b;
Explosion e;
Burger() {
b = new Sprite(BURGER_IMAGE);
e = new Explosion(100, -1, -1);
}
// ...
} // class Burger
At any time, a Burger
object is either acting as a sprite or an
explosion. Thus we need some way to keep track of that, and so we
introduce a new variable called state
that tells us how to treat a
Burger
:
class Burger {
Sprite b;
Explosion e;
String state; // "burger", "exploding"
Burger() {
b = new Sprite(BURGER_IMAGE);
e = new Explosion(100, -1, -1);
state = "burger";
}
// ...
} // class Burger
A Burger
has always in one of two possible states: it’s a
hamburger, or it’s an explosion.
Now we can write render()
and update()
using if-statements
that check the value of state
in order to decide what to do:
class Burger {
Sprite b;
Explosion e;
String state; // "burger", "exploding"
Burger() {
b = new Sprite(BURGER_IMAGE);
e = new Explosion(100, -1, -1);
state = "burger";
}
void render() {
if (state.equals("burger")) {
b.render();
}
else if (state.equals("exploding")) {
e.render();
}
}
void update() {
if (state.equals("burger")) {
b.update();
}
else if (state.equals("exploding")) {
e.update();
}
}
} // class Burger
Note
Notice that we write state.equals("burger")
to test the
value of state
, and not state == "burger"
. The expression
state == "burger"
tests if the two string objects have the same
memory address, and that is not what we want to test here: we want
to know if the two strings consist of the same letters in the same
order. The expression state == "burger"
is almost never useful,
and, unfortunately, Processing does not give you any warning when
you use it.
Next consider what should happen when we change the value of
state
. If state
is currently "burger"
, then changing it to
"explosion"
will, for us, count as the creation of a new
explosion. Thus when state
goes from "burger"
to
"explosion"
, we need to reset the explosion to start fresh.
Going from "burger"
to "explosion"
doesn’t require anything
special, and so all we need to do in this case is change the value of
state
.
To ensure that the explosion is properly reset when the state changes
from "burger"
to "explosion"
, we’ll add a new function called
setState
:
class Burger {
// ...
void setState(String s) {
state = s;
if (state.equals("exploding")) {
e.reset();
setLocation(b.x, b.y);
}
}
// ...
} // class Burger
Note
It is up to the programmer to remember to set the state by
calling setState
instead of setting state
directly (e.g. by
writing burger.state = "explosion"
). Processing has a feature
known as private
variables that can help with this. We could
have declared state
to be private
, which means that code
outside of the Burger
class is not allow to read or write
state
. Thus there would be now way to accidentally set
state
directly.
If you are reading carefully, then you will have noticed that the
functions e.reset
and setLocation
don’t yet exist! So lets
write those now.
Resetting an Explosion¶
To avoid using too much memory, we add a reset()
function to
Explosion
that resets each shard:
class Explosion {
ArrayList<Shard> shards;
// ...
void reset() {
for(Shard s : shards) {
s.reset();
}
}
// ...
} // class Explosion
We’ll call reset()
when we want to re-initialize an explosion.
Setting the Location and Visibility of a Burger
¶
We also want to add a function to Burger
that lets us set its
location on the screen. Since a Burger
contains both a sprite and
an explosion, we use a function to do the setting:
class Burger {
// ...
void setLocation(float x, float y) {
// set burger location
b.x = x;
b.y = y;
// set explosion location
for(Shard s : e.shards) {
s.x = x;
s.y = y;
}
}
// ...
} // class Burger
While we’re at it, lets also add a similar function for setting the
visibility of a Burger
:
class Burger {
// ...
void setVisibility(boolean visible) {
// set burger visibility
b.visible = visible;
// set explosion visibility
for(Shard s : e.shards) {
s.visible = visible;
}
}
// ...
} // class Burger
This lets us control whether or not we can see a Burger
, no matter
its state.
Testing the Burger Class¶
Now lets test our new Burger
class in a program:
PImage BURGER_IMAGE; // needed by the Burger class
Burger burger;
void setup() {
size(500, 500);
smooth();
BURGER_IMAGE = loadImage("burger.gif");
burger = new Burger();
burger.b.dx = 2;
burger.b.dy = 1.5;
burger.b.bouncing = true;
burger.setVisibility(true);
}
void draw() {
background(255);
burger.update();
burger.render();
}
void mousePressed() {
if (burger.state.equals("burger")) {
burger.setState("exploding");
}
else if (burger.state.equals("exploding")) {
burger.setState("burger");
burger.setLocation(mouseX, mouseY);
}
}
Creating Reusable Code¶
If you’ve read these notes, then you’ve notice how much more work it
is to create re-usable code than it is to create one demo. Our
Explosion
class worked fine when we used it in simple
demonstration programs, but it proved to be inadequate when we used it
as part of the Burger
class. This is typical. Making a class (or
function) usable in any situation is harder than making it work in
one or two specific demos. It takes experience and practice to get
good at making re-usable code.
Programming Questions¶
- Add an explosion sound effect to the
Explosion
class. - Make the hamburger explode just when the user clicks on it with the mouse. If they click somewhere on the screen but not on the hamburger, then nothing happens.
The Code¶
Here is all the code to make the final example program work:
PImage BURGER_IMAGE;
Burger burger;
void setup() {
size(500, 500);
smooth();
BURGER_IMAGE = loadImage("burger.gif");
burger = new Burger();
burger.b.dx = 2;
burger.b.dy = 1.5;
burger.b.bouncing = true;
burger.setVisibility(true);
}
void draw() {
background(255);
burger.update();
burger.render();
}
void mousePressed() {
if (burger.state.equals("burger")) {
burger.setState("exploding");
}
else if (burger.state.equals("exploding")) {
burger.setState("burger");
burger.setLocation(mouseX, mouseY);
}
}
///////////////////////////////////////////////////////////
class Burger {
Sprite b;
Explosion e;
String state; // "burger", "exploding"
Burger() {
b = new Sprite(BURGER_IMAGE);
e = new Explosion(100, -1, -1);
state = "burger";
}
void render() {
if (state.equals("burger")) {
b.render();
}
else if (state.equals("exploding")) {
e.render();
}
}
void update() {
if (state.equals("burger")) {
b.update();
}
else if (state.equals("exploding")) {
e.update();
}
}
void setState(String s) {
state = s;
if (state.equals("exploding")) {
e.reset();
setLocation(b.x, b.y);
}
}
void setLocation(float x, float y) {
// set burger location
b.x = x;
b.y = y;
// set explosion location
for(Shard s : e.shards) {
s.x = x;
s.y = y;
}
}
void setVisibility(boolean visible) {
// set burger visibility
b.visible = visible;
// set explosion visibility
for(Shard s : e.shards) {
s.visible = visible;
}
}
} // class Burger
/////////////////////////////////////////////////////////////////////////////////
class Explosion {
ArrayList<Shard> shards;
Explosion(int numParticles, float x, float y) {
shards = new ArrayList<Shard>();
for(int i = 0; i < numParticles; ++i) {
Shard s = new Shard();
s.x = x;
s.y = y;
shards.add(s);
}
}
void reset() {
for(Shard s : shards) {
s.reset();
}
}
void render() {
for(Shard s : shards) {
s.render();
}
}
void update() {
for(Shard s : shards) {
s.update();
}
}
} // class Explosion
/////////////////////////////////////////////////////////////////////////////////
class Shard {
float x, y; // position
float dx, dy; // velocity
float r; // radius
float dr; // rate of change of radius
boolean visible;
Shard() {
reset();
}
void reset() {
dx = random(-2.0, 2.0);
dy = random(-2.0, 2.0);
r = 2 * random(2.0, 5.0);
dr = random(-0.5, 0.0);
visible = true;
}
void render() {
if (visible) {
ellipse(x, y, r, r);
}
}
void update() {
x += dx;
y += dy;
r += dr;
if (r < 1) { // when the shard gets too small, make it disappear
visible = false;
}
}
} // class Shard
/////////////////////////////////////////////////////////////////////////////////
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 *= -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?
}
// 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