(Extra) Creating an Explosion with an Image

In these notes you will learn:

  • How to design an “explosion” by making an image fade away.
  • How to make an object explode.

Introduction

In these notes we’ll create a demo program that lets you click on hamburgers as they fall from the sky. A clicked burger explodes into an image of an explosion that quickly fades away.

We’ll create this program in multiple steps.

A Fading Image Explosion

Our first step is to create a simple demo that causes an explosion whenever we click on the screen:

PImage explosion;
boolean exploding;

float transparency;  // current transparency of the explosion
float dtrans;        // rate of change of transparency

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

  explosion = loadImage("explosion.png");
  exploding = false;
}


void draw() {
  background(255);

  if (exploding == true) {
    int mtrans = int(map(transparency, 0.0, 1.0, 0, 255));
    tint(255, mtrans);
    image(explosion, 250 - explosion.width / 2, 250 - explosion.height / 2);

    transparency += dtrans;

    if (transparency <= 0) {
      exploding = false;
    }
  }
}

void mousePressed() {
  exploding = true;
  transparency = 1.0;
  dtrans = -0.04;
}

Note the following:

  • A file named “explosion.png” must be in the program’s data folder. This is an image of an explosion that you create (e.g. using a paint program), or copied from somewhere else.

  • The variable exploding is of type boolean, meaning it either has the value true or false. We need this variable to keep track of when the explosion is occurring, and when nothing is happening.

  • The variable transparency store’s the image’s current transparency level as a number in the range 0.0 to 1.0. The variable dtrans is the rate of change of the transparency when the explosion is occurring.

  • Inside the draw() function, we only draw the explosion if the variable explosion is true; if it’s false, then nothing is drawn.

  • The first step of drawing the explosion is to map the value of transparency, which is in the range 0.0 to 1.0, into the range 0 to 255 required by the tint function.

  • The statement tint(255, mtrans) sets the transparency of the images that are drawn after it. The first value, 255, sets the tint to white, and the second value, mtrans is in the range 0 to 255 and sets the transparency of the image. This is a bit of a roundabout way to set transparency, but it works.

  • The explosion is drawn so that it’s upper-left corner is at the point (250 - explosion.width / 2, 250 - explosion.height / 2). This ensures that the middle of the image is in the center of the screen.

  • The explosion is considered to have finished when it has become totally transparent, and so this code resets the value of exploding:

    if (transparency <= 0) {
      exploding = false;
    }
    
  • Whenever the user of the program clicks the mouse, a new explosion is initialized:

    void mousePressed() {
      exploding = true;
      transparency = 1.0;
      dtrans = -0.04;
    }
    

    Notice that dtrans is negative — the value of transparency starts at 1.0 and then decreases.

Making it Object-Oriented

The previous program is useful for experimenting with different kinds of explosions. When you’ve settled on the basic style of the explosion, then it is a good idea to encapsulate it in a class:

class Explosion {
  PImage explosion;
  boolean exploding;

  float x, y;  // center of the explosion

  float transparency;  // current transparency of the explosion
  float dtrans;        // rate of change of transparency

  // constructor
  Explosion() {
    explosion = loadImage("explosion.png");
    exploding = false;
  }

  void render() {
    if (exploding == true) {
      int mtrans = int(map(transparency, 0.0, 1.0, 0, 255));
      tint(255, mtrans);
      image(explosion, x - explosion.width / 2, y - explosion.height / 2);
    }
  }

  void update() {
    if (exploding) {
      transparency += dtrans;

      if (transparency <= 0) {
        exploding = false;
      }
    }
  }

  void explode() {
    exploding = true;
    transparency = 1.0;
    dtrans = -0.04;
    x = mouseX;
    y = mouseY;
  }
} // class Explosion


Explosion e;

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

  e = new Explosion();
}


void draw() {
  background(255);

  e.render();
  e.update();
}

void mousePressed() {
  e.explode();
}

Note the following:

  • This object-oriented version of the program is essentially a re-organization of the code in the first demo program. It greatly simplifies the “calling code” in setup(), draw(), and mousePressed().

  • A difference between this program and the previous one is that the explosion is drawn at the point (x, y):

    void render() {
      if (exploding == true) {
        // ...
        image(explosion, x - explosion.width / 2, y - explosion.height / 2);
      }
    }
    

    This allows an explosion to appear anywhere on the screen.

  • A subtle but important detail is that this Explosion class calls loadImage inside of its constructor. While this is fine for a small demo program, it becomes a serious problem in larger programs that might have hundreds (or more) of explosions: calling loadImage for each explosion is bad because loadImage has to load the image from the disk — and disk operations are always relatively slow compared to the rest of the program. Another serious problem is that each explosion object has its own personal copy of the explosion image, which is a significant waste of memory.

    A more efficient approach is to load the image once outside of the class:

    class Explosion {
      PImage explosion;
    
      // ...
    
      // constructor
      Explosion() {
        explosion = explosionImage;
        // ...
      }
    
      // ...
    
    } // class Explosion
    
    PImage explosionImage;
    
    
    void setup() {
      explosionImage = loadImage("explosion.png");
    }
    
    // ...
    

    In this code, the image for the explosion is loaded one time in setup(). If you were to create, say, 100 Explosion, then loadImage is called only one time, and all the Explosion objects would share this one copy of the explosion image. This saves both time and memory.

Adding a Hamburger

Next, lets create a demo that displays hamburgers on the screen. When you click on a hamburger, it explodes.

The basic idea of the program is to create a class called Burger that displays either a hamburger image, or the explosion. Each Burger object stores an Explosion object, plus an image of a hamburger, and then uses a boolean variable called exploding to keep track of what should be displayed (either the burger, or the explosion).

Here’s the code:

class Explosion {
  PImage explosion;
  boolean exploding;

  float x, y;  // center of the explosion

  float transparency;  // current transparency of the explosion
  float dtrans;        // rate of change of transparency

  // constructor
  Explosion() {
    explosion = loadImage("explosion100.png");
    exploding = false;
  }

  void render() {
    if (exploding) {
      int mtrans = int(map(transparency, 0.0, 1.0, 0, 255));
      tint(255, mtrans);
      image(explosion, x - explosion.width / 2, y - explosion.height / 2);
    }
  }

  void update() {
    if (exploding) {
      transparency += dtrans;

      if (transparency <= 0) {
        exploding = false;
      }
    }
  }
} // class Explosion


// returns true if point (a, b) is the rectangle with upper-left
// corner (x, y), and width w and height h

boolean pointInRect(float a, float b,   // point
                    float x, float y,   // rectangle's upper-left corner
                    float w, float h)   // width and height
{
   if ((a >= x && a <= x + w) && (b >= y && b <= y + h)) {
      return true;
   } else {
      return false;
   }
}


class Burger {
  PImage burger;

  Explosion e;

  float x, y;  // location of the burger
  float dx, dy; // speed of the burger

  boolean exploding;

  // constructor
  Burger(float init_x, float init_y, float init_dx, float init_dy) {
    burger = loadImage("burger100.gif");
    x = init_x;
    y = init_y;
    dx = init_dx;
    dy = init_dy;
    exploding = false;

    // We create the explosion here so that the explosion graphic
    // is loaded only once at the beginning. The explosion is only
    // drawn when exploding is true.
    e = new Explosion();
  }

  // A burger is drawn either as the regular burger image (when it is
  // not exploding), or the explosion image (when it is exploding).
  void render() {
    if (exploding) {
      e.render();
      if (e.transparency < 0) {
        exploding = false;
      }
    }
    else {
      tint(255, 255);  // draw the burger totally opaque
      image(burger, x, y);
    }
  }

  void update() {
    if (exploding) {
      e.update();
    }
    else {
      x += dx;
      y += dy;
    }
  }

  // make the hamburger explode
  void explode() {
    if (!exploding) {
      exploding = true;

      e.exploding = true;
      e.transparency = 1.0;
      e.dtrans = -0.02;
      e.x = x + burger.width / 2;
      e.y = y + burger.height / 2;
    }
  }

  boolean contains(float a, float b) {
    return pointInRect(a, b, x, y, burger.width, burger.height);
  }
} // class Burger

///////////////////////////////////////////////////////////////////////

Burger b1, b2, b3;

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

  b1 = new Burger(200, 250, 0, 0);
  b2 = new Burger(100, 200, 0, 0);
  b3 = new Burger(400, 300, 0, 0);
}

void draw() {
  background(255);

  b1.render();
  b1.update();

  b2.render();
  b2.update();

  b3.render();
  b3.update();
}

void mousePressed() {
  if (b1.contains(mouseX, mouseY)) {
    b1.explode();
  } else if (b2.contains(mouseX, mouseY)) {
    b2.explode();
  } else if (b3.contains(mouseX, mouseY)) {
    b3.explode();
  }
}

Note the following:

  • The function in Burger called contains(a, b) tests if the point (a, b) is inside the bounding box of the burger image. Recall that the bounding box of the image is the (imaginary) rectangle that the image resides in.

    The function pointInRect was borrowed from earlier notes.

  • The boolean variable explosion in Burger is used to keep track of how a Burger object should be rendered and updated: if explosion is true, then it is rendered/updated as if it were an Explosion, and if it is false then it is rendered/updated as an image.

  • In the Burger render() function, the code for drawing the image of the burger is this:

    tint(255, 255);  // draw the burger totally opaque
    image(burger, x, y);
    

    If you left out the tint statement, then the burger would often be transparent due to the tint statement in Explosion. The tint of an image is always whatever the last call to tint set it to be, and the last call to tint in Explosion sets it to be totally transparent.

  • When the mousePressed function is called, it checks to see if one of the three burgers contains the point (mouseX, mouseY). If one does, that burger explodes.

Programming Questions

  1. Modify the basic image explosion so that, in addition to fading away, the size of the explosion increases.
  2. In the final program, if you click on the intersection of two (or more) overlapping burgers, then both burgers will explode. Modify the program so that, in this case, only the top burger explodes (and the burgers underneath don’t).
  3. To give explosions more variety, modify the explosions in the final program so that the image used for the explosion is chosen at random from two (or more) different explosion images.
  4. Create a simple game by modifying the final program (below) as follows:
    • Show a score on the screen.
    • Score (say) 1 point for clicking a burger before it reaches the bottom of the screen.
    • Score (say) -1 points when a burger hits the bottom of the screen without being clicked.
    • After a burger is clicked, or hits the bottom of the screen, re-set it so that it starts from the top of the screen again.
    • An introductory screen that explains the game.
    • Add “levels”, so that higher levels are harder to play (e.g. the burgers drop faster).
    • Have the game end after, say, 10 levels, or after too many burgers have been missed. Show a final screen that summarizes the player’s score.

The Final Program

//
// In this demo, hamburgers rain down from the top of the screen.
// If you click one it explodes.
//

PImage burgerImage;
PImage explosionImage;

class Explosion {
  PImage explosion;
  boolean exploding;

  float x, y;  // center of the explosion

  float transparency;  // current transparency of the explosion
  float dtrans;        // rate of change of transparency

  // constructor
  Explosion() {
    explosion = explosionImage;
    exploding = false;
  }

  void render() {
    if (exploding) {
      int mtrans = int(map(transparency, 0.0, 1.0, 0, 255));
      tint(255, mtrans);
      image(explosion, x - explosion.width / 2, y - explosion.height / 2);
    }
  }

  void update() {
    if (exploding) {
      transparency += dtrans;

      if (transparency <= 0) {
        exploding = false;
      }
    }
  }
} // class Explosion



// returns true if point (a, b) is the rectangle with upper-left
// corner (x, y), and width w and height h

boolean pointInRect(float a, float b, // point
float x, float y, // rectangle's upper-left corner
float w, float h)   // width and height
{
  if ((a >= x && a <= x + w) && (b >= y && b <= y + h)) {
    return true;
  }
  else {
    return false;
  }
}


class Burger {
  PImage burger;

  Explosion e;

  float x, y;  // location of the burger
  float dx, dy; // speed of the burger

  boolean exploding;
  boolean visible;

  // default constructor
  Burger() {
    this(random(100, width - 100), random(-175, -120), 0, random(1, 3));
  }

  // constructor
  Burger(float init_x, float init_y, float init_dx, float init_dy) {
    burger = burgerImage; //loadImage("burger100.gif");
    x = init_x;
    y = init_y;
    dx = init_dx;
    dy = init_dy;
    exploding = false;
    visible = true;

    // We create the explosion here so that the explosion graphic
    // is loaded only once at the beginning. The explosion is only
    // drawn when exploding is true.
    e = new Explosion();
  }

  // A burger is drawn either as the regular burger image (when it is
  // not exploding), or the explosion image (when it is exploding).
  void render() {
    if (!visible) return;
    if (exploding) {
      e.render();
    }
    else {
      tint(255, 255);  // draw the burger totally opaque
      image(burger, x, y);
    }
  }

  void update() {
    if (exploding) {
      e.update();
      if (e.transparency < 0) {
        exploding = false;
        visible = false;
      }
    }
    else {
      x += dx;
      y += dy;
    }
  }

  // make the hamburger explode
  void explode() {
    if (!exploding) {
      exploding = true;

      e.exploding = true;
      e.transparency = 1.0;
      e.dtrans = -0.05;
      e.x = x + burger.width / 2;
      e.y = y + burger.height / 2;
    }
  }

  boolean contains(float px, float py) {
    return pointInRect(px, py, x, y, burger.width, burger.height);
  }
} // class Burger

///////////////////////////////////////////////////////////////////////

ArrayList<Burger> burgers;

int numBurgers = 10;

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

  explosionImage = loadImage("explosion100.png");
  burgerImage = loadImage("burger100.gif");

  burgers = new ArrayList<Burger>();

  int i = 0;
  while (i < numBurgers) {
    Burger b = new Burger();
    burgers.add(b);
    ++i;
  }
}


void draw() {
  background(255);

  for (Burger b : burgers) {
    b.render();
    b.update();
  }
}

void mousePressed() {
  for (Burger b : burgers) {
    if (b.contains(mouseX, mouseY)) {
      b.explode();
    }
  }
}