More Complex Animations with State Variables

In these notes you will learn how to:

  • Use a state variable to make an object do different things.
  • Use if-else-if statements in render/update functions to create complex animated behavior.

Introduction

Suppose you want to create an animation that shows little people (paratroopers) drop from the top of the screen. They fall for a few moments, and then suddenly open their parachutes and float gently to the ground. On the ground, their parachutes disappear and they walk off the screen.

What’s interesting about this example is that the paratroopers have three distinct states that we will need to keep track of:

  • The “falling” state, where they fall quickly without a parachute.
  • The “floating” state, where they fall slowly with a parachute.
  • The “walking” state, where they are walk, either left or the right, along the bottom of the screen.

A paratroopers appearance and behavior depends on its state. We’ll use if- statements inside the render() and update() functions to decide what to do based upon the current state.

We will also need to manage state transitions. For our paratroopers, we will use the following rules to decide when their state changes:

  • falling \(\rightarrow\) floating: When the paratrooper is falling, it should change to floating after it has fallen some randomly- chosen distance
  • floating \(\rightarrow\) walking: When the paratrooper is floating, it should change to walking when it reaches the ground.
  • walking \(\rightarrow\) falling: When the paratrooper is walking, it should change to falling after it has reached either the left edge or right edge of the screen.

When we change the state of the paratrooper, we may also perform other actions. For instance, when the state changes from “walking” to “falling”, we will move the paratrooper to the top of the screen and drop them again as if they were brand new.

The Main Program

Before we dive into the details of the paratrooper, lets look at the main program that controls them:

ArrayList<Paratrooper> troopers;  // initially null

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

  // make a new, initially empty, ArrayList of paratroopers
  troopers = new ArrayList<Paratrooper>();

  // add some paratroopers
  int i = 0;
  while (i < 25) {
     Paratrooper p = new Paratrooper();
     p.randomRestart();
     troopers.add(p);
     ++i;
  }
}

void draw() {
  background(255);

  for(Paratrooper t : troopers) {
    t.render();
    t.update();
  }
}

This is very similar to the code we wrote for the water particle system. The major difference will be the code we put in the Paratrooper class.

The Paratrooper Class

The Paratropper class has this overall structure:

class Sprite {
  float x;
  float y;
  float dx;
  float dy;

  void update() {
    x += dx;
    y += dy;
  }
}

class Paratrooper extends Sprite {

  // ... variables for this Paratrooper ...

  void randomRestart() {
    // ...  initializes variables ...
  }

  void render() {
    // ... drawing code ...
  }

  void update() {
    // ... updates the variables of the paratrooper object ...
  }

} // class Paratrooper

Lets start by defining the variables. We want:

  • drag, which calculates the speed of a floating paratrooper. A paratrooper floats downwards at speed drag * dy, and so drag should be between 0 and 1.
  • chuteOpenHeight, for the height along the y-axis for when a paratrooper’s chute opens.
  • bodyColor, for the color of the paratrooper’s body (which we’ll draw as a rectangle).
  • state, for the current state of the paratrooper: "falling", "floating", or "walking".

Now lets add those variables to the class, and initialize them in randomRestart:

class Paratrooper extends Sprite {
    float drag;
    float chuteOpenHeight;
    color bodyColor;

    String state; // "falling", "floating", or "walking"

    void randomRestart() {
        x = random(50, 450);
        y = 0;
        drag = random(0.02, 0.08);
        chuteOpenHeight = random(0.25 * height, 0.75 * height);
        dx = 0;
        dy = random(3.0, 6.0);
        bodyColor = color(random(256), random(256), random(256));
        state = "falling";
    }

    void render() {
      // ...
    }

    void update() {
      // ...
    }

} // class Paratrooper

Notice that we initialize state to the value "falling", so that paratroopers will begin falling from the top of the screen.

Now lets sketch the render() function:

void render() {
    pushMatrix();
    if (state.equals("falling")) {
      // ...
    } else if (state.equals("floating")) {
      // ...
    } else if (state.equals("walking")) {
      // ...
    }
    popMatrix();
}

As usual, we call pushMatrix() at the start of render() to save the current coordinate system. The call popMatrix() at the end restores it.

Between pushMatrix() and popMatrix() we have a big if-else-if structure that does different things depending upon the value of state:

  • When state is “falling”, we draw a colored rectangle.
  • When state is “floating”, we draw the colored rectangle plus a parachute on top.
  • When state is “walking”, we draw a colored rectangle.

Of course, you can make much more elaborate drawings than this if you have the time and interest.

Here’s a full implementation of render():

void render() {
  pushMatrix();
  if (state.equals("falling")) {
    translate(x, y);
    fill(bodyColor);
    noStroke();
    rect(0, 0, 5, 10);
  } else if (state.equals("floating")) {
    translate(x, y);

    // body
    fill(bodyColor);
    noStroke();
    rect(0, 0, 5, 10);

    // parachute
    fill(200);
    stroke(0);
    arc(2, -5, 20, 20, PI, 2*PI);
    line(0, 0, -8, -5);
    line(5, 0, 11, -5);
  } else if (state.equals("walking")) {
    translate(x, y);
    fill(bodyColor);
    noStroke();
    rect(0, 0, 5, 10);
  }
  popMatrix();
}

The only tricky thing here is how arc is used to draw a half-circle that looks like a parachute.

The update() function has a similar overall structure:

void update() {
    if (state.equals("falling")) {
      // ...
    } else if (state.equals("floating")) {
     // ...
    } else if (state.equals("walking")) {
      // ...
    }
}

There’s no need to call pushMatrix() and popMatrix() because update() doesn’t (and shouldn’t!) change the screen coordinate system.

Let’s list the things we need to do for each state in update():

  • When state is “falling”, we need update the sprite’s position with usual super.update() (i.e. we call the update() function in Sprite``q). If ``y >= chuteOpenHeight, then we will set state to “floating”.
  • When state is “floating”, we update y by adding drag * dy to it. If the paratrooper reaches the bottom of the screen (i.e. y >= height - 10), then we do this:
    • Set state to “walking”.
    • Set dx to random value chosen from a small range.
    • Set dy to 0.
  • When state is "walking", we call super.update(), and then . check if the paratrooper reaches the edge of the screen (i.e. if x < 0 or x > 500). If it does, we do this:
    • Set state to "falling".
    • Call randomRestart() so that the object starts again from the top of of the screen.

Here’s a complete implementation of update():

void update() {
  if (state.equals("falling")) {
    super.update(); // call the update() function in Sprite
    if (y >= chuteOpenHeight) {
      state = "floating";
    }
  } else if (state.equals("floating")) {
    x += dx;
    y += drag * dy;
    if (y >= height - 10) {
      state = "walking";
      dx = random(0.5, 2);
      dy = 0;
      if (random(0.0, 1.0) < 0.5) {
        dx = -dx;
      }
    }
  } else if (state.equals("walking")) {
    super.update(); // call the update() function in Sprite
    if (x < 0 || x > width) {
      state = "falling";
      randomRestart();
    }
  }
}

Notice this code:

if (random(0.0, 1.0) < 0.5) {
    dx = -dx;
}

The expression random(0, 1.0) returns a randomly chosen number in the range 0 to 1, and so random(0, 1.0) < 0.5 is true 50% of the time. It is like a coin-flip: half the time dx will be negated, the other half it won’t.

A Note on Efficiency

We’ve implemented state as a String variable so that it is easy to keep track of the current state. However, testing if two strings are equal usually requires more work than checking if, say, two int values are equal. If you only have a few animated objects on the screen at once, then making state a String probably makes no noticeable difference. However, for program with lots and lots of animated objects — such as a game — it could be too slow.

To increase the speed of checking state, there are a couple of common alternative approaches:

  • Make state of type int, and use numbers to represent states. For example, “falling” could be 1, “floating” 2, and “walking” 3. Programmers often define variables to remember what number each state is, e.g.:

    final int FALLING = 1;
    final int FLOATING = 2;
    final int WALKING = 3;
    

    The keyword final means that these variables cannot be changed later in the program — they are constants.

    We can then write code like this:

    void update() {
        if (state == FALLING) {
          // ...
        } else if (state == FLOATING) {
          // ...
        } else if (state == WALKING) {
          // ...
        }
    }
    

    == is usually more efficient then equals, so this could be somewhat faster code.

  • Use an enum. An enum is essentially a built-in version of the previous idea that is safe, readable, and efficient.

Programming Questions

  1. Add wind to the simulation, so that the paratroopers move a little left/right as they fall.

  2. Parachuting is a dangerous business, and so sometimes paratroopers die when they hit the ground. Add another state to the paratroopers called “dead” which causes them to appear dead at the bottom of the screen. You could render a dead paratrooper as a little tombstone, or a skull and cross bones.

    Paratroopers shouldn’t always become “dead” when they hit the ground. Use the random function to make, say, 50% of the paratroopers die when they hit the ground.

  3. Add an airplane that continually flies across the top of the screen, dropping the paratroopers as it goes.

Source Code

class Sprite {
  float x;
  float y;
  float dx;
  float dy;

  void update() {
    x += dx;
    y += dy;
  }
}

class Paratrooper extends Sprite {
  float drag;
  float chuteOpenHeight;
  color bodyColor;

  String state; // "falling", "floating", or "walking"

  void randomRestart() {
    x = random(50, width - 50);
    y = 0;
    drag = random(0.02, 0.08);
    chuteOpenHeight = random(0.25 * height, 0.75 * height);
    dx = 0;
    dy = random(3.0, 6.0);
    bodyColor = color(random(256), random(256), random(256));
    state = "falling";
  }

  void render() {
    pushMatrix();
    if (state.equals("falling")) {
      translate(x, y);
      fill(bodyColor);
      noStroke();
      rect(0, 0, 5, 10);
    } else if (state.equals("floating")) {
      translate(x, y);

      // body
      fill(bodyColor);
      noStroke();
      rect(0, 0, 5, 10);

      // parachute
      fill(200);
      stroke(0);
      arc(2, -5, 20, 20, PI, 2*PI);
      line(0, 0, -8, -5);
      line(5, 0, 11, -5);
    } else if (state.equals("walking")) {
      translate(x, y);
      fill(bodyColor);
      noStroke();
      rect(0, 0, 5, 10);
    }
    popMatrix();
  }

  void update() {
    if (state.equals("falling")) {
      super.update(); // call the update() function in Sprite
      if (y >= chuteOpenHeight) {
        state = "floating";
      }
    } else if (state.equals("floating")) {
      x += dx;
      y += drag * dy;
      if (y >= height - 10) {
        state = "walking";
        dx = random(0.5, 2);
        dy = 0;
        if (random(0.0, 1.0) < 0.5) {
          dx = -dx;
        }
      }
    } else if (state.equals("walking")) {
      super.update(); // call the update() function in Sprite
      if (x < 0 || x > width) {
        state = "falling";
        randomRestart();
      }
    }
  }
} // class Paratrooper

ArrayList<Paratrooper> troopers;  // initially null

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

  // make a new, initially empty, ArrayList of paratroopers
  troopers = new ArrayList<Paratrooper>();

  // add some paratroopers
  int i = 0;
  while (i < 25) {
    Paratrooper p = new Paratrooper();
    p.randomRestart();
    troopers.add(p);
    ++i;
  }
}

void draw() {
  background(255);

  for (Paratrooper p : troopers) {
    p.render();
    p.update();
  }
}