Handling Inputs in Phaser 3: Appendix A: Input Scene
Last time, we explored supporting mouse input and wrapped up the main part of the series.
This time, we’re exploring the dark arts: having multiple scenes running at once.1
Making a Scene
This whole series I’ve been lying to you. I’ve led you to believe that I updated the input set in each update loop, dutifully relying on re-registering the input handler’s inputs every time I came back from a pause to avoid Phaser’s key input difficulties.2
Why did I keep this mysterious tool secret from you? I don’t know if it actually works better than using the workaround above.
So keep this in mind as something that you can do, but shouldn’t feel obligated to do. Now that I’ve warned you of the futility of introducing the code, let’s get into it.
interface KHIInputRegistrar {
registerInputSet(inputSet: KHInputSet);
unregisterInputSet(inputSet: KHInputSet);
}
class KHInputScene extends Phaser.Scene implements KHIInputRegistrar {
static SharedInput: KHInputScene;
controllerProviders: KHInputProviderController[];
keyboardProvider: KHInputProviderKeyboard;
mouseProvider: KHInputProviderMouse;
registeredDerivedInputs: KHIDerivedInput[];
registeredInputSets: Set<KHInputSet>;
constructor() {
super("KHInputScene");
this.registeredInputSets = new Set();
KHInputScene.SharedInput = this;
}
registerInputSet(inputSet: KHInputSet) {
this.registeredInputSets.add(inputSet);
}
unregisterInputSet(inputSet: KHInputSet) {
this.registeredInputSets.delete(inputSet);
}
create() {
this.controllerProviders = [
new KHInputProviderController(this, 0),
new KHInputProviderController(this, 1),
new KHInputProviderController(this, 2),
new KHInputProviderController(this, 3)
];
this.keyboardProvider = new KHInputProviderKeyboard(this);
this.mouseProvider = new KHInputProviderMouse(this);
this.events.on('preupdate', (now, delta) => {
this.updateInputs(now, delta);
}, this);
}
private updateInputs(now: number, delta: number): void {
// Update mouse inputs first so that bumping the mouse while holding
// a button doesn't trigger mouse movement.
this.mouseProvider.update(now, delta);
for (let i = 0; i < this.controllerProviders.length; i++) {
this.controllerProviders[i].update(now, delta);
}
this.keyboardProvider.update(now, delta);
// Registered derived inputs must be iterated in the order that
// they're registered, as this guarantees that a derived input's
// inputs will be updated before it is, preventing extra input
// latency.
this.registeredInputSets.forEach(function(value) {
value.update(now, delta);
})
}
}
Because this is the first time I’ve specifically instantiated one, this is what a scene looks like. Its intent is to manage all the inputs to the game (barring mouse-over events). If you want an input provider, you get it from here.
First off, let’s start the scene. As usual, you start it using the following command, preferably in the first scene that starts up, in such a way that it will be called once3:
this.scene.run("KHInputScene");
After that, you can get the scene anywhere by calling KHInputScene.SharedScene.4
Something’s Leaking, and It’s Not the Abstractions
Most of the code should be fairly straightforward, but there’s one part that may pop out to you more than the rest: the input registrar code.
registerInputSet(inputSet: KHInputSet) {
this.registeredInputSets.add(inputSet);
}
unregisterInputSet(inputSet: KHInputSet) {
this.registeredInputSets.delete(inputSet);
}
When introducing the derived inputs, I created the concept of an input set, which derived inputs add themselves to. It bundled up these sets of derived inputs so that the update()
method would update them. When I first implemented derived inputs, these inputs were instead added directly to the input scene, which would directly iterate over them.
Every time the scene would change, that scene would register its own derived inputs directly with the singleton. The array of inputs would grow even longer with each scene change.
You can see the problem. These inputs were added, and they were never removed.
This resulted in a memory leak, since these keys were still referenced by KHInputScene, and a processing sink, since it would still update the keys no longer used.5
KHInputSet
was a response to that. Each scene would have its own set that it would register with KHInputScene
, and, once the scene went away, it would unregister its input set, freeing the inputs and the processing power. The base inputs would remain, stalwart, shared between all of the input sets.6
As a side note, this is how you make sure that an input set is unregistered when a scene shuts down:
scene.events.on('shutdown', () => {
this.inputSet.unregister();
})
And here’s the code I omitted from KHInputSet
previously:
class KHInputSet {
private registrar: KHIInputRegistrar;
...
registerWith(registrar: KHIInputRegistrar) {
if (this.registrar) {
console.warn(`Input set ${this.identifier} already registered.`
+ "Unregistering from previous set.");
this.registrar.unregisterInputSet(this);
}
this.registrar = registrar;
this.registrar?.registerInputSet(this);
}
unregister() {
this.registrar?.unregisterInputSet(this);
}
}
Creating Something
create() {
this.controllerProviders = [
new KHInputProviderController(this, 0),
new KHInputProviderController(this, 1),
new KHInputProviderController(this, 2),
new KHInputProviderController(this, 3)
];
this.keyboardProvider = new KHInputProviderKeyboard(this);
this.mouseProvider = new KHInputProviderMouse(this);
this.events.on('preupdate', (now, delta) => {
this.updateInputs(now, delta);
}, this);
}
This code instantiates all of the input providers (notably, it generates four gamepad controllers).7 It also registers a callback to update all the inputs in preupdate, which is guaranteed to occur before update.8
This guarantees that it will always be updated before any scene that’s reading from it. This delivers another advantage to this approach—not requiring code in update in every scene. Also another one, in that our input code doesn’t require a reference to the main running scene, as KHInputScene
handles all of it.
Update to Something
private updateInputs(now: number, delta: number): void {
// Update mouse inputs first so that bumping the mouse while holding
// a button doesn't trigger mouse movement.
this.mouseProvider.update(now, delta);
for (let i = 0; i < this.controllerProviders.length; i++) {
this.controllerProviders[i].update(now, delta);
}
this.keyboardProvider.update(now, delta);
// Registered derived inputs must be iterated in the order that
// they're registered, as this guarantees that a derived input's
// inputs will be updated before it is, preventing extra input
// latency.
this.registeredInputSets.forEach(function(value) {
for (let i = 0; i < value.inputs.length; i++) {
value.inputs[i].updateDerivedInput(now, delta);
}
})
}
This part probably looks familiar—it’s pretty much the code we had running in our scene, moved over here. By having a consistent order here, we guarantee that inputs will work the same across all scenes, and make it easier to tweak the behavior.
Overview
Now that we’ve gone over the (rather simple) code, here are some of the tradeoffs.
Advantages:
- Avoids the “stuck input' issue that can happen with a paused scene.
- Slight performance advantage by not creating new providers for each scene.
- Avoids having to update providers in each scene.
- Allows inputs to be updated outside of the normal scene lifecycle.
Disadvantages:
- Requires registering and unregistering of input sets, a process that is error-prone and allows for leaks.
- Allows inputs to be updated outside of the normal scene lifecycle.9
Other:
- It’s a singleton. You know what that means.
I’m not going to make a recommendation as to whether you use KHInputScene
or not, but I’m happy with the state that it’s in now, and plan on continuing to use it for future projects.
Now that I’ve exposed my greatest secret and have lifted a weight from my heart, we can move on to our next appendix topic: dead zones.
-
This isn’t secret. The examples in the documentation frequently do it, and, from what I’ve seen, it’s a widely accepted way of doing things, if not the preferred way. ↩︎
-
Coming back from a pause in Phaser 3 (at least the last version I tested it on, 3.12.0), would erroneously leave the “down” button pressed if it was pressed when pausing the scene. As a result, when you came back to the scene, you would need to press the button twice to get it to register. For all I know, this bug could be fixed. The solution was to re-register the inputs, as mentioned above. ↩︎
-
Also make sure that you know the class has had its create called before you access its input providers. I recommend starting it from your preload scene, which you should have. You can see the tutorial I used to make my own preload scene here, if you’re not sure how to make one. ↩︎
-
Yes, this is a singleton. Everyone has opinions about them. They make testing and mocking more difficult, but they’re darn convenient, and the way that the KHInputKey system works makes testing inputs easy anyway—it’s really easy to swap out user inputs with your own. Most of the advantages and disadvantages of this approach come directly from its singleton nature. ↩︎
-
Remember how I talked about how updating 300+ derived keys didn’t take that much time? This is how I know. ↩︎
-
This is how this approach gets past the key down problem when the scene is paused—the scene never pauses. ↩︎
-
There’s probably a way to do this lazily, but this ensures the keys for a controller will be available, even if a controller isn’t connected yet. ↩︎
-
Hence the name. ↩︎
-
I know this is listed in both the advantages and disadvantages. It can be both. ↩︎