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: Appendix C: Good Vibrations

Previously, we spent far too much time going over dead zones.

This time, we’re adding vibration support.1 I’d suggest re-reading the post that goes over input providers, since we’ll be working with them almost exclusively in this post.

Feeling the Vibes

There’s good news and bad news for vibration support in the browser.

The good news:

  • It’s really easy to use the playEffect API, and it seems fairly flexible.

The bad news:

  • playEffect isn’t a cross-browser API standard (only Chromium-based browsers support it).
  • All of the APIs are marked as experimental
  • pulse, which MDN calls the standard (and the only one with documentation), is theoretically only supported in Firefox, and even, then, it wouldn’t work for me.
  • pulse is worse than playEffect, so if they do standardize to it, the rumble control will get worse.
  • Safari doesn’t support vibration at all.

Some more good news:

  • My implementation theoretically supports pulse correctly, so if it does get fixed, it should just work.

Our Goals

Of course, we want to get the controller to vibrate. But we also only want it to vibrate sometimes. Have you ever played a game that rumbled the controller, regardless of whether you were using it or not? I have. It’s startled me many times.

To avoid that, we should vibrate only the input that’s actively giving inputs. Since using multiple input types at once is rare,2 we can just vibrate the last input source. Since we have a class that matches input sources already (KHInputProvider), we can piggy-back off of that. We could do something specific to controllers, but that would most likely end up making our code messier, and for all we know keyboards could add vibration support in the future.3

Let’s list our goals:

  1. Be able to get the last used input provider.
  2. Add vibration support to the applicable input providers.

Getting the Last Used Input Provider

Do you remember way back when I was first discussing input providers, and how I added methods for updateInput and updateAxis, even though they were only one line long? This is why.

First, let’s introduce a parent class to the input providers:

class KHInputProvider {
    static lastInputSource: KHInputProvider;
}

For now, this parent class will just contain a single static variable4: lastInputSource, which will hold a reference to the last input provider that received activity.

Now we need to update this variable. First, let’s amend the _updateAxis and _updateInput methods in the controller and mouse providers, since they both handle their own inputs.

class KHInputProviderController extends KHInputProvider {
    ...
    private _updateInput(input: KHPadInput, value: boolean) {
        this.buttons.get(input).update(value);
        if (value) {
            KHInputProvider.lastInputSource = this;
        }
    }

    private _updateAxis(axis: KHPadAxis, value: number) {
        if (this.axis.get(axis).update(value)) {
            KHInputProvider.lastInputSource = this;
        }
    }
}

The only difference between the two is the enum in the input methods.

class KHInputProviderMouse extends KHInputProvider {
    ...
    private _updateInput(input: KHMouseInput, value: boolean): void {
        this.input.get(input).update(value);
        if (value) {
            KHInputProvider.lastInputSource = this;
        }
    }

    private _updateAxis(axis: KHMouseAxis, value: number): void {
        if (this.axis.get(axis).update(value)) {
            KHInputProvider.lastInputSource = this;
        }
    }
}

As you can see from the code, the providers will set themselves as the last input source under two conditions:

  1. A button was pressed or is currently being held. This indicates active input.
  2. The value of an axis changed. We can’t check against the axis value being zero, as the mouse x and y axes will usually be non-zero at rest. Since all of the other axes require you to hold them and they have a relatively high amount of granularity, their values will likely change from frame to frame.

Now we get to the keyboard input provider, which gives us two options:

  1. Pass the input provider into each KHInputKeyKeyboard object, and set it when its updateFromKey() method is called. This makes it a chunkier class, and makes both objects have a reference to each other, which I’m always cagey about.5
  2. Check the value of each key after calling updateFromKey(), and update the input source from the input provider.

I’m going to take the second approach, if only because it keeps the KHInputKeyKeyboard class slimmer and makes all lastInputSource updates take place in input provider classes. The changes for this are also minimal.

class  KHInputKeyKeyboard extends KHInputKey {
    ...

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

updateFromKey() is changed to return its current down state.

class KHInputProviderKeyboard extends KHInputProvider {
    ...

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

update() is changed to set the last input source if updateFromKey() returns true.

With this change, we now have the last input source hooked up correctly.

Adding Vibration Support

Before we hook in the APIs, let’s make it so input providers can try to vibrate. First of all, we’ll add a method to KHInputProvider that requests a vibration.

class KHInputProvider {
    static lastInputSource: KHInputProvider;

    /**
     * Attempt to vibrate the input source.
     * @param duration Duration in millis
     * @param weak Magnitude of weak rumble (0-1)
     * @param strong Magnitude of strong rumble (0-1)
     */
    tryVibrate(duration: number, weak: number, strong: number) {
        // Default is no implementation.
    }
}

Before we get around to adding vibration support, let’s look at the two vibration APIs. Both of them exist on the GamepadHapticActuator API.

playEffect()

The playEffect() API seemingly was proposed here, which is also the best documentation I could find as to how it works. It takes in a string defining the input type (as of 2021-02-01, just “dual-rumble”), and a dictionary defining the effect type. The dictionary for “dual-rumble” contains the start delay, duration, magnitude of the weak motor, and magnitude of the strong motor.

All Chromium browsers support playEffect(), and no others do. The API is marked on MDN as non-standard and experimental, which is quite the winning combination. It’s been around for over two years now, so I wouldn’t call it bleeding edge.

pulse()

The pulse() API is defined here. It’s more restrictive, as it only allows the user to define the duration and a generic magnitude. I’m unsure whether the magnitude would trigger the weak rumble, the strong rumble, or both, as I’ve been unable to get the API to work.

This API is only supported by Firefox, and I’ve been unable to get it to work there either (once again, as of 2021-02-01). We’ll still try to integrate it. What’s the worst that could happen from putting in a reference to an API that you can’t test that sits in your code base like a ticking time bomb until they enable it again?6

Without further ado, let’s add some vibrations.

class KHInputProviderController {
    ...

    tryVibrate(duration: number, weak: number, strong: number) {
        let gamepad = this._getGamepad();
        if (!gamepad) {
            return;
        }
        let haptics = gamepad.vibration;
        if (haptics) {
            // playEffect is more flexible, so attempt to use that first
            if ((haptics as any).playEffect) {
                (haptics as any).playEffect('dual-rumble', {
                    startDelay: 0,
                    duration: duration,
                    weakMagnitude: weak,
                    strongMagnitude: strong,
                });
            } else if (haptics.pulse) {
                let strength = Math.max(weak, strong);
                haptics.pulse(strength, duration);
            }
        }
    }
}

We start by getting a reference to the gamepad. The reference can, of course, be null (controllers can be detached at any time), so we bail out in that case.

Phaser is kind enough to provide a reference to the gamepad’s GamepadHapticActuator class through gamepad.vibration, and we’ll verify that that reference is non-null as well.7

Since playEffect() is non-standard, typescript gets angry if we try to access the method directly. By casting it to any first, we avoid all the type-checking (usually a bad thing, but the only option I’m aware of here). We verify that the method exists, since, once again, it’s non-standard and experimental, and call it if it does.

If playEffect() doesn’t exist, we fall back to pulse(). I did this for two reasons: playEffect() actually works, and I like its behavior better.

I take the highest of the two magnitudes, and pass it in as the pulse strength. We can’t pass a start delay into tryVibrate() because pulse() doesn’t support it.

Let’s Rumble

I’ve told you this all works, but let’s take a go at it. Left mouse click, keyboard enter, and the bottom button on a gamepad’s face (A on Xbox, for instance) will all trigger the game to try to vibrate. It should only actually vibrate if you press the button on a controller that can vibrate. Test it out8

End of Input

If you want to see the source code for the vibration demo, it’s in the repository here.

Well, you’ve made it. I’ve exhausted all I have to say about in inputs in Phaser 3.9 I enjoyed writing these posts, so I hope something new and exciting will happen to pull me back.


  1. Only for Chromium-based browsers, unfortunately. I wasn’t able to get Firefox’s API to work, and Safari doesn’t support the functionality. ↩︎

  2. Well, rare between a controller and another input. We might need to rethink it if mice had vibration support—maybe a combination of the last input source and all input sources used in the last X seconds. ↩︎

  3. I could see it happening. Imagine a terrible, completely flat keyboard that used haptics to guide your fingers. ↩︎

  4. The usual warnings regarding static fields exist, like the instance potentially being held on after all other references have gone away. In this case, it shouldn’t be a problem. If the input scene is used, the input providers last for the whole game lifecycle, and if they’re being recreated in each scene, they’ll likely be displaced by a new source shortly anyway. ↩︎

  5. I’m fairly certain that javascript’s garbage collector would catch this, but I’m used to working with Objective-C, where retain cycles were an easy way to lose memory. Since I’m using the input scene, the lifecycle of the input provider is the game lifecycle, so it’s not a concern. ↩︎

  6. A rhetorical question. ↩︎

  7. I don’t have access to test, but I wouldn’t be surprised if the property was undefined on unsupported browsers. ↩︎

  8. Remember, Chromium only, unless the Firebox pulse time bomb activated. ↩︎

  9. For now. ↩︎