Handling the Keyboard

Processing has built-in support for reading key-presses and key-releases from the keyboard. So in this note we’ll make a program that lets the user move a box around the screen using both the WASD keys and the arrow keys.

A Box Class

First, lets create a class called Box that will draw the sprite we will be moving around the screen:

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

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

class Box extends Sprite {
  String state; // "square", "circle"
  float radius;

  // constructor
  Box() {
    x = width / 2;
    y = height / 2;
    dx = 0;
    dy = 0;
    state = "circle";
    radius = 50;
  }
  void render() {
    if (state.equals("square")) {
      rectMode(CENTER);
      noStroke();
      fill(255, 0, 0);
      rect(x, y, 2 * radius, 2 * radius);
    } else if (state.equals("circle")) {
      noStroke();
      fill(255, 0, 0);
      ellipse(x, y, 2 * radius, 2 * radius);
    }
  }

  void update() {
    super.update();
    x = constrain(x, radius, width - radius);
    y = constrain(y, radius, height - radius);
  }

  void halt() {
    dx = 0;
    dy = 0;
  }

  void flipState() {
    if (state.equals("square")) {
      state = "circle";
    } else if (state.equals("circle")) {
      state = "square";
    }
  }

  void increaseSize() {
    radius += 5;
    radius = constrain(radius, 5, 100);
  }

  void decreaseSize() {
    radius += -5;
    radius = constrain(radius, 5, 100);
  }
} // class Box

Box includes these functions:

  • Box, a default constructor, that initializes the variables with the values we want them to have. This constructor is called whenever we create a new Box object, e.g. in statements like this:

    Box b = new Box();   // Box constructor called
    
  • render, for drawing the Box, either as a circle or a square (depending upon the value of state). Note that the square is drawn using rectMode(CENTER) so that it appears in the same position as the circle.

  • update, which first calls the update() function in the Sprite class, and then uses constrain to make sure the sprite is always on the screen.

  • halt, for stopping the sprite.

  • flipState, for changing it from a circle to square, and vice-versa.

  • increaseSize() and decreaseSize(), for changing the size of the sprite. Note how these functions use constrain to keep the sprite from getting too small or too big.

Here’s our starting program, which draws a non-moving square in the middle of the screen:

Box box;

void setup() {
  size(500, 500);
  box = new Box();
}

void draw() {
  background(255);
  box.render();
  box.update();
}

Key Presses and Key Releases

The keyboard is complex piece of hardware. Not only does it have many different keys, but it also allows more than one key to be pressed at once, allowing you type things like Q, or shift-Q, or alt-Q, or ctrl-Q, etc.

An individual key-press consists of two distinct actions: pressing the key, and then releasing the key. Processing treats these as two different events that you can handle in whatever way you like using these functions:

void keyPressed() {
  // automatically called when any key is pressed
}

void keyReleased() {
  // automatically called when any key is released
}

Moving the Sprite

Lets move the sprite by pressing the WASD keys, where W is up, S is down, A is left, and D is right. Lets agree that we only want the sprite to move while the corresponding key is being pressed:

void keyPressed() {
  if (key == 'w' || key == 'W') { // up
    box.dx = 0;
    box.dy = -2;
  } else if (key == 's' || key == 'S') { // down
    box.dx = 0;
    box.dy = 2;
  } else if (key == 'a' || key == 'A') { // left
    box.dx = -2;
    box.dy = 0;
  } else if (key == 'd' || key == 'D') { // right
    box.dx = 2;
    box.dy = 0;
  }
}

void keyReleased() {
  if (key == 'w' || key == 'W') { // up
    box.halt();
  } else if (key == 's' || key == 'S') { // down
    box.halt();
  } else if (key == 'a' || key == 'A') { // left
    box.halt();
  } else if (key == 'd' || key == 'D') { // right
    box.halt();
  }
}

Notice a couple of important things here:

  • We check for both the lowercase and uppercase versions of the letter. So if the CAPS-LOCK key happens to be on the sprite will still move. Recall that || means “or”.
  • When, say, the W key is pressed down, the object starts moving. It doesn’t stop moving until the the W key is released. Thus, we need to use both the keyPressed and keyReleased functions.
  • The special Processing variable key is of type char, and things like 's' and 'D' are examples of char literals. Notice how they are wrapped in single-quotes, and not double-quotes (only String literals are wrapped in double quotes). Also, notice that == (not .equals, which is used to compare strings) is used to test if two characters are the same.

Handling Arrow Keys

Processing treats the arrow-keys as specially encoded keys that are handled slightly differently from letter-keys (like W, A, S, and D). You need to check two variables to see if an arrow key is pressed, e.g.:

if (key == CODED && keyCode == UP) {
  println("up arrow pressed");
}

When you press and arrow key, Processing assigns key the special value CODED, and then assigns the variable keyCode the value of the key that was pressed.

So, to handle arrow keys, we re-write keyPressed and keyReleased like this:

void keyPressed() {
    if ((key == CODED && keyCode == UP)
       || key == 'w' || key == 'W') { // up
    box.dx = 0;
    box.dy = -2;
  } else if ((key == CODED && keyCode == DOWN)
              || key == 's' || key == 'S') { // down
    box.dx = 0;
    box.dy = 2;
  } else if ((key == CODED && keyCode == LEFT)
              || key == 'a' || key == 'A') { // left
    box.dx = -2;
    box.dy = 0;
  } else if ((key == CODED && keyCode == RIGHT)
              || key == 'd' || key == 'D') { // right
    box.dx = 2;
    box.dy = 0;
  }
}

void keyReleased() {
  if ((key == CODED && keyCode == UP)
       || key == 'w' || key == 'W') { // up
    box.halt();
  } else if ((key == CODED && keyCode == DOWN)
              || key == 's' || key == 'S') { // down
    box.halt();
  } else if ((key == CODED && keyCode == LEFT)
              || key == 'a' || key == 'A') { // left
    box.halt();
  } else if ((key == CODED && keyCode == RIGHT)
              || key == 'd' || key == 'D') { // right
    box.halt();
  }
}

The if-statement expressions are starting to get long and messy, and so we split them into lines.

With this change the user can now move the sprite using either the WASD key, or the arrow keys — whichever they prefer.

Changing the Sprite

Finally, lets allow the user to change the sprite from a circle to a square (or vice versa) by pressing the space bar, and to increase, or decrease, the size of the sprite using - and +. Only the keyPressed function needs to be changed:

void keyPressed() {
  if (key == '-' || key == '_') {
    box.decreaseSize();
  } else if (key == '+') {
    box.increaseSize();
  } else if (key == ' ') {
    box.flipState();
  } else if ((key == CODED && keyCode == UP)
              || key == 'w' || key == 'W') { // up
    box.dx = 0;
    box.dy = -2;
  } else if ((key == CODED && keyCode == DOWN)
              || key == 's' || key == 'S') { // down
    box.dx = 0;
    box.dy = 2;
  } else if ((key == CODED && keyCode == LEFT)
              || key == 'a' || key == 'A') { // left
    box.dx = -2;
    box.dy = 0;
  } else if ((key == CODED && keyCode == RIGHT)
              || key == 'd' || key == 'D') { // right
    box.dx = 2;
    box.dy = 0;
  }
}

Notice that _ can also be used to decrease the size of the sprite. This makes it easy to hold down the shift key and alternate between pressing the + and - keys.

Questions

  1. Explain in brief, clear English the difference between a key-press and a key-release.
  2. Why does the program check for both the lowercase and uppercase versions of keys?
  3. What is the data type of the pre-defined Processing variable key?
  4. Describe, in English, how the sprite moves if you comment-out the entire keyReleased function.

Programming Questions

  1. Modify the program so that when the user presses one of the digit keys '0', '1', '2', ..., '9', the speed of movement is set to be that number. For example, pressing '4' will cause the sprite to move 4 pixels per frame for the rest of the program (or until the user presses another digit key). If the user presses '0', the sprite will not move at all (i.e. it will move at 0 pixels per frame).

    To aid with this, write a helper function called isDigit with this structure:

    boolean isDigit(char c) {
      // ...
    }
    

    It should return true just when c is one of '0', '1', '2', ..., or '9', and false otherwise.

  2. Modify the program to allow diagonal movement. For instance, you could use 'q', 'e', 'z', and 'c' for the diagonal keys.

  3. Modify the final program so that when the sprite goes off an edge, it wraps-around to the opposite edge and continues moving with the same velocity.

Final Program

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

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

class Box extends Sprite {
  String state; // "square", "circle"
  float radius;

  // constructor
  Box() {
    x = width / 2;
    y = height / 2;
    dx = 0;
    dy = 0;
    state = "circle";
    radius = 50;
  }

  void render() {
    if (state.equals("square")) {
      rectMode(CENTER);
      noStroke();
      fill(255, 0, 0);
      rect(x, y, 2 * radius, 2 * radius);
    } else if (state.equals("circle")) {
      noStroke();
      fill(255, 0, 0);
      ellipse(x, y, 2 * radius, 2 * radius);
    }
  }

  void update() {
    super.update();
    x = constrain(x, radius, width - radius);
    y = constrain(y, radius, height - radius);
  }

  void halt() {
    dx = 0;
    dy = 0;
  }

  void flipState() {
    if (state.equals("square")) {
      state = "circle";
    } else if (state.equals("circle")) {
      state = "square";
    }
  }

  void increaseSize() {
    radius += 5;
    radius = constrain(radius, 5, 100);
  }

  void decreaseSize() {
    radius += -5;
    radius = constrain(radius, 5, 100);
  }
} // class Box


Box box;

void setup() {
  size(500, 500);
  box = new Box();
}

void draw() {
  background(255);
  box.render();
  box.update();
}

void keyPressed() {
  if (key == '-' || key == '_') {
    box.decreaseSize();
  } else if (key == '+') {
    box.increaseSize();
  } else if (key == ' ') {
    box.flipState();
  } else if ((key == CODED && keyCode == UP) || key == 'w' || key == 'W') { // up
    box.dx = 0;
    box.dy = -2;
  } else if ((key == CODED && keyCode == DOWN) || key == 's' || key == 'S') { // down
    box.dx = 0;
    box.dy = 2;
  } else if ((key == CODED && keyCode == LEFT) || key == 'a' || key == 'A') { // left
    box.dx = -2;
    box.dy = 0;
  } else if ((key == CODED && keyCode == RIGHT) || key == 'd' || key == 'D') { // right
    box.dx = 2;
    box.dy = 0;
  }
}

void keyReleased() {
  if ((key == CODED && keyCode == UP) || key == 'w' || key == 'W') { // up
    box.halt();
  } else if ((key == CODED && keyCode == DOWN) || key == 's' || key == 'S') { // down
    box.halt();
  } else if ((key == CODED && keyCode == LEFT) || key == 'a' || key == 'A') { // left
    box.halt();
  } else if ((key == CODED && keyCode == RIGHT) || key == 'd' || key == 'D') { // right
    box.halt();
  }
}