1. Introduction
  2. Graphics
  3. Physics
  4. Collision Detection
  5. Computer AI (You’re here!)
  6. Screen Transitions
  7. Postmortem

Player and CPU Input

Johnny 5 Short Circuit -- NEED INPUT

(If you haven’t seen Short Circuit yet, go watch it!)

Fundamental to any video game is player input. Without it, there’s no way to interact with the game, and very little else can happen. There are some exceptions – Plantera and its upcoming sequel essentially play themselves and are very good. But lets be real, a rocket-powered soccer car game is kind of boring if you can’t do anything.

In previous articles we touched a bit on how Pocket League players can jump, drive and roll based on buttons they pressed. Mostly, we glossed over this by saying something like “the car jumps when the player presses A”. Technically, this is the truth, but there is more behind it, such as limiting controls while the timer counts down or preventing the car from rolling when on the ground. Furthermore, the non-player car also needs to be able to make the same movements, and that car should also be constrained by the same rules as the player.

Let’s dig into how this was accomplished.


Reading Gameboy Input with GBDK

First, a little background, especially for those not used to bitmasks and bitwise operations.

GBDK uses an 8 bit unsigned integer to store the current state of the joypad – all the buttons like Start, Select, A, B, and the D-Pad directions. This number can have a value of 0 - 255. The actual value of that number is relatively unimportant, since it’s used as a bitmask.

In essence, each bit of the number is a flag that determines which button was pressed. Since it’s an 8 bit number, there must be 8 buttons mapped. We can confirm this by looking at some of the GBDK library code where the button constants are defined:

// In gb.h

/** Joypad bits.
    A logical OR of these is used in the wait_pad and joypad
    functions.  For example, to see if the B button is pressed
    try
    UINT8 keys;
    keys = joypad();
    if (keys & J_B) {
    	...
    }
    @see joypad
 */
#define	J_START      0x80U
#define	J_SELECT     0x40U
#define	J_B          0x20U
#define	J_A          0x10U
#define	J_DOWN       0x08U
#define	J_UP         0x04U
#define	J_LEFT       0x02U
#define	J_RIGHT      0x01U

These constants are defined in hex, so it’s a little hard to see what’s going on if you’re not used to it. Let’s break it apart a bit. The easiest case is J_RIGHT or 0x01:

J_RIGHT
hex:     0x01
decimal: 1
binary:  0000 0001

So in this case, the last bit is set to 1. For another button, a different bit will be set to 1:

J_DOWN
hex:     0x08
decimal: 8
binary:  0000 1000

And so on. The full mapping goes in order they’re defined above.

GBDK provides a utility function called joypad to read the inputs. Note, more than one key can be pressed at a time, so multiple flags can be set! For instance, it might be 0x28 or 0010 1000, meaning both the B and Down buttons are pressed. Once the inputs are read, we now need to check which ones are pressed so that we can do things.


Bitwise Operations (AND and OR)

The most common thing we’re going to want to do in Pocket League is check whether a button is pressed. We can do this easily with the bitwise AND operator (&). Using this, we can take any two numbers and return only the bits that are set in both numbers.

Similarly, we can use bitwise OR (|) to return the bits that are set in either one number or the other. Pocket League doesn’t use this much for player input, but setting sprite properties in Part 1 relies heavily on bitwise OR. The computer car, as you’ll see, also uses it quite a bit, but in a different way.

Lets see an example so it’s easier to visualize:

Bitwise Operations

For the joypad constants like J_START, since only a single bit is set, this is particularly useful. If we wanted to check if the Start button is pressed, we can simply AND it with J_START:

UINT8 input = joypad();

if (input & J_START) {
  // Do some things!
}

Giving the Player Control

I’m going to include the bulk of the tick_car_physics function here. We’ve already been over how the actual game physics are calculated, but this time around we will be talking about what it’s doing with the input1 and input2 parameter.

void tick_car_physics(UINT8 n, UINT8 *x, UINT8 *y, INT8 *d_x, INT8 *d_y, UINT8 *rot, UINT8 input1, UINT8 input2) {
  // Jump
  if (debounced_input(J_A, input1, input2) && *y == FLOOR) {
    *d_y = -JUMP_ACCELERATION;
  }

  // Drive
  if (*y == FLOOR) {
    *rot = 0;
    
    if (input1 & J_RIGHT)  {
      *d_x += ACCELERATION;
    }
    else if (input1 & J_LEFT) {
      *d_x -= ACCELERATION;
    }
    else if (!(input1 & J_B)) {
      if (*d_x > 0) {
        *d_x -= ACCELERATION;

        if (*d_x <= 0) {
          *d_x = 0;
        }
      }

      if (*d_x < 0) {
        *d_x += ACCELERATION;

        if (*d_x >= 0) {
          *d_x = 0;
        }
      }
    }
  }
  else {
    if (input1 & J_RIGHT)  {
      *rot += ROTATION_SPEED;
    }
    else if (input1 & J_LEFT) {
      *rot -= ROTATION_SPEED;
    }
  }

  if (input1 & J_B) {
    calculate_boost_velocity_vectors(n, *rot, d_x, d_y);
  }
}

As we’ve talked about, this function is essentially a determination of what the car can do based on its position and the input that you give it. There are four main actions: drive, jump, boost, and air roll.


debounced_input

I want to make special callout to the debounced_input function here. It serves one purpose, which is to limit an input from being held. Hence, it takes three parameters (the target button, the current input, and the previous input). If the two match, the button is being held. If it’s not, then the button press is new and we should do something with it:

// Check for match on new input BUT NOT old input
INT8 debounced_input(INT8 target, INT8 new, INT8 old) {
  return (new & target) && !(old & target);
}

Without this helper function, you could hold down the A button and the car would “bounce” repeatedly. Not good. We want to require a new A press every time you want to jump. debounced_input also solves some similar issues with the menu as well, but we won’t go over that here.


Johnny 5 Pocket League IS ALIVE!

If you don’t get this joke, seriously watch Short Circuit.

You may have caught on at this point, but the CPU car uses the exact same tick_car_physics function, so it needs to provide the current and previous inputs in order to work. Conveniently, this means that it’ll behave EXACTLY like the player car – it abides by the same rules. In order to facilitate this, we now simply need to calculate the optimum input for the CPU car, which we do using a function called calculate_cpu_input.

Here it is in its entirety, the “brain” of Pocket League:

INT8 calculate_cpu_input(UINT8 x, UINT8 y, UINT8 rot, UINT8 ball_x, UINT8 ball_y, INT8 ball_d_x, INT8 ball_d_y) {
  UINT8 counter = 0;
  UINT8 input = 0x00; // Nothing
  UINT8 quadrant = 0;

  while (counter < CPU_PREDICTION) {
    tick_ball_physics(
      &ball_x, &ball_y, &ball_d_x, &ball_d_y,
      255, 255, 255, 255, 0,
      255, 255, 255, 255, 0,
      1 // skip collision checking
    );

    counter++;
  }

  if (int_distance(ball_y, y) < 15) { // Same Y value, should drive if on the floor
    if (y == FLOOR) {
      if (x < ball_x) {
        input = J_RIGHT;
      } else {
        input = J_LEFT;
      }
    }
  } 
  else if(int_distance(ball_y, y) >= 15) { // Not same Y, should jump or rotate
    if (y == FLOOR && int_distance(ball_x, ball_y) < 20) { // On the floor and close
      input = J_A;
    } else {
      if (ball_x > x) {
        input = input | J_RIGHT;
      } else {
        input = input | J_LEFT;
      }
    }
  }

  quadrant = calculate_ball_quadrant(x, y, ball_x, ball_y);

  if (rotation_quadrant(rot) == quadrant) {
    input = input | J_B;
  }

  return input;
}

Were you expecting more? It’s really quite simple, and is a very effective analogue for a human player. The second half should be fairly straightforward, as it’s just a determination of whether to drive, jump, or roll. The first part is the only new stuff, so we can walk through it.


CPU Prediction

In order for the car to give accurate key presses, it doesn’t need to know where the ball is now, but where it WILL BE when the car intercepts it. The first part of this function calculates the ball’s FUTURE position:

  UINT8 counter = 0;

  while (counter < CPU_PREDICTION) {
    tick_ball_physics(
      &ball_x, &ball_y, &ball_d_x, &ball_d_y,
      255, 255, 255, 255, 0,
      255, 255, 255, 255, 0,
      1 // skip collision checking
    );

    counter++;
  }

CPU_PREDICTION is a constant (as of Pocket League v0.1.1 it is set to 2) and determines the number of game ticks to look ahead. Without this, the ball would move from the spot it’s currently in before the car can get there.

The call to tick_ball_physics added a flag to skip collision checking with the other cars as it’s expensive to calculate all the hitboxes. It’s also not that important, so we skipped it. It still calculates bounces correctly.


Putting It All Together

Now that we know where the ball will be, the CPU car can simply determine whether it should try to drive to it, jump, or rotate while in the air. If the ball is in front of the car, even if it’s rotated, the car should boost towards it to hit it at max speed.

Note that we’re setting the input with bitwise OR (|) – This is important because we don’t want to overwrite the existing input! For instance, if we changed the code slightly:

  if (rotation_quadrant(rot) == quadrant) {
    // input = input | J_B;
    input = J_B;
  }

This car can no longer rotate AND boost at the same time! ORing the input with the new input ensures multiple key presses still work.


Limiting Control

There’s one last piece of the puzzle (for both humans and bots). There are cases where we would like to disallow input from either the player or the CPU car. These are:

  1. Before the countdown has finished, so that the cars wait til kickoff
  2. The CPU car should stop driving once a goal is scored

Let’s look at that code:

if (controls_enabled) {
  // Read the keys
  key2 = key1;
  key1 = joypad();

  if (CPU_DISABLED != 1) {
    cpu_key2 = cpu_key1;
    cpu_key1 = calculate_cpu_input(cpu_x_pos, cpu_y_pos, cpu_rot, ball_x_pos, ball_y_pos, ball_d_x, ball_d_y);
  }

  if (score_flag) { // Kill CPU controls after scoring
    cpu_key1 = 0x00;
    cpu_key2 = 0x00;
  }
}

Pretty simple. controls_enabled is false before the kickoff completes. And once a goal is made, score_flag is true, so we kill the CPU controls. Easy peasy. Note that you can still drive around as a human player!


Next Time

That’s all there is for this time, folks! Next time, I hope to have made progress on the sound part of the game. I’d also like to talk about how the game transitions from different screens, but it might not take a lot of explaining as the implementation is fairly simple.

I hope you’re continuing to enjoy the series, and thanks for reading!