Configure keyboard inputs once for all scenes to use

Hello,

I’m new to Phaser and not an epert at JS but I understand what I’m doing so far.

In the process of rebuilding a game that was made from scratch without Phaser, I’m trying to figure out how to efficiently define the controls in Phaser. From the examples I found, I know I can do this :

export default class IntroScene extends Scene {

    constructor() {
        super('Intro');
    }

    preload() {
    }

    create() {
        // Controls
        this.enter = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.ENTER);

        // Other stuff...
    }

    update() {
        // Detect PRESS ENTER and change scene
        if (Phaser.Input.Keyboard.JustDown(this.enter))
        {
            this.scene.start('MenuMain');
        }
    }
}

However I find it very unconvenient, since I will have to declare again the keyboard inputs in create() for the next scene, and the scene after, etc… I’m looking for a way to define once for all a set of keyboard inputs that will be used across all my scenes. It would be much easier to manage and make changes in case I change my mind on what key to use for a given action. Also if I want to let the player choose his own inputs in the future, I need to address this correctly.

I have read that it’s recommended to use a dedicated scene for the UI, that would exchange info with the actual game scene via an EventEmitter. I imagine I could use a blank scene for the controls, that is detecting the inputs and emitting them for the current game scene to catch. But it sounds twisted and unpractical, and maybe not performant. Am I missing an easy way?

Thanks for your help

When I started with Phaser the controls were the most complicated things for me and they still are.
My current approach is to create a ControlsPlugin class as scene plugin. Every scene I create is a child of a class I call “BaseScene”. In this BaseScene’s update method, I call a method of the plugin, so I don’t have to call it in every single scene by hand. Example:

export const EVENT_UP = 'CONTROL_UP'
export const EVENT_RIGHT = 'CONTROL_RIGHT'
export const EVENT_DOWN = 'CONTROL_DOWN'
export const EVENT_LEFT = 'CONTROL_LEFT'
export const EVENT_INTERACT = 'CONTROL_INTERACT'

class ControlsPlugin extends Phaser.Plugins.ScenePlugin {
    constructor (scene, pluginManager) {
        super(scene, pluginManager)

        this.keys = {
            up: ['UP', Phaser.Input.Keyboard.KeyCodes.UP, Phaser.Input.Keyboard.KeyCodes.W],
            right: ['RIGHT', Phaser.Input.Keyboard.KeyCodes.RIGHT, Phaser.Input.Keyboard.KeyCodes.D],
            down: ['DOWN', Phaser.Input.Keyboard.KeyCodes.DOWN, Phaser.Input.Keyboard.KeyCodes.S],
            left: ['LEFT', Phaser.Input.Keyboard.KeyCodes.LEFT, Phaser.Input.Keyboard.KeyCodes.A],
            interact: [Phaser.Input.Keyboard.KeyCodes.ENTER, Phaser.Input.Keyboard.KeyCodes.SPACE],
        }
    }

    update () {
        this._emitKeyEvent({ keys: this.keys.up, eventName: EVENT_UP })
        this._emitKeyEvent({ keys: this.keys.right, eventName: EVENT_RIGHT })
        this._emitKeyEvent({ keys: this.keys.down, eventName: EVENT_DOWN })
        this._emitKeyEvent({ keys: this.keys.left, eventName: EVENT_LEFT })
        this._emitKeyEvent({ keys: this.keys.interact, eventName: EVENT_INTERACT })
    }

    _emitKeyEvent ({ keys, eventName }) {
        keys.forEach(key => {
            const keyObj = this.scene.input.keyboard.addKey(key)
            if (Phaser.Input.Keyboard.JustDown(keyObj)) {
                this.scene.events.emit(eventName, this.scene)
            }
        })
    }
}

class BaseScene extends Phaser.Scene {
    update () {
        this.controls.update()
    }
}

class StartMenuScene extends BaseScene {
    create () {
        // Listen to the events from controls plugin and do something. For example select a button in a start menu
        this.events.on(EVENT_UP, this._selectPreviousButton, this)
        this.events.on(EVENT_DOWN, this._selectNextButton, this)
        this.events.once(EVENT_INTERACT, this._onInteract, this)

        // This is the most complicated part, because you have to make sure that all event listeners will be removed when the scene shuts down
        this.events.once('shutdown', () => {
            this.events.off(EVENT_UP, this._selectPreviousButton, this)
            this.events.off(EVENT_UP, this._selectNextButton, this)
            this.events.off(EVENT_INTERACT, this._onInteract, this)
        })
    }
}

Unfortunately I don’t have any idea if this is a good approach :smiley: Maybe it’s way too complicated…

1 Like

Nick, I was just reading your example of “Play and skip intro video” earlier and noticed the use a plugin for your controls, a thing I was unaware of in Phaser.

I realized it might well be a solution, thanks for writing the detailed code above, it helps a ton!
Maybe it’s still a bit complicated but I can’t think of a better way in Phaser as of now.

It felt quite simple in the first build of my game in basic JS though, something along those lines for the main script:

        Game.KEYS = {};
        Game.JUMP = "Space";
        Game.ATTACK = "KeyA";

        window.addEventListener("keydown", e => {
            Game.KEYS[e.code] = true;
        });

        window.addEventListener("keyup", e => {
            Game.KEYS[e.code] = false;
        });

And then in the update() method for any scene I like:

            if(Game.KEYS[Game.JUMP])
            {
                // Do jumpy stuff
            }
1 Like

I think in the end it’s nearly the same.

You could also define the “doings” right in the “.on()” method:

this.events.on(EVENT_UP, () => { /* Some magic code here */ })

But I tried this and had a ton of problems, because I could not remove the listeners like this :’)

I have a lot of scenes and a lot of keyboard inputs for my adventure (Dialog System, Minigames, etc.).
A platformer for example, where you just have one input for jumping or running, maybe even just a single scene, would not need to shutdown all the listeners, I guess. :thinking:

I wouldn’t add keys continuously. It should be something like

this.keys = {
  up: [
    'UP',
    this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.UP),
    this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W),
  ],
  // …
};
1 Like

You can make shared functions createControls() and updateControls() and pass in the scene.

Edit: Aww, @samme beat me to it again :sweat_smile:

This post made me think a lot about WHY Phaser doesn’t provide a global keyboardPlugin in favor of scene-specific keyboardPlugin instances, and I believe it has to do with handling event propagation. Much like how click events “bubble up” in javascript when DOM elements are layered on top of each other, Phaser’s approach provides a lot of methods that give you control over how input events propagate through scenes in the scene list, all the way to the global event listener. For a little clarity, I’d suggest reading this dev log, reading rex’s notes on keyboard events, and fiddling with this example of events propagating across scenes.

This all might be a bit overkill for your game though, and it’s a classic case of sacrificing simplicity for tighter control. I think your primary concern is code maintainability - you want your inputs to be defined just once so you only have to change them in one place, both for your own purposes and if your players want to customize their controls. So with that in mind, here’s what I’m thinking:

  1. Create new key objects for each scene, but abstract the actual keys used. This gives you the full power of event propagation between scenes, but gives you the freedom to change your key codes in one place. In this way you’ve dissociated your naming that you’ll use in your scene’s logic (update function, events, etc) from the actual keys used.
// in a global object variable, or in a separate file that you import into each scene
const { KeyCodes } = Phaser.Input.Keyboard; 
const KEY_BINDINGS = {
  JUMP:   KeyCodes.SPACE,
  ATTACK: KeyCodes.A,
};

// in one of your scenes
create() {
  const KEYS = this.input.keyboard.addKeys(KEY_BINDINGS);
}

update() {
  if (KEYS.JUMP.isDown) // Do jumpy stuff
}
  1. Do what other people have suggested and create one set of controls in a scene that persists throughout your game and runs in parallel to your other scenes. You can access the key objects from this scene in your other scenes, and you can use whatever method you want to make them globally accessible. While this seems simpler in theory, since your key objects only exist in one place and you’re just accessing the same references from different scenes, I don’t think I like it as much - you can’t take advantage of Phaser’s scene propagation methods, and it make your scene classes just a little less modular and independent.
// Scene A  (with key "sceneA")
create() {
  const { KeyCodes } = Phaser.Input.Keyboard; 
  this.KEYS = this.input.keyboard.addKeys({
    JUMP:   KeyCodes.SPACE,
    ATTACK: KeyCodes.A,
  });
}

// Scene B  (with key "sceneB")
create() {
  this.KEYS = this.scene.get("sceneA").KEYS;
}

update() {
  const { KEYS } = this;
  if (KEYS.JUMP.isDown) // Do jumpy stuff
}

In the end though, it’s entirely up to you! Phaser is just javascript, and it has full access to anything you’d normally do in the browser - so you could use your original game controls code of adding event listeners to the window and using a global object to track their up/down states, and refer to these in the update methods of your scenes. Then you could change this later if you feel it’s necessary as you get more comfortable with the framework.

I’d suggest trying out the events system though, it’s no less performant, and although it seems more obtuse, it gives you a lot of freedom and can make your code very modular and succinct! You may even be inspired to emit your own custom events to communicate between scenes or even between game objects as you get more comfortable with it!

1 Like

Thanks for the replies everyone :grinning_face_with_smiling_eyes:

@StinOfSin I think I’ll give your first method a try, it looks light and sufficient for what I want to achieve. I plan to do it slightly differently though, instead of importing the KEY_BINDINGS into each scene, I could create a new scene class that imports the controls once, then have my other scenes extend the one with controls.

I’ll let you know how it goes!

1 Like

Kudos for a scene plugin.

1 Like

So I implemented Stin’s first method and it works as intended for me, thanks!

Now I’m confused about sound management, I guess I’ll have to make another topic but I want to dig into it as much as I can by myself beforehand.