Coding into the Void

Coding into the Void

A blog that I’ll probably forget about after making thirty-one posts.

Handling Inputs in Phaser 3: Part 2: Gaining Control Over Controllers

Last post we went over my journey from having written short, concise code to handle inputs to writing an over-engineered bit of code that would handle the busywork for me, and how I adapted to modifying the input keys.

This time, we’re going to be discussing how I added controller support to Four Block Drop, and how it blew up my existing input paradigm.

Making the Buttons Work

The first thing to do if you want controller support is to add the following lines to your Phaser.Game’s config object1:

const config = {
    ...
    input: {
        gamepad: true,
    },
    ...
};

const game = new Phaser.Game(config);

Here is the API for the Gamepad. If you look you’ll notice a list of all the usual suspects on an Xbox 360+ controller: A, B, L1, etc., and that these values are only mostly booleans. In the end, there were five differences between how I was currently doing key inputs and how controller inputs were handled:

  1. Controller inputs only have the equivalent of down, with no justDown in sight.
  2. My input handler class takes in keyboard code references, not anything generic.
  3. You have to hold a reference to the gamepad to get access to the button value, instead of just registering for a key.
  4. L1 and R1 are axes,2 3 so treating them as booleans might be a mistake.
  5. There are actual axes on the controller, like the left stick and right trigger.

I had no plans to use the back triggers or thumbsticks, so I could ignore those pesky axes.4 Unfortunately, I did want to use the L1 and R1, but if you’re confident an axis’s value is either going to be zero or one and don’t care about the inbetween state, it’s easy to translate the value.

Difference four was trivial and five was irrelevant, leaving only the first three as problems I had to solve.

Evaluating the Problem

When you have two different structures that you want to unify, it’s important to first see if you can unify them well. You may be better off not abstracting them away if you abstraction ends up being too leaky.5

In this case, key presses and button presses emit the same underlying data: either down or not down. The keyboard’s justDown boolean is only Phaser helpfully maintaining state for you.

Since the similarities are vast and the differences are mostly fluff, it makes sense to unify the two APIs. If all the buttons were axes that provided a measure of how far down they were, it would be better to treat them differently and unify the inputs later.6

With that in mind, let’s take a look at the differences above.

Controller inputs only have the equivalent of down, with no justDown in sight.

This difference exposes two issues with the way we’re currently handling input:

  1. The lack of justDown, which I use extensively in my game code.
  2. Keys are updated by Phaser every frame, which is used to compute justDown. Since button code doesn’t have that, I will need to add support for it myself.

Clearly, we’ll need to update the controller references every frame and add code that adds our own justDown. This code needs to be run before our code that reads the input, or it will get stale data.

My input handler class takes in keyboard code references, not anything generic.

There are two solutions to this: change the input handler to take in a dictionary of { inputName: key } and { inputName: button } pairs,7 or provide one class to wrap them both.

You have to hold a reference to the gamepad to get access to the button value, instead of just registering for a key.

This throws a bit of a wrench in the works, as I could just pass around keys willy-nilly, and references to buttons would need to also contain a reference to their controller.

Finding the Key

Considering the analysis above, it seems fairly clear what the solution is. A simple class, KHInputKey,8 which can wrap both keyboard keys and buttons. This class will handle justDown itself, which both provides the justDown flag for the controller and makes justDown true for the entirety of the frame that it’s pressed in.

/**
 * Input key that specifies the state of one key. Managers of this key must
 * update it once and only once per frame, or strange behavior can occur.
 */
class KHInputKey {
    protected down: boolean;
    protected justDown: boolean;
    
    constructor() {
        this.down = false;
        this.justDown = false;
    }

    update(down: boolean): void {
        let justDown = down && !this.down;
        this.updateInternal(down, justDown);
    }

    protected updateInternal(down: boolean, justDown: boolean): void {
        this.justDown = justDown;
        this.down = down;
    }

    isDown(): boolean {
        return this.down;
    }

    isJustDown(): boolean {
        return this.justDown;
    }
}

With this new class, we can replace the previous map of {string: Phaser.Input.Keyboard.KeyCodes} with a map of {string: KHInputKey}, and we’re done. Right? Well, not quite.

We’ve now made it so that keyboard keys also need to be updated each frame. Progress, of a sort. Here’s what the keyboard key implementation looks like:

class  KHInputKeyKeyboard extends KHInputKey {
    private key: Phaser.Input.Keyboard.Key;

    constructor(key: Phaser.Input.Keyboard.Key) {
        this.key = key;
    }

    updateFromKey(): void {
        this.update(this.key.isDown);
    }
}

Just call updateFromKey() each frame and it’s happy. Keen eyes will notice that this is different from the contract laid out above (calling update(down)), implying that it will be handled differently. Before we cover that, let’s look at how we handle it for controller buttons.

Without recreating the concept of a button, we can’t mimic the updateFromKey() concept, but doing that would result in us creating two layers of button wrappers. Instead, lets come to terms with a cold, hard truth of the Phaser gamepad API: we cannot update a button without having a reference to its controller.

We have two options: have a controller class with references to its buttons, or have buttons each hold a reference to its controller. In the interest of brevity and less passing around of controller options, I chose the former approach. Introducing the KHInputProviderController9:

enum KHPadInput {
    Left,
    Right,
    Up,
    L1,
}

class KHInputProviderController {

    scene: Phaser.Scene;
    buttons: Map<KHPadInput, KHInputKey>;
    gamePadNumber: number;
    private connected: boolean;

    constructor(scene: Phaser.Scene, gamePadNumber: number) {
        super();
        this.scene = scene;

        this.gamePadNumber = gamePadNumber;

        this.buttons = new Map();
        this.buttons.set(KHPadInput.Left, new KHInputKey());
        this.buttons.set(KHPadInput.Right, new KHInputKey());
        this.buttons.set(KHPadInput.Up, new KHInputKey());
        this.buttons.set(KHPadInput.L1, new KHInputKey());
    }

    protected updateConnect() {
        this.connected = true;
    }

    // Disconnecting a controller should zero out all inputs, so no phantom 
    // inputs exist in perpetuity.
    updateDisconnect() {
        if (!this.connected) {
            return;
        }
        this.connected = false;
        this.buttons.forEach((value, key) => {
            value.update(false);
        })
    }

    private _getGamepad() {
        if (!this.scene.input.gamepad || this.gamePadNumber 
            >= this.scene.input.gamepad.gamepads.length) {
            this.updateDisconnect();
            return;
        }
        let gamePad  = this.scene.input.gamepad.gamepads[this.gamePadNumber];
        if (!gamePad) {
            this.updateDisconnect();
            return;
        }
        
        this.updateConnect(gamePad);
        return gamePad;
    }

    update(now: number, delta: number) {
        let gamePad = this._getGamepad();
        if (!gamePad) return;

        this._updateInput(KHPadInput.Left, gamePad.left);
        this._updateInput(KHPadInput.Right, gamePad.right);
        this._updateInput(KHPadInput.Up, gamePad.up);
        this._updateInput(KHPadInput.L1, gamePad.L1 > 0.9);
    }

    private _updateInput(input: PadInput, value: boolean) {
        this.buttons.get(input).update(value);
    }

    getInput(input: PadInput): KHInputKey {
        return this.buttons.get(input);
    }
}

Now in order to get a KHInputKey for a controller button, you ask the controller provider for a reference to its button input. As long as you call update() on the provider each frame before you grab inputs from it, everything’s great.

You can also see that I worked around the L1 button being an axis by checking if its value was greater than 0.9 or not. These axes go from 0-1, so 0.9 feels like a safe bet for considering it pressed, even if it does end up being backed by an axis.

It may feel like overkill to have a separate method for updating inputs, given that it’s a one-liner, but that method will be fleshed out in a later post.10

That’s the controller infrastructure sorted, so let’s get back to the problem with the keyboard inputs not being updated. The answer, as you may have guessed, is another provider, this time for the keyboard:

class KHInputProviderKeyboard {
    scene: Phaser.Scene;
    keys: Map<any, KHInputKeyKeyboard>;

    constructor(scene: Phaser.Scene) {
        super();
        this.scene = scene;
        this.keys = new Map();
    }

    update(now: number, delta: number) {
        this.keys.forEach((value, key) => {
            value.updateFromKey();
        })
    }

    // The any in the signature is because Phaser.Input.Keyboard.KeyCodes is 
    // a namespace and cannot be assigned as a type (as far as I know).
    getInput(input: any): KHInputKey {
        if (!this.keys.has(input)) {
            const phaserKey = this.scene.input.keyboard.addKey(input);
            const inputKey: KHInputKeyKeyboard =
                new KHInputKeyKeyboard(phaserKey, this);

            const justDown = Phaser.Input.Keyboard.JustDown(phaserKey);
            const down = phaserKey.isDown;

            // Populate key isDown.
            if (down) {
                inputKey.update(down);
            }
            // Clear out justDown if it isn't just down.
            if (!justDown) {
                inputKey.update(down);
            }

            this.keys.set(input, inputKey);
        }
        const value = this.keys.get(input);
        return value;
    }
}

A few differences between the implementations: the keyboard lazily adds keys, whereas the controller adds them greedily, and the keyboard doesn’t handle a disconnect event like the controller does. Notably, these don’t have any difference as to how the consumer of the resulting KHInputKey functions.

Here’s what our input handling code looks like now11:

controller: KHInputProviderController;
keyboard: KHInputProviderKeyboard;

create(): void {
    this.controller new KHInputProviderController(this, 0);
    this.keyboard = new KHInputProviderKeyboard(this);

    this.inputHandler = new FBDInputHandler(this, {
        'left': [
            this.keyboard.getInput(Phaser.Input.Keyboard.KeyCodes.LEFT),
            this.keyboard.getInput(
                Phaser.Input.Keyboard.KeyCodes.NUMPAD_FOUR),
            this.controller.getInput(KHPadInput.Left),
        ],
        'right': [
            this.keyboard.getInput(Phaser.Input.Keyboard.KeyCodes.RIGHT), 
            this.keyboard.getInput(
                Phaser.Input.Keyboard.KeyCodes.NUMPAD_SIX),
            this.controller.getInput(KHPadInput.Right),
        ],
        'up': [
            this.keyboard.getInput(Phaser.Input.Keyboard.KeyCodes.UP),
            this.keyboard.getInput(
                Phaser.Input.Keyboard.KeyCodes.NUMPAD_EIGHT),
            this.controller.getInput(KHPadInput.Up),
        ],
    });
}

update(now: number, delta: number): void {
    this.controller.update(now, delta);
    this.keyboard.update(now, delta);

    this.directionalComputer.update(
        now, 
        this.inputHandler.isJustDown('left'), 
        this.inputHandler.isDown('left'), 
        this.inputHandler.isJustDown('right'),
        this.inputHandler.isDown('right'));
    if (this.directionalComputer.pressLeft) {
        // do left code
    }
    if (this.directionalComputer.pressRight) {
        // do right code
    }
    if (this.inputHandler.isJustDown('up')) {
        // do up code
    }
}

We added a few new objects that we have to manage, but we now have controller support with no changes to our input method. Altogether, it felt like a win, but I couldn’t help but look at that directional computer code and wonder if we could replace it with a new kind of KHInputKey, one that is derived from other inputs.

In the next post, we’ll explore how we can create derived inputs, and the tradeoffs that ensue.


  1. I missed this step, and was banging my head against the wall trying to get controller support to work. When I went to a friend’s house for brunch, the solution popped into my head while I was boring them with a recount of my struggles. This was on 30 December 2019, and I had no idea that in a few months going to a friend’s place for a meal would be out of the question. ↩︎

  2. The plural of axis. Thankfully I haven’t wanted to put an axe through my code quite yet. ↩︎

  3. There must be some reason for this decision. Either some controllers do treat them like axes, or it’s just future proofing. ↩︎

  4. A later post will discuss supporting these. It’s not complicated as long as you have the infrastructure set up. ↩︎

  5. A leaky abstraction is an abstraction that leaks some of the details of its implementation. For instance, imagine a 2D array on a system that punished non-sequential memory accesses. Since a 2D array is a 1D array under the covers, iterating across the x and then the y component would be far more efficient than iterating across the y and then the x component of the array. Knowing this would be a requirement for making performant software on that system. Here's where Joel Spolsky coined the term. ↩︎

  6. The PS2’s DualShock 2 controller had pressure-sensitive face buttons. If you wanted to handle these correctly, you probably wouldn’t merge the two. I’m not sure if XInput supports such behavior. ↩︎

  7. My first approach, which is omitted since it’s an intermediary step that isn’t very interesting. ↩︎

  8. Probably poorly named, since it covers both keyboard keys and controller buttons, but all these names are overloaded so it’s challenging to come up with precise terminology. ↩︎

  9. Inputs simplified for brevity. The only unexpected part omitted is that start and select aren’t directly mapped to inputs, so they have to be set using gamePad.getButtonValue(9) and gamePad.getButtonValue(8), respectively (this might be different in DirectInput, if browsers support it). They also come in as an axis, so you will need to map them to a boolean value. ↩︎

  10. This one. ↩︎

  11. Modifications to support FBDInputHandler to take in KHInputKey instead of Phaser Keycodes are not shown, but should be easy enough to do on your own. Similarly, you may wish to have multiple controller input providers, one for each controller that can be plugged in. ↩︎