Coding into the Void

Coding into the Void

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

Handling Inputs in Phaser 3: Part 5: Capturing the Mouse

Previously, we eschewed an ill-advised hypothetical and added support for axes in our input system.

This time, we’re coming for the mouse.1

A Mouse in the House

With controller support in Four Block Drop, I happily moved on to other projects.2 Then, this year, I had a revelation.

What if… mouse.

Driven by this divine mandate, I scurried back to work on my long-neglected first Phaser game.3 Could a mouse truly be used to control a video game? Only time would tell.4

It All Clicks

When we last added a new input type, the biggest difference was adding a new input provider.5 This time, all we need to do is add a new input provider. It’s all coming together.

If you remember back to the first post in the series, keyboard inputs could be captured by either callbacks or polling. Controller inputs could only be captured by polling. Mouse inputs can6 be captured by either callbacks or polling!7

However, unlike the keyboard, they don’t have any keys that you can listen on. They’re kind of a combination of the keyboard and mouse, in that way. And if there’s one thing we can handle, it’s a combination of two things we’ve already handled.

Without further ado, KHInputProviderMouse:8

enum KHMouseInput {
    Left,
    Right,
}

enum KHMouseAxis {
    X,
    Y,
    Scroll,
}

class KHInputProviderMouse {
    scene: Phaser.Scene;

    input: Map<MouseInput, KHInputKey>;
    axis: Map<MouseAxis, KHInputAxis>;
    /** 
     * Used to store intermediary state for updating scroll wheel. Used
     * because we don't get a callback for no scroll occurring, so we have to 
     * clear the value at the end of the frame.
     */
    wheelCallbackState: number;

    constructor(scene: Phaser.Scene) {
        super();
        this.scene = scene;
        this.input = new Map();
        this.input.set(MouseInput.Left, new KHInputKey());
        this.input.set(MouseInput.Right, new KHInputKey());

        this.axis = new Map();
        this.axis.set(MouseAxis.X, new KHInputAxis());
        this.axis.set(MouseAxis.Y, new KHInputAxis());
        this.axis.set(MouseAxis.Scroll, new KHInputAxis());

        this.scene.input.on('wheel',
                (pointer, currentlyOver, dx, dy, dz, event) => {
            this.wheelCallbackState = pointer.deltaY;
        });
    }

    update(now: number, delta: number) {
        const pointer = this.scene.input.activePointer;

        this._updateInput(MouseInput.Left, pointer.leftButtonDown());
        this._updateInput(MouseInput.Right, pointer.rightButtonDown());

        this._updateAxis(KHMouseAxis.X, pointer.x);
        this._updateAxis(KHMouseAxis.Y, pointer.y);

        // Ostensibly we could set scroll value from pointer.deltaY here, but
        // that has a strange habit of not getting cleared across frames. By
        // using the callback value and clearing it after reading it, we
        // can ensure it is cleared ourselves.
        this._updateAxis(KHMouseAxis.Scroll, this.wheelCallbackState);
        this.wheelCallbackState = 0;
    }

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

    private _updateAxis(axis: MouseAxis, value: number): void {
        this.axis.get(axis).update(value);
    }

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

    getAxis(axis: MouseAxis): KHInputAxis {
        return this.axis.get(axis);
    }
}

All in all, quite similar to how we did it for the controller. The sore thumb of the whole deal is the scroll wheel, whose deltaY value does not get cleared between frames. By listening on the callback,9 we can get the desired behavior.

For references, scrolling down gives a positive axis value, and scrolling up gives a negative axis value. Be on the lookout, as this might be platform dependent.

Fighting the Mouse

A funny thing about browsers: they’re primarily designed to be used with mice. If you want to use right click, you should disable it in the config:

const config = {
    ...
    disableContextMenu: true,
};

export const game = new Phaser.Game(config);

Once you set that option, Phaser will conveniently disable right clicks while the cursor is over the game window.

A similar issue occurs with the scroll wheel—it isn’t a good experience for the user to try to use the scroll wheel in your game and have it scroll the page as well. It’s an even worse experience if you disable scrolling on the whole page (unless there’s no other content).

Last time I checked, Phaser didn’t support similar behavior for disabling the scroll wheel inputs, but I had reasonable luck with the two bits of code:

// We want to block scrolling if mouse is over game area.
let phaser_game_block_scroll: boolean = false;
window.addEventListener("wheel", function(e){ 
    if (phaser_game_block_scroll) e.preventDefault(); 
}, {passive: false} );

...

create() {
    // This will block scrolling in this scene if mouse is over the game area.
    this.scene.input.on('gameout', function () {
        phaser_game_block_scroll = false;
    });
    this.scene.input.on('gameover', function () {
        phaser_game_block_scroll = true;
    });
}

These two bits of code should conditionally disable the scroll wheel events getting sent to the page when the mouse is over the game view. It doesn’t handle cases where the game starts up with the mouse on it, but that should be doable too.

A Different Type of Axis

Remember when I talked at length about how bad leaky abstractions are? Well, our mouse input provider class is full of them. Our mouse X and Y axes return the mouse cursor’s absolute coordinates,10 and the scroll wheel returns a value of 100, 0, or -100 from my experiments using Chrome on Windows with my mouse. This is a far cry from the [-1, 1] and [0, 1] axis ranges of the controller.

When using an axis, you must be aware of its range. The difference in input range is a hassle for unifying input, but it’s also a gift.11 The mouse can give you inputs that no other input type can. It can let you select a menu option at whim. It can set your character’s destination to another point on a map without holding a key!

If you’re going to support the mouse, don’t try to shove it into the box of a controller’s input, or a keyboard’s input. Let it do its own thing, or you might be better off not allowing mouse input at all.

Never Zero

Controller axes will rarely hit zero because of joystick drift. That’s easily accountable by adding a dead zone to inputs from the controller.12 Mouse inputs get a little more weird. It will only be zero if you’re in the top left of the screen, and even then, it’s not a guarantee.

When dealing with mouse position, you can’t treat a non-zero value as a sign of active user input. In this case, it’s helpful to check the KHInputAxis’s changed() value.

Ineffable Inputs

Our mouse input system has limitations. The most salient of these is that Phaser gives you callbacks when the pointer is over, out, down, and up on an interactable object. I most commonly use this in menus, but whole games can be built around these Phaser inputs. There’s probably a way to bang these into our input system, but I prefer to handle these separately. In my mind, the cost in complexity to figuring out how to do it properly and the ease of use of Phaser’s current system make it pointless to try to work around it.

If something doesn’t work with the input system, let it not work with the input system. Nothing needs to be everything.

Having the Touch

The good news is: Phaser’s pointer system covers both mouse input and touch input. If you just use the left mouse button input, you should get it for free. Multi-touch isn’t covered, but it should be possible by spinning up a new KHInputProviderMouse for each pointer,13 similar to how you can support multiple controllers. With enough effort, you could probably bang dragging and pinching into the system.

A Practical Example

As you’re working on your game, you might find out that having the absolute mouse X and Y coordinates isn’t really all that useful, especially since we’ve yielded the object interaction to Phaser proper. Here’s an example of how I used it in Four Block Drop to determine what column the mouse was over. To understand how the mouse acts in-game, it probably is best that you give it a shot first.

First, we need KHInputAxisProcessed, an axis that invokes a callback to set the value.

/**
 * An axis that is run through a lambda before being updated.
 */
class KHInputAxisProcessed extends KHInputAxisDerived {
    private source: KHInputAxis;
    private callback: (value: number) => number;
    
    constructor(inputSet: KHInputSet, source: KHInputAxis, 
            callback: (value: number) => number) {
        super(inputSet);
        this.source = source;
        this.callback = callback;
    }

    updateDerivedInput(now: number, delta: number) {
        this.update(this.callback(this.source.getValue()));
    }
}

I could have also created a specialty class for doing the modification code, but since it I’m not going to reuse it, I didn’t bother.

const columnWidth: number;
const playFieldX: number;

new KHInputAxisProcessed(inputSet, 
        mouseProvider.getAxis(KHMouseAxis.X), (value) => {
    let idx = Math.floor((value - playFieldX) / columnWidth);
    return idx;
}));

By doing that conversion, I turned an arbitrary value—the mouse x position—into a position my game logic could act on, the column to try to move to.

This is all great, except it should really only do that movement if you’re using the mouse. Otherwise, you’ll be fighting your mouse cursor while you’re using the controller. And the mouse will win.14

My first instinct was only to use the mouse input if it changed, which on first thought didn’t sound like a terrible idea. The problem is that it feels bad, since some events make it so you can move further to the left or right without any mouse input. To get it to move over in these cases, you’d have to jiggle your mouse, which feels unnatural.

The solution I came to was to inspect the last used input provider (updated when a button was pressed or an axis value changed), and only using the mouse input when the last input provider was the mouse.15

Another alternative is to only act on the mouse position when the left mouse button is pressed, like in old hack ‘n’ slash games like Diablo. It wouldn’t work in Four Block Drop, but did in The Appeasement of Cyclus, another one of my games.

It’s not a particularly difficult problem to overcome, but it is something to think about when setting up your inputs. After all, the only thing that really matters about your input system is how good it feels to use, not the elegance of the design.

Scroll On

We’ve reached the point where we have a more or less fully featured input system, capable of supporting pretty much every input system supported in the browser. It’s a nice milestone, and with that milestone, we’ve also reached the end of our main posts about creating the input system.

After this, it’ll move on to the appendices. The first one covers a scene that I spin up exclusively for input, and the tradeoffs of using that approach.


  1. Not the the mouse that the house is of. ↩︎

  2. Moving on from Four Block Drop was actually a result of trying to make my own interpreted language that could set the game rules. As often happens with a project of that scope, I lost interest fairly quickly. There is a bit of scripting in the engine currently, determining if the player has met the win condition. ↩︎

  3. This part is also a lie. I came back to add controller vibrations. That will probably be covered in an appendix. ↩︎

  4. The answer: yes. This is an example of the paradigm-shattering insights you come here for. ↩︎

  5. Well, that, and coming up with the input provider model. Oh, and adding support for axes. I guess derived inputs came from that as well. This will be simpler, despite the concept being more complex. ↩︎

  6. Drum roll, please. ↩︎

  7. Or at least, they should be able to. We’ll get to that later. ↩︎

  8. Miraculously, nothing is omitted this time. ↩︎

  9. Don’t remind me. ↩︎

  10. You could somewhat bang this into a normal thumbstick model by creating a derived deltaX axis, but that’s more suited to a first person game than anything I can think of that you could do in Phaser. ↩︎

  11. Admittedly, sometimes this gift just ends up being more convoluted code. ↩︎

  12. Still coming later. ↩︎

  13. You’ll need to modify the code to do so, but once again I inflict upon you an exercise left to the reader. ↩︎

  14. It is resolute. ↩︎

  15. ELTTR, or wait until I get to the appendix on supporting controller vibration. ↩︎