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 3: Deriving Input

Last post we went over my journey from reading keyboard inputs to adopting a unified system that supported both keyboard and controller inputs transparently.

This time, we’re going to discuss how to abstract logic away in the keys, further simplifying the reading of input in our update loop. This is a pathway to many abilities, some considered to be unnatural.1

Starting Simple

What if there was a way to modify our input handler to only accept one key instead of multiple? Right now it looks like this:

class FBDInputController {
    ...

    isJustDown(key): boolean {
        for (let i = 0; i < this.mappings[key].length; i++) {
            if (Phaser.Input.Keyboard.JustDown(this.mappings[key][i])) 
                return true;
        }
        return false;
    }

    isDown(key): boolean {
        for (let i = 0; i < this.mappings[key].length; i++) {
            if (this.mappings[key][i].isDown) return true;
        }
        return false;
    }
}

This code resembles something a little bit, doesn’t it? It kind of looks like the code of KHInputKey. What if we made code that would functionally ‘or’ all these inputs together? Let’s call it KHInputKeyOr. It looks fairly straight-forward:

class KHInputKeyOr extends KHInputKey {
    private sources: KHInputKey[];

    constructor(sources: KHInputKey[]) {
        super(inputSet);
        this.sources = sources;
    }

    updateDerivedInput() {
        let anyDown: boolean = false;

        for (let i = 0; i < this.sources.length; i++) {
            if (this.sources[i].isDown()) {
                anyDown = true;
                break;
            }
        }

        super.update(anyDown);
    }
}

Fixing the Bugs

Immediately we run into a couple different issues. First of all, similar to how we handled keyboard inputs in the previous post with updateFromKey(), it doesn’t make sense for it to be updated by a call to update(boolean). It manages its own state, so we instead expect whoever updates it to call updateDerivedInput().

This feels similar to our input provider model, but it doesn’t behave similarly in any other way. We can just stick it in a list of derived inputs for now.

The other, more insidious, issue is that we have now made the input key update order matter matter.

Derived inputs must be updated after the inputs that they derive from. Otherwise, they will read inputs that could be a combination of current and one frame old, introducing strange, hard to track down, input bugs into your code.

This is further compounded by the fact that KHInputKeyOr, being a subclass of KHInputKey, can also be an input to another KHInputKeyOr.2 Fortunately, as long as we update the keys in creation order (let’s call it creation order consistency), we can avoid any issues with reading stale inputs.

Instead of having KHInputKeyOr directly extend KHInputKey, let’s slot in an abstract class between: KHInputKeyDerived.

abstract class KHInputKeyDerived extends KHInputKey {
    constructor(inputSet: KHInputSet) {
        super();
        if (inputSet) {
            inputSet.addInput(this);
        }
    }

    abstract updateDerivedInput(now: number, delta: number): void;
}

KHInputSet is how we guarantee that we always get inputs in the correct order, with minimal boilerplate3:

interface KHIDerivedInput {
    updateDerivedInput();
}

class KHInputSet {
    readonly inputs: KHIDerivedInput[];

    constructor(inputs: KHIDerivedInput[] = []) {
        this.inputs = inputs;
    }

    addInput(input: KHIDerivedInput) {
        this.inputs.push(input);
    }

    update(now: number, delta: number) {
        for (let i = 0; i < this.inputs.length; i++) {
            this.inputs[i].updateDerivedInput(now, delta);
        }
    }
}

By registering a derived input with our input set on creation, we avoid race conditions. Since all KHInputKeys provided in the constructor must be instantiated before this object is instantiated, the order is correctly preserved.4

Our code now looks like this5:

controller: KHInputProviderController;
keyboard: KHInputProviderKeyboard;
inputSet: KHInputSet;

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

    this.inputHandler = new FBDInputHandler(this, {
        'left': new KHInputKeyOr(this.inputSet, [
            this.keyboard.getInput(Phaser.Input.Keyboard.KeyCodes.LEFT),
            this.keyboard.getInput(
                Phaser.Input.Keyboard.KeyCodes.NUMPAD_FOUR),
            this.controller.getInput(KHPadInput.Left),
        ]),
        'right': new KHInputKeyOr(this.inputSet, [
            this.keyboard.getInput(Phaser.Input.Keyboard.KeyCodes.RIGHT), 
            this.keyboard.getInput(
                Phaser.Input.Keyboard.KeyCodes.NUMPAD_SIX),
            this.controller.getInput(KHPadInput.Right),
        ]),
        'up': new KHInputKeyOr(this.inputSet, [
            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.inputSet.update(now, delta);

    ...
}

You’ll notice that the declaration code is getting chunkier and chunkier. That’s one of the tradeoffs of this approach: it favors instantiation complexity over update complexity in the scene. Performance is theoretically a concern, but in my tests I hadn’t noticed any major issues.6

Getting to the Point

With our or-ed input key in place, we’ve come alarmingly close to the goals we went down this path to accomplish in the first place: making a KHInputKey that repeats and one that competes between two others.

In order to have repeating keys, we need some concept of time. Go back and add a reference to now and delta to updateDerivedInput() so that it looks like updateDerivedInput(now: number, delta: number).

First, let’s take a stab at KHInputKeyCompeting. As inputs, we need to be able to get isDown and isJustDown from both the primary key and the key it’s competing with. We also need to know which key is preferred if they’re both pressed in the same frame. With all that, it’s just a matter of making code similar to that which we had in the directional computer, all the way back in the first blog post.

class KHInputKeyCompeting extends KHInputKeyDerived {
    private source: KHInputKey;
    private competing: KHInputKey;
    private lastDown: KHInputKey;
    private inputIsPreferred: boolean;
    
    constructor(inputSet: KHInputSet, source: KHInputKey, 
            competing: KHInputKey, inputIsPreferred: boolean) {
        super(inputSet);
        this.source = source;
        this.competing = competing;
        this.inputIsPreferred = inputIsPreferred;
    }

    updateDerivedInput() {
        // This is more explicit than the code in the directional computer, as
        // we don't have a single reference to the preferred input here. It's
        // easier, and more readable, to just check for both here.
        if (this.source.isJustDown() && this.competing.isJustDown()) {
            this.lastDown = this.inputIsPreferred ? 
                this.source : this.competing;
        } else if (this.source.isJustDown()) {
            this.lastDown = this.source;
        } else if (this.competing.isJustDown()) {
            this.lastDown = this.competing;
        }

        // If the lastDown key is released, the other key should be activated.
        if (this.lastDown && !this.lastDown.isDown()) {
            if (this.source.isDown()) this.lastDown = this.source;
            else if (this.competing.isDown()) this.lastDown = this.competing;
            else this.lastDown = null;
        }

        this.update(this.lastDown == this.source && this.lastDown.isDown());
    }
}

And, now that we have a competing input, let’s try to make it repeat. This is where we’ll use the time that we passed into updateDerivedInput.

class KHInputKeyRepeating extends KHInputKeyDerived {
    private key: KHInputKey;
    private initialDelay: number;
    private incrementalDelay: number;
    private downTime: number;
    private lastTime: number;
    
    constructor(inputSet: KHInputSet, key: KHInputKey, 
            initialDelay: number, incrementalDelay: number) {
        super(inputSet);
        this.key = key;
        this.initialDelay = initialDelay;
        this.incrementalDelay = incrementalDelay;
    }

    updateDerivedInput(now: number) {
        if (this.key.isJustDown()) {
            this.downTime = now;
            this.lastTime = now;
            this.updateInternal(true, true);
            return;
        } else if (!this.key.isDown()) {
            this.updateInternal(false, false);
            return;
        }

        let lastDiff = this.lastTime - this.downTime;
        let diff = now - this.downTime;
        let justDown = false;

        // Initial delay elapsed this frame.
        if (this.isBetween(lastDiff, this.initialDelay, diff) {
            justDown = true;
        }
        // Could be a clause on the previous if, separated for clarity.
        // Incremental delay elapsed this frame.
        else if(diff > this.initialDelay && this.looped(
                    lastDiff - this.initialDelay, 
                    this.incrementalDelay,
                    diff - this.initialDelay))) {
            justDown = true;
        }

        this.updateInternal(true, justDown);
        this.lastTime = now;
    }

    // Returns true if val is between num1 and num2.
    isBetween(num1: number, val: number, num2: number) {
        return num1 < val && val <= num2;
    }

    // Returns true if num1 has is a higher integer multiple of repeatVal than
    // num2.
    looped(num1: number, repeatVal: number, num2: number) {
        let loop1 = num1 / repeatVal;
        let loop2 = num2 / repeatVal;
        let ceilLoop1 = Math.ceil(loop1);
        return loop2 >= loop1 + 1 
            || (loop1 < ceilLoop1 && ceilLoop1 <= loop2);
    }
}

And there’s our repeating input. Some of the code that I’m using to detect where it repeats is a bit esoteric, but it’s just trying to find out where I’m overlapping with a loop point without losing the overage (i.e. trying to make sure that if I’m looping once every 100 ms, and it’s been 125 ms since the last loop, I only require 75 ms more for the next iteration).

If you go back and compare it to FBDDirectionalComputer class, you’ll find it more readable.7

There’s a lot of potential here: for instance, I have derived inputs that I can be reset externally to require that a key be released in order to be pressed again, ones that detect a tap or hold, and ones that take in a callback.

Putting it All Together

Without further ado, here’s our final input code.

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

    const left = new KHInputKeyOr(this.inputSet, [
        this.keyboard.getInput(Phaser.Input.Keyboard.KeyCodes.LEFT),
        this.keyboard.getInput(Phaser.Input.Keyboard.KeyCodes.NUMPAD_FOUR),
        this.controller.getInput(KHPadInput.Left),
    ]);

    const right = new KHInputKeyOr(this.inputSet, [
        this.keyboard.getInput(Phaser.Input.Keyboard.KeyCodes.RIGHT), 
        this.keyboard.getInput(Phaser.Input.Keyboard.KeyCodes.NUMPAD_SIX),
        this.controller.getInput(KHPadInput.Right),
    ]);

    this.inputHandler = new FBDInputHandler(this, {
        'left': new KHInputKeyRepeating(
            this.inputSet, 
            new KHInputKeyCompeting(this.inputSet, left, right, false),
            100,
            25
        ),
        'right': new KHInputKeyRepeating(
            this.inputSet, 
            new KHInputKeyCompeting(this.inputSet, right, left, true),
            100,
            25
        ),
        'up': new KHInputKeyOr(this.inputSet, [
            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.inputSet.update(now, delta);

    if (this.inputHandler.isJustDown('left')) {
        // do left code
    }
    if (this.inputHandler.isJustDown('right')) {
        // do right code
    }
    if (this.inputHandler.isJustDown('up')) {
        // do up code
    }
}

Once again, we’ve decreased the complexity of our update loop, while increasing the complexity of our input handler creation. It also has the pleasant side effect of decoupling our input logic from the game logic itself, easily allowing us to customize inputs without touching the game code.

In the end, our update code looks almost identical to our first iteration, way back in the original blog post. Almost makes it seem futile, until you notice that you’ve gotten the complex input and controller support you wanted.

Next time, we’ll be working on adding axis support.


  1. You can probably take this too far, but it is a really useful way to handle disparate forms of input. Code thoughtfully when you use these. ↩︎

  2. I struggle to think of a good reason to do this specifically, but it becomes ubiquitous once you have other sorts of derived inputs. ↩︎

  3. You can treat KHIDerivedInput as if it were KHInputKeyDerived for now. Taking the interface in as an argument simplifies the code later. ↩︎

  4. Replacing inputs on a derived inputs after creation is dangerous, as it can result in creation order consistency being violated. Don’t do it, but if you have to, make sure that you’re adding inputs created earlier than the input you’re adding it to. You should treat all input sources as read-only. ↩︎

  5. Once again, code is omitted. In this case, code updating the input handler to potentially take a single input. I didn’t actually do it: mine can take in an array or single object. Also omitted: changing KHInputKeyOr to subclass KHDerivedInput and take in an input set in the constructor. ↩︎

  6. When testing with over 300 derived inputs of moderate complexity, the amount time processing the inputs was always under a millisecond. It could vary, but I’d wait until it becomes a problem before trying to optimize. Additionally, if you have that many derived inputs, you’re probably doing something wrong. ↩︎

  7. I hope. Otherwise, what am I wasting my time for? ↩︎