GaryStanton / phaser3-merged-input

A Phaser 3 plugin to map input from keyboard & gamepad to player actions
MIT License
34 stars 5 forks source link

Merged input plugin for Phaser 3

A Phaser 3 plugin to map input from keyboard, gamepad & mouse to player actions.

The merged input plugin listens to input from keyboard, connected gamepads and the mouse pointer, updating ‘player’ objects that you may interrogate instead of writing separate input handlers in your game. Each player object contains direction and button actions. These are updated by the corresponding gamepad input, as well as any keys that you assign to the action.

Benefits

. Single place to handle all your input.
. Keyboard, Gamepad & Mouse input is amalgamated.
. Handle input for multiple player objects to easily create multiplayer games.
. Assign and reassign keys to actions for each player, allowing for ‘redefine keys’ function.
. Assign multiple keys to a single action.
. Interrogate current state of all buttons.
. Global events emitted on button down/up.
· (v1.7.0) Plugin specific events for button/keyboard/mouse presses, as well as device changes
. Check for gamepad button presses (i.e. ‘justDown()’ functionality for gamepads)
. Check the last device type used for interaction.
· (v1.4.0) Button mapping to consistent names such as 'RC_X' for the right cluster of buttons
· (v1.4.0) Normalising of gamepad devices, including generating dpad events for gamepads that map them as axis internally
· (v1.8.0) 'ButtonCombos' mimic Phaser's 'KeyCombo' functionality for gamepads.

Installation

npm install phaser3-merged-input

Then you can either add the plugin to Phaser 3's global configuration:

const config = {
    plugins: {
        scene: [
            {
                key: "mergedInput",
                plugin: MergedInput,
                mapping: "mergedInput",
            },
        ],
    }
};

Or using a scene's local configuration:

class InputController extends Phaser.Scene {
    preload() {
        this.load.scenePlugin('mergedInput', MergedInput);
    }

TypeScript

If you're using TypeScript, you will also need to add a class member to the scene so TypeScript knows how to type it.

Example:

class InputController extends Phaser.Scene {
    private mergedInput?: MergedInput;

If you're using the Phaser global config for the plugin, the member name must have the same name as the value the mapping property specified in the Phaser configuration above, or the plugin won't work.

If you're using the scene local plugin, the member name must match the key specified in scenePlugin(key, ...).


Setup

Set up a player object for each player in your game with addPlayer(). Then assign keys to each action with the defineKey() function, e.g.

var player1 = mergedInput.addPlayer(0);
mergedInput.defineKey(0, 'UP', 'W')
    .defineKey(0, 'DOWN', 'S')
    .defineKey(0, 'LEFT', 'A')
    .defineKey(0, 'RIGHT', 'D')
    .defineKey(0, 'B0', 'U')
    .defineKey(0, 'B1', 'I')
    .defineKey(0, 'B2', 'O')
    .defineKey(0, 'B3', 'P')

var player2 = mergedInput.addPlayer(1);
mergedInput.defineKey(1, 'UP', 'UP')
    .defineKey(1, 'DOWN', 'DOWN')
    .defineKey(1, 'LEFT', 'LEFT')
    .defineKey(1, 'RIGHT', 'RIGHT')
    .defineKey(1, 'B0', 'NUMPAD_0')
    .defineKey(1, 'B1', 'NUMPAD_1')
    .defineKey(1, 'B2', 'NUMPAD_2')
    .defineKey(1, 'B3', 'NUMPAD_3')

NEW in v1.4.0

You may now choose to use 'mapped button names' to define keys, instead of button numbers. The plugin will attempt to map each button to the corresponding number, depending on the type of joypad entered. So, instead of using B0, which is the 'A' button on an Xbox controller, but the 'B' button on an 8-bit Do controller, and the 'X' button on a GeeekPi controller, you can now use 'RC_S' for 'Right cluster: South' - for a more consistent approach.

    var player1 = mergedInput.addPlayer(0);
    mergedInput.defineKey(0, 'UP', 'W')
        .defineKey(0, 'DOWN', 'S')
        .defineKey(0, 'LEFT', 'A')
        .defineKey(0, 'RIGHT', 'D')
        .defineKey(0, 'RC_S', 'U')
        .defineKey(0, 'RC_E', 'I')
        .defineKey(0, 'RC_W', 'O')
        .defineKey(0, 'RC_N', 'P')

Then, interrogate your player objects to check for the state of the action, rather than the key, e.g.

    if(player1.direction.DOWN) {
        // Move your player down. This will remain true for as long as the down button is depressed.
    }

    if(player2.buttons.B0 > 0) {
        // Player two is pressing the first button. This will remain true for as long as B0 is depressed.
    }

    if(player1.buttons_mapped.RC_W > 0) {
        // Player one is pressing left button in the right cluster. This will remain true for as long as the button is depressed.
    }

    if(player1.buttons_mapped.START > 0) {
        // Player one is pressing what the plugin considers to be the 'start' button - depending on the controller config.
    }

    if(player1.interaction.device == 'gamepad') {
        // Player one is using a gamepad, you may wish to update your prompts accordingly.
    }

    if (['B8', 'B9', 'B0'].filter(x => player1.interaction.pressed.includes(x)).length) {
        // Player one has just pressed one of the following buttons - B8, B9 or B0.
        // The 'pressed' interaction flag differs from interrogating the buttons directly. It will contain the button(s) pressed for a single update tick, as it happens.
        // Here we're comparing an array of button names to the array of buttons pressed in the step.
    }

    // NEW in v1.6.0
    if (player1.interaction.isPressed(['RC_S', 'LC_E'])) {
        // Player one has just pressed one of the following buttons - Right cluster: South or Left cluster (DPad): East.
        // Instead of comparing arrays directly as above, we're using the included helper function here, which will return any matching buttons that were pressed in this update step.
    }

New in v1.7.0

A new plugin specific eventEmitter instance exists at mergedInput.events. You may use this across your game to listen for keypresses, button presses and device changes (i.e. moving from using the keyboard to a gamepad).

New in v1.8.0

BUTTON COMBOS ARE HERE!!
A new 'ButtonCombo' exists in the merged input plugin to mimic Phaser's native KeyCombos for gamepad/player combinations.
Button combos emit buttoncombomatch events.
Setting them up is easy:

    let combos_konami = mergedInput.createButtonCombo(player1, ['UP', 'UP', 'DOWN', 'DOWN', 'LEFT', 'RIGHT', 'LEFT', 'RIGHT', 'RC_E', 'RC_S'], { resetOnMatch: true });
    combos_konami.name = 'Konami Code';

    mergedInput.events.on('buttoncombomatch', event => {
        console.log(`Player: ${event.player.index} entered: ${event.combo.name}!`);
    });

Note that combo checking only occurrs on gamepad actions. Keyboard combos are still handled by Phaser.

Demo / Dev

A demo scene is included in the repository.
The demo has been updated to incorporate the mapped buttons and interactions included in v1.4.0 and the helper functions added in v1.6.0

Install with npm install, then use npm run dev to spin up a development server and run the demo scene.

Build plugin

Build the plugin including minified version. Targets the dist folder. npm run build

Changelog

v1.8.6 - 2024-10-06 Added axis threshold, below which an analogue stick will not generate a value.
Previously this was hardcoded at 0.5 to avoid drift, but you may now change this via setAxisThreshold(0.2)

v1.8.5 - 2024-05-15
Bugfix: Gamepad button combo events were missing timestamps.
Bugfix: Incorrect keyboard event states firing. (Thanks to @brntns)

v1.8.4 - 2023-11-26
Bugfix: Mouse pointer checkDown function timers were handled incorrrectly.

v1.8.3 - 2023-11-18
Bugfix: When using a joypad that maps direction buttons to the left axis, the fake DPad functionality was not mimicking button number value changes for buttons and buttons_mapped.

v1.8.2 - 2023-11-13
Bugfix: Gamepad button release was not freeing the timer's tick var.

v1.8.1 - 2023-10-29
Added mouse pointers to checkDown function.
Fixed issue with generic player helper functions when player not fully initialised.

v1.8.0 - 2023-10-29
Added new ButtonCombos, to mimic Phaser's KeyCombos with gamepad buttons.
Added timers to button presses, we're now able to retrieve a pressed, released, and duration value.
Added extra helper functions to the player object, including isDown and checkDown to mimic Phaser's keyboard handling with merged input.
Player helper objects are now able to be called directly on the player object and will accept either mapped or unmapped button actions.

v1.7.0 - 2023-10-15 Added a new plugin specific instance of the event emitter. The old 'mergedInput' events continue to fire on the scene's emitter; however as they are all the same event with extra data, you need to listen to all every 'mergedInput' event and filter for the ones you need. The new plugin specific instance allows you to listen only to the events you need.

v1.6.1 - 2023-06-01
Updated pointer events to only be set when adding the first player. Pointer events now check for player object. Updated typings With many thanks to @Dan-Mizu for help with this release.

v1.6.0 - 2022-12-05
Improved handling of the 'pressed' and 'released' events. Previously it was possible to miss a press event if two happened within the same update step.
IMPORTANT: The pressed & released properties under the player's interaction object has changed from a string to an array, to allow for multiple values in an update step.
Any code that checks these properties should be updated to expect an array of one or more values. New helper functions isPressed() and isReleased() have been added to the interaction and interaction_mapped properties of the player object. Use these to check if one or more buttons were pressed/released in the current update step. See the demo for more details.

v1.5.0 - 2022-08-22
When the game loses focus, the plugin will now reset each of the defined keys to avoid them getting stuck when returning to the game.

v1.4.0 - 2022-07-03
Added normalisation of gamepad devices, using mapping files located in the new configs folder.
Added friendly mapped button names, and a new batch of properties under interaction_mapped and buttons_mapped.
Added fake DPad functionality to better handle joypads that map their DPads to the left axis, instead of the standard buttons 12-15.
Added a debug scene to the demo.

v1.3.1 - 2022-03-11
Fixed missing code caused by bad merge!
Added keywords
Clean up readme.md

v1.3.0 - 2022-03-10
Migrated keyboard interaction flags from the justDown and justUp key functions, to instead use the keyboard's keyDown and keyUp events.
This way we maintain consistancy between keyboard and gamepad interactions, as events trigger before the scene's update call.
Added a new released key to the interaction object to indicate when a button has been released.
Added a new lastPressed and lastReleased key, to replace the existing pressed key - the old pressed key remains for backwards compatability.
Added TypeScript support.
With many thanks to @zewa666 and @bbugh for help with this release.

v1.2.8 - 2021-07-23
Added gamepad directions to interaction buffer/presses to match keyboard interactions.

v1.2.7 - 2021-07-06
Changed the order of buffer/pressed checking in update loop.

v1.2.6 - 2021-05-04
Guess who forgot to build again??

v1.2.5 - 2021-05-04
Updated buttondown and buttonup event listeners from per pad, to per input system. It seems the per pad listeners weren't firing for pad 2 and this method works around the problem. Also added an addPlayer call if the corresponding player is missing. Updated phaser dependancy

v1.2.4 - 2020-05-08
And again, remembering to include the built files would be a bonus.

v1.2.3 - 2020-05-08
Added extra handling for 'null' gamepads.

v1.2.2 - 2020-05-03
Added secondary direction key detection, so that secondary directions may be instigated through a keypress as well as the right stick of a gamepad. Added timestamps to interactions making it possible to tell which was last used, e.g. keyboard vs mouse.

v1.2.1 - 2020-04-27
Actually added the build files.

v1.2.0 - 2020-04-27
You are now able to pass a player's X/Y position to a player object, whereupon the position of the mouse in relation to that player will be used to determine mouse bearings and degrees

v1.1.0 - 2020-04-19
Plugin now handles secondary directional movement from the second stick on a gamepad. Bearings and degrees have been added to direction objects.

Credits

Written by Gary Stanton
Built from the Plugin Starter Kit by Niklas Berg
Demo sprites by Nicolae Berbece


Functions

addPlayer(index)

Add a new player object to the players array.
If an index is provided and a player object at that index already exists, this will be returned instead of another object created

getPlayer(thisPlayer)

Get player object

setupControls()

Returns a struct to hold input control information Set up a struct for each player in the game Direction and Buttons contain the input from the devices The keys struct contains arrays of keyboard characters or mouse buttons that will trigger the action

defineKey(player, action, value, append)

Define a key for a player/action combination

createButtonCombo(player, buttons, [config])

A ButtonCombo will listen for a specific combination of buttons from the given player's gamepad, and when it receives them it will emit a buttoncombomatch event.

{player}.isPressed(button)

Pass one or more button names to check whether one or more buttons were pressed during an update tick.

{player}.isReleased(button)

Pass one or more button names to check whether one or more buttons were released during an update tick.

{player}.isDown(button)

Pass one or more button names to check whether one or more buttons are held down during an update tick.

{player}.checkDown(button, duration, includeFirst)

Pass one or more button names to check whether one or more buttons are held down during an update tick. You may provide a duration to this method and it will return true every X milliseconds.

addPlayer()

Add a new player object to the players array

Param Type
index number

getPlayer(index)

Get player object

Param Type
thisPlayer number

defineKey(player, action, value, append)

Define a key for a player/action combination Param Type
player number The player ID on which we're defining a key
action string The action to define
value string The key to use
append boolean When true, this key definition will be appended to the existing key(s) for this action

createButtonCombo(player, buttons, [config])

A ButtonCombo will listen for a specific combination of buttons from the given player's gamepad, and when it receives them it will emit a buttoncombomatch event.

Param Type
player object The player object on which we're defining a key
buttons array An array of buttons to act as the combo. You may use directions ['UP'], button IDs ['B12'] or mapped buttons ['LC_N']
config Phaser.Types.Input.Keyboard.KeyComboConfig A Key Combo configuration object. Uses the same config as Phaser's native KeyCombo classes

{player}.isPressed(button)

Check if button(s) were pressed during an update tick

Param Type
button string/array

{player}.isReleased(button)

Check if button(s) were released during an update tick

Param Type
button string/array

{player}.isDown(button)

Check if button(s) were held down during an update tick

Param Type
button string/array

{player}.checkDown(button)

Check if button(s) were held down during an update tick
You may provide a duration to this method and it will return true every X milliseconds.

Param Type
button string/array
duration number The duration which must have elapsed before this button is considered as being down.
includeFirst boolean When true, include the first press of a button, otherwise wait for the first passing of the duration.

Events

Event Description Data
gamepad_connected Gamepad is connected gamepad instance
device_changed The last input device has changed last device used (keyboard/gamepad/mouse)
keyboard_keydown Keyboard key pressed player: player instance, key: keycode pressed
keyboard_keyup Keyboard key released player: player instance, key: keycode pressed
gamepad_buttondown Gamepad button pressed player: player instance, button: button number pressed
gamepad_buttonup Gamepad button released player: player instance, button: button number released
gamepad_directiondown Gamepad D-Pad pressed player: player instance, direction: D-Pad direction pressed
gamepad_directionup Gamepad D-Pad released player: player instance, direction: D-Pad direction released
gamepad_directionup Gamepad D-Pad released player: player instance, direction: D-Pad direction released
pointer_down Mouse button pressed button number pressed
pointer_up Mouse button released button number released
buttoncombomatch A button combo match has occurred player: player instance, combo: The ButtonCombo object that matched