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 behavior.

Introduction

Suppose you want to create an animation that shows little people (paratroopers) drop from the top of the screen for a few moments, and then, near the middle, suddenly open their parachutes and float gently to the ground. On the ground, their parachutes disappear and theu walk left or right along the bottom of the screen until they disappear through one of the edges.

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 downwards without a parachute.
  • The “floating” state, where they fall slowly downwards with a parachute.
  • The “walking” state, where they are walking, either towards the 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 paratrooper, we have the following state transitions:

  • 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();
     troopers.add(p);
     ++i;
  }
}

void draw() {
  background(255);

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

We define a variable of type ArrayList<Paratrooper>, and then in setup assign a new (initially empty) ArrayList<Paratrooper> object to it. Then we add some paratroopers to trooper using a while loop. Inside draw, after making the screen white, we then call render and update on each Paratrooper object in trooper.

The Paratrooper Class

The Paratropper class has this overall structure:

class Paratrooper {

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

  Paratrooper() {  // constructor
    // ...  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:

  • x and y, to store the position of the paratrooper.
  • fallingSpeed, to store the speed at which a “falling” paratrooper falls.
  • walkingSpeed, to store the speed at which a “walking” paratrooper walks.
  • drag, which will be used to calculate the speed of a floating paratrooper. A paratrooper will float downwards at speed drag * fallingSpeed, and so drag should be some value between 0 and 1.
  • chuteOpenHeight, to store the height along the y-axis for when a paratrooper’s chute should open up.
  • bodyColor, to store the color of the paratrooper’s body (which we’ll draw as a rectangle).
  • state, to store the current state of the paratrooper. The possible values of state are the strings "falling", "floating", and "walking".

Of course, you could add other variables if we wanted to make a more detailed paratrooper.

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

class Paratrooper {
  float x, y;
  float fallingSpeed;
  float walkingSpeed;
  float drag;
  float chuteOpenHeight;
  color bodyColor;

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

  Paratrooper() {
    x = random(50, 450);
    y = 0;
    drag = random(0.02, 0.08);
    chuteOpenHeight = random(50, 350);
    fallingSpeed = 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", which means that paratroopers will start out falling from the top of the screen.

Now lets create the render() function. It will have this general structure:

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 this saved coordinate system.

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

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

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

Here’s the 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);
      fill(bodyColor);
      noStroke();
      rect(0, 0, 5, 10); // body

      fill(200, 200, 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() for update() because it will not (should not!) be changing the screen coordinate system.

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

  • When state is “falling”, we need to increment y by fallingSpeed. Also, we need to check if y >= chuteOpenHeight. If it is, then we will set state to “floating”.
  • When state is “floating”, we increment y by drag * fallingSpeed. Also, we check to see if y >= 490, i.e. if the paratrooper has reached the bottom of the screen. If y >=  490 is true, then we do this:
    • Set state to “walking”.
    • Assign a value to walkingSpeed chosen at random from a small range of numbers.
  • When state is walking, we increment x (not y!) by walkingSpeed. Also, we check to see if the paratrooper has reached an edge of the screen, i.e. if x < 0 or x > 500. If so, then we do this:
    • Set state to “falling”.
    • Set x to be a random number in the range 50 to 450.
    • Set y to be 0 (so it will start falling from the top of the screen).

Here’s the entire implementation of update():

void update() {
    if (state.equals("falling")) {
      y += fallingSpeed;
      if (y >= chuteOpenHeight) {
        state = "floating";
      }
    } else if (state.equals("floating")) {
      y += drag * fallingSpeed;
      if (y >= 490) {
        state = "walking";
        walkingSpeed = random(0.5, 2);
        if (random(0.0, 1.0) < 0.5) {
          walkingSpeed = -walkingSpeed;
        }
      }
    } else if (state.equals("walking")) {
      x += walkingSpeed;
      if (x < 0 || x > width) {
        state = "falling";
        x = random(50, 450);
        y = 0;
      }
    }
}

Take a look at this code:

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

The expression random(0, 1.0) returns a randomly chosen number in the range 0 to 1, and to the express random(0, 1.0) < 0.5 is true approximately 50% of the time. It is like a coin-flip: half the time walkingSpeed will negated, while the rest of the time 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 using String for state probably doesn’t make any noticeable difference. However, for program with lots and lots of animated objects, it could be a problem.

To increase the speed of checking state, there are a couple of common 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.

Questions

  1. Add another state to the paratroopers called “waiting” which causes them to wait for a short, random amount of time after they land on the ground. After they finish waiting, they should start walking.
  2. Instead of having the paratroopers just appear falling on the screen, add an airplane that continually flies across the top of the screen, dropping the paratroopers as it goes.

Source Code

ArrayList<Paratrooper> troopers;

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

  troopers = new ArrayList<Paratrooper>();
  int i = 0;
  while (i < 25) {
     troopers.add(new Paratrooper());
     ++i;
  }
}

void draw() {
  background(255);

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


class Paratrooper {
  float x, y;
  float fallingSpeed;
  float walkingSpeed;
  float drag;
  float chuteOpenHeight;
  color bodyColor;

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

  Paratrooper() {
    x = random(50, 450);
    y = 0;
    drag = random(0.02, 0.08);
    chuteOpenHeight = random(50, 350);
    fallingSpeed = 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);
      fill(bodyColor);
      noStroke();
      rect(0, 0, 5, 10); // body

      fill(200, 200, 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")) {
      y += fallingSpeed;
      if (y >= chuteOpenHeight) {
        state = "floating";
      }
    } else if (state.equals("floating")) {
      y += drag * fallingSpeed;
      if (y >= 490) {
        state = "walking";
        walkingSpeed = random(0.5, 2);
        if (random(0.0, 1.0) < 0.5) {
          walkingSpeed = -walkingSpeed;
        }
      }
    } else if (state.equals("walking")) {
      x += walkingSpeed;
      if (x < 0 || x > width) {
        state = "falling";
        randomRestart();
      }
    }
  }

} // class Paratrooper