(Extra) 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

  1. Add an explosion sound effect to the Explosion class.
  2. 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