├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── dist ├── MergedInput.js ├── MergedInput.js.map ├── MergedInput.min.js ├── MergedInput.min.js.map └── MergedInput.min.map ├── main.d.ts ├── package-lock.json ├── package.json ├── src ├── ButtonCombo.js ├── configs │ ├── bearings.js │ ├── pad_dualshock.js │ ├── pad_generic.js │ ├── pad_unlicensedSNES.js │ └── pad_xbox360.js ├── controlManager.js ├── demo │ ├── assets │ │ ├── gamepad.json │ │ └── gamepad.png │ ├── button.js │ ├── debug.js │ ├── demo.js │ ├── index.html │ ├── inputController.js │ ├── main.js │ └── merged-input-demo.gif └── main.js ├── webpack.build.config.js └── webpack.demo.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = false 9 | indent_style = tab 10 | indent_size = 4 11 | tab_width = 4 12 | 13 | [*.{md,markdown}] 14 | trim_trailing_whitespace = false 15 | insert_final_newline = false 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # System and IDE files 2 | Thumbs.db 3 | .DS_Store 4 | *.bak* 5 | dev/ 6 | 7 | # Vendors 8 | node_modules/ 9 | 10 | # Build 11 | build/ 12 | /npm-debug.log 13 | *.code-workspace 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Gary Stanton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Merged input plugin for Phaser 3 2 | A Phaser 3 plugin to map input from keyboard, gamepad & mouse to player actions. 3 | 4 | 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. 5 | 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. 6 | 7 | ## Benefits 8 | . Single place to handle all your input. 9 | . Keyboard, Gamepad & Mouse input is amalgamated. 10 | . Handle input for multiple player objects to easily create multiplayer games. 11 | . Assign and reassign keys to actions for each player, allowing for ‘redefine keys’ function. 12 | . Assign multiple keys to a single action. 13 | . Interrogate current state of all buttons. 14 | . Global events emitted on button down/up. 15 | · (v1.7.0) Plugin specific events for button/keyboard/mouse presses, as well as device changes 16 | . Check for gamepad button presses (i.e. ‘justDown()’ functionality for gamepads) 17 | . Check the last device type used for interaction. 18 | · (v1.4.0) Button mapping to consistent names such as 'RC_X' for the right cluster of buttons 19 | · (v1.4.0) Normalising of gamepad devices, including generating dpad events for gamepads that map them as axis internally 20 | · (v1.8.0) 'ButtonCombos' mimic Phaser's 'KeyCombo' functionality for gamepads. 21 | 22 | ## Installation 23 | 24 | ``` 25 | npm install phaser3-merged-input 26 | ``` 27 | 28 | Then you can either add the plugin to Phaser 3's global configuration: 29 | 30 | ```javascript 31 | const config = { 32 | plugins: { 33 | scene: [ 34 | { 35 | key: "mergedInput", 36 | plugin: MergedInput, 37 | mapping: "mergedInput", 38 | }, 39 | ], 40 | } 41 | }; 42 | ``` 43 | 44 | Or using a scene's local configuration: 45 | 46 | ```javascript 47 | class InputController extends Phaser.Scene { 48 | preload() { 49 | this.load.scenePlugin('mergedInput', MergedInput); 50 | } 51 | ``` 52 | 53 | 54 | ### TypeScript 55 | 56 | If you're using TypeScript, you will also need to add a class member to the scene so TypeScript knows how to type it. 57 | 58 | **Example**: 59 | 60 | ```typescript 61 | class InputController extends Phaser.Scene { 62 | private mergedInput?: MergedInput; 63 | ``` 64 | 65 | 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. 66 | 67 | If you're using the scene local plugin, the member name **must** match the key specified in `scenePlugin(key, ...)`. 68 | 69 | --- 70 | 71 | ## Setup 72 | Set up a player object for each player in your game with `addPlayer()`. 73 | Then assign keys to each action with the `defineKey()` function, e.g. 74 | ```javascript 75 | var player1 = mergedInput.addPlayer(0); 76 | mergedInput.defineKey(0, 'UP', 'W') 77 | .defineKey(0, 'DOWN', 'S') 78 | .defineKey(0, 'LEFT', 'A') 79 | .defineKey(0, 'RIGHT', 'D') 80 | .defineKey(0, 'B0', 'U') 81 | .defineKey(0, 'B1', 'I') 82 | .defineKey(0, 'B2', 'O') 83 | .defineKey(0, 'B3', 'P') 84 | 85 | var player2 = mergedInput.addPlayer(1); 86 | mergedInput.defineKey(1, 'UP', 'UP') 87 | .defineKey(1, 'DOWN', 'DOWN') 88 | .defineKey(1, 'LEFT', 'LEFT') 89 | .defineKey(1, 'RIGHT', 'RIGHT') 90 | .defineKey(1, 'B0', 'NUMPAD_0') 91 | .defineKey(1, 'B1', 'NUMPAD_1') 92 | .defineKey(1, 'B2', 'NUMPAD_2') 93 | .defineKey(1, 'B3', 'NUMPAD_3') 94 | ``` 95 | 96 | ### NEW in v1.4.0 97 | You may now choose to use 'mapped button names' to define keys, instead of button numbers. 98 | The plugin will attempt to map each button to the corresponding number, depending on the type of joypad entered. 99 | 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. 100 | ```javascript 101 | var player1 = mergedInput.addPlayer(0); 102 | mergedInput.defineKey(0, 'UP', 'W') 103 | .defineKey(0, 'DOWN', 'S') 104 | .defineKey(0, 'LEFT', 'A') 105 | .defineKey(0, 'RIGHT', 'D') 106 | .defineKey(0, 'RC_S', 'U') 107 | .defineKey(0, 'RC_E', 'I') 108 | .defineKey(0, 'RC_W', 'O') 109 | .defineKey(0, 'RC_N', 'P') 110 | ``` 111 | 112 | Then, interrogate your player objects to check for the state of the _action_, rather than the key, e.g. 113 | ```javascript 114 | if(player1.direction.DOWN) { 115 | // Move your player down. This will remain true for as long as the down button is depressed. 116 | } 117 | 118 | if(player2.buttons.B0 > 0) { 119 | // Player two is pressing the first button. This will remain true for as long as B0 is depressed. 120 | } 121 | 122 | if(player1.buttons_mapped.RC_W > 0) { 123 | // Player one is pressing left button in the right cluster. This will remain true for as long as the button is depressed. 124 | } 125 | 126 | if(player1.buttons_mapped.START > 0) { 127 | // Player one is pressing what the plugin considers to be the 'start' button - depending on the controller config. 128 | } 129 | 130 | if(player1.interaction.device == 'gamepad') { 131 | // Player one is using a gamepad, you may wish to update your prompts accordingly. 132 | } 133 | 134 | if (['B8', 'B9', 'B0'].filter(x => player1.interaction.pressed.includes(x)).length) { 135 | // Player one has just pressed one of the following buttons - B8, B9 or B0. 136 | // 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. 137 | // Here we're comparing an array of button names to the array of buttons pressed in the step. 138 | } 139 | 140 | // NEW in v1.6.0 141 | if (player1.interaction.isPressed(['RC_S', 'LC_E'])) { 142 | // Player one has just pressed one of the following buttons - Right cluster: South or Left cluster (DPad): East. 143 | // 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. 144 | } 145 | ``` 146 | 147 | ### New in v1.7.0 148 | A new plugin specific eventEmitter instance exists at `mergedInput.events`. 149 | 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). 150 | 151 | 152 | ### New in v1.8.0 153 | BUTTON COMBOS ARE HERE!! 154 | A new 'ButtonCombo' exists in the merged input plugin to mimic Phaser's native KeyCombos for gamepad/player combinations. 155 | Button combos emit `buttoncombomatch` events. 156 | Setting them up is easy: 157 | ```javascript 158 | let combos_konami = mergedInput.createButtonCombo(player1, ['UP', 'UP', 'DOWN', 'DOWN', 'LEFT', 'RIGHT', 'LEFT', 'RIGHT', 'RC_E', 'RC_S'], { resetOnMatch: true }); 159 | combos_konami.name = 'Konami Code'; 160 | 161 | mergedInput.events.on('buttoncombomatch', event => { 162 | console.log(`Player: ${event.player.index} entered: ${event.combo.name}!`); 163 | }); 164 | ``` 165 | 166 | Note that combo checking only occurrs on gamepad actions. Keyboard combos are still handled by Phaser. 167 | 168 | 169 | 170 | 171 | ## Demo / Dev 172 | A demo scene is included in the repository. 173 | 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 174 | 175 | ![](src/demo/merged-input-demo.gif) 176 | 177 | Install with `npm install`, then use `npm run dev` to spin up a development server and run the demo scene. 178 | 179 | 180 | ## Build plugin 181 | Build the plugin including minified version. Targets the dist folder. 182 | `npm run build` 183 | 184 | ## Changelog 185 | 186 | v1.9.0 - 2025-04-20 187 | Updates to pointer handling, in relation to player position. 188 | Updates to build dependancies. 189 | 190 | v1.8.6 - 2024-10-06 191 | Added axis threshold, below which an analogue stick will not generate a value. 192 | Previously this was hardcoded at 0.5 to avoid drift, but you may now change this via `setAxisThreshold(0.2)` 193 | 194 | v1.8.5 - 2024-05-15 195 | Bugfix: Gamepad button combo events were missing timestamps. 196 | Bugfix: Incorrect keyboard event states firing. (Thanks to @brntns) 197 | 198 | v1.8.4 - 2023-11-26 199 | Bugfix: Mouse pointer checkDown function timers were handled incorrrectly. 200 | 201 | v1.8.3 - 2023-11-18 202 | 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`. 203 | 204 | v1.8.2 - 2023-11-13 205 | Bugfix: Gamepad button release was not freeing the timer's tick var. 206 | 207 | v1.8.1 - 2023-10-29 208 | Added mouse pointers to checkDown function. 209 | Fixed issue with generic player helper functions when player not fully initialised. 210 | 211 | v1.8.0 - 2023-10-29 212 | Added new ButtonCombos, to mimic Phaser's KeyCombos with gamepad buttons. 213 | Added timers to button presses, we're now able to retrieve a pressed, released, and duration value. 214 | Added extra helper functions to the player object, including `isDown` and `checkDown` to mimic Phaser's keyboard handling with merged input. 215 | Player helper objects are now able to be called directly on the player object and will accept either mapped or unmapped button actions. 216 | 217 | v1.7.0 - 2023-10-15 218 | Added a new plugin specific instance of the event emitter. 219 | 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. 220 | The new plugin specific instance allows you to listen only to the events you need. 221 | 222 | v1.6.1 - 2023-06-01 223 | Updated pointer events to only be set when adding the first player. 224 | Pointer events now check for player object. 225 | Updated typings 226 | With many thanks to @Dan-Mizu for help with this release. 227 | 228 | v1.6.0 - 2022-12-05 229 | 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. 230 | **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. 231 | Any code that checks these properties should be updated to expect an array of one or more values. 232 | New helper functions `isPressed()` and `isReleased()` have been added to the `interaction` and `interaction_mapped` properties of the player object. 233 | Use these to check if one or more buttons were pressed/released in the current update step. See the demo for more details. 234 | 235 | v1.5.0 - 2022-08-22 236 | 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. 237 | 238 | v1.4.0 - 2022-07-03 239 | Added normalisation of gamepad devices, using mapping files located in the new `configs` folder. 240 | Added friendly mapped button names, and a new batch of properties under `interaction_mapped` and `buttons_mapped`. 241 | Added fake DPad functionality to better handle joypads that map their DPads to the left axis, instead of the standard buttons 12-15. 242 | Added a debug scene to the demo. 243 | 244 | v1.3.1 - 2022-03-11 245 | Fixed missing code caused by bad merge! 246 | Added keywords 247 | Clean up readme.md 248 | 249 | v1.3.0 - 2022-03-10 250 | Migrated keyboard interaction flags from the `justDown` and `justUp` key functions, to instead use the keyboard's `keyDown` and `keyUp` events. 251 | This way we maintain consistancy between keyboard and gamepad interactions, as events trigger before the scene's update call. 252 | Added a new `released` key to the interaction object to indicate when a button has been released. 253 | Added a new `lastPressed` and `lastReleased` key, to replace the existing `pressed` key - the old `pressed` key remains for backwards compatability. 254 | Added TypeScript support. 255 | With many thanks to @zewa666 and @bbugh for help with this release. 256 | 257 | v1.2.8 - 2021-07-23 258 | Added gamepad directions to interaction buffer/presses to match keyboard interactions. 259 | 260 | v1.2.7 - 2021-07-06 261 | Changed the order of buffer/pressed checking in update loop. 262 | 263 | v1.2.6 - 2021-05-04 264 | Guess who forgot to build again?? 265 | 266 | v1.2.5 - 2021-05-04 267 | Updated buttondown and buttonup event listeners from per pad, to per input system. 268 | It seems the per pad listeners weren't firing for pad 2 and this method works around the problem. 269 | Also added an addPlayer call if the corresponding player is missing. 270 | Updated phaser dependancy 271 | 272 | v1.2.4 - 2020-05-08 273 | And again, remembering to include the built files would be a bonus. 274 | 275 | v1.2.3 - 2020-05-08 276 | Added extra handling for 'null' gamepads. 277 | 278 | v1.2.2 - 2020-05-03 279 | Added secondary direction key detection, so that secondary directions may be instigated through a keypress as well as the right stick of a gamepad. 280 | Added timestamps to interactions making it possible to tell which was last used, e.g. keyboard vs mouse. 281 | 282 | v1.2.1 - 2020-04-27 283 | Actually added the build files. 284 | 285 | v1.2.0 - 2020-04-27 286 | 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 287 | 288 | v1.1.0 - 2020-04-19 289 | Plugin now handles secondary directional movement from the second stick on a gamepad. 290 | Bearings and degrees have been added to direction objects. 291 | 292 | 293 | ## Credits 294 | Written by [Gary Stanton](https://garystanton.co.uk) 295 | Built from the [Plugin Starter Kit](https://github.com/nkholski/phaser-plugin-starter) by Niklas Berg 296 | Demo sprites by [Nicolae Berbece](https://opengameart.org/content/free-keyboard-and-controllers-prompts-pack) 297 | 298 | --- 299 | 300 | ## Functions 301 | 302 |
303 |
addPlayer(index)
304 |

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

305 |
306 |
getPlayer(thisPlayer)
307 |

Get player object

308 |
309 |
setupControls()
310 |

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

314 |
315 |
defineKey(player, action, value, append)
316 |

Define a key for a player/action combination

317 |
318 |
createButtonCombo(player, buttons, [config])
319 |

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.

320 |
321 |
{player}.isPressed(button)
322 |

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

323 |
324 |
{player}.isReleased(button)
325 |

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

326 |
327 |
{player}.isDown(button)
328 |

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

329 |
330 |
{player}.checkDown(button, duration, includeFirst)
331 |

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.

332 |
333 |
334 | 335 | 336 | 337 | ### addPlayer() 338 | Add a new player object to the players array 339 | 340 | | Param | Type | 341 | | --- | --- | 342 | | index | number | 343 | 344 | 345 | 346 | 347 | ### getPlayer(index) 348 | Get player object 349 | 350 | | Param | Type | 351 | | --- | --- | 352 | | thisPlayer | number | 353 | 354 | 355 | 356 | 357 | ### defineKey(player, action, value, append) 358 | Define a key for a player/action combination 359 | | Param | Type | | 360 | | --- | --- | --- | 361 | | player | number | The player ID on which we're defining a key | 362 | | action | string | The action to define | 363 | | value | string | The key to use | 364 | | append | boolean | When true, this key definition will be appended to the existing key(s) for this action | 365 | 366 | 367 | 368 | ### createButtonCombo(player, buttons, [config]) 369 | 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. 370 | 371 | | Param | Type | | 372 | | --- | --- | --- | 373 | | player | object | The player object on which we're defining a key | 374 | | buttons | array | An array of buttons to act as the combo. You may use directions ['UP'], button IDs ['B12'] or mapped buttons ['LC_N'] | 375 | | config | Phaser.Types.Input.Keyboard.KeyComboConfig | A Key Combo configuration object. Uses the same config as Phaser's native KeyCombo classes | 376 | 377 | 378 | 379 | 380 | ### {player}.isPressed(button) 381 | Check if button(s) were pressed during an update tick 382 | 383 | | Param | Type | 384 | | --- | --- | 385 | | button | string/array | 386 | 387 | 388 | 389 | 390 | ### {player}.isReleased(button) 391 | Check if button(s) were released during an update tick 392 | 393 | | Param | Type | 394 | | --- | --- | 395 | | button | string/array | 396 | 397 | 398 | 399 | ### {player}.isDown(button) 400 | Check if button(s) were held down during an update tick 401 | 402 | | Param | Type | 403 | | --- | --- | 404 | | button | string/array | 405 | 406 | 407 | 408 | ### {player}.checkDown(button) 409 | Check if button(s) were held down during an update tick 410 | You may provide a duration to this method and it will return true every X milliseconds. 411 | 412 | | Param | Type | 413 | | --- | --- | 414 | | button | string/array | 415 | | duration | number | The duration which must have elapsed before this button is considered as being down. 416 | | includeFirst | boolean | When true, include the first press of a button, otherwise wait for the first passing of the duration. 417 | 418 | 419 | 420 | 421 | ## Events 422 | 423 | | Event | Description | Data | 424 | | --- | --- | --- | 425 | | gamepad_connected | Gamepad is connected | gamepad instance | 426 | | device_changed | The last input device has changed | last device used (keyboard/gamepad/mouse) | 427 | | keyboard_keydown | Keyboard key pressed | player: player instance, key: keycode pressed | 428 | | keyboard_keyup | Keyboard key released | player: player instance, key: keycode pressed | 429 | | gamepad_buttondown | Gamepad button pressed | player: player instance, button: button number pressed | 430 | | gamepad_buttonup | Gamepad button released | player: player instance, button: button number released | 431 | | gamepad_directiondown | Gamepad D-Pad pressed | player: player instance, direction: D-Pad direction pressed | 432 | | gamepad_directionup | Gamepad D-Pad released | player: player instance, direction: D-Pad direction released | 433 | | gamepad_directionup | Gamepad D-Pad released | player: player instance, direction: D-Pad direction released | 434 | | pointer_down | Mouse button pressed | button number pressed | 435 | | pointer_up | Mouse button released | button number released | 436 | | buttoncombomatch | A button combo match has occurred | player: player instance, combo: The ButtonCombo object that matched | -------------------------------------------------------------------------------- /main.d.ts: -------------------------------------------------------------------------------- 1 | declare module "phaser3-merged-input" { 2 | import * as Phaser from "phaser"; 3 | 4 | export type KeyCode = keyof typeof Phaser.Input.Keyboard.KeyCodes; 5 | export type Bearing = 6 | | "" 7 | | "W" 8 | | "NW" 9 | | "N" 10 | | "NE" 11 | | "E" 12 | | "SE" 13 | | "S" 14 | | "SW"; 15 | export interface Player { 16 | direction: { 17 | UP: 0 | 1; 18 | DOWN: 0 | 1; 19 | LEFT: 0 | 1; 20 | RIGHT: 0 | 1; 21 | BEARING: Bearing; 22 | BEARING_LAST: Bearing; 23 | DEGREES: number; 24 | DEGREES_LAST: number; 25 | TIMESTAMP: number; 26 | }; 27 | 28 | direction_secondary: { 29 | UP: 0 | 1; 30 | DOWN: 0 | 1; 31 | LEFT: 0 | 1; 32 | RIGHT: 0 | 1; 33 | BEARING: Bearing; 34 | BEARING_LAST: Bearing; 35 | DEGREES: number; 36 | DEGREES_LAST: number; 37 | TIMESTAMP: number; 38 | }; 39 | 40 | buttons: { 41 | B1: 0 | 1; 42 | B2: 0 | 1; 43 | B3: 0 | 1; 44 | B4: 0 | 1; 45 | B5: 0 | 1; 46 | B6: 0 | 1; 47 | B7: 0 | 1; 48 | B8: 0 | 1; 49 | B9: 0 | 1; 50 | B10: 0 | 1; 51 | B11: 0 | 1; 52 | B12: 0 | 1; 53 | B13: 0 | 1; 54 | B14: 0 | 1; 55 | B15: 0 | 1; 56 | B16: 0 | 1; 57 | }; 58 | 59 | pointer: { 60 | M1: 0 | 1; 61 | M2: 0 | 1; 62 | M3: 0 | 1; 63 | M4: 0 | 1; 64 | M5: 0 | 1; 65 | BEARING: Bearing; 66 | BEARING_DEGREES: number; 67 | ANGLE: number; 68 | TIMESTAMP: number; 69 | }; 70 | 71 | position: {}; 72 | interaction: { 73 | buffer: string[]; 74 | device: string; 75 | pressed: string[]; 76 | released: string[]; 77 | lastPressed: string; 78 | lastReleased: string; 79 | }; 80 | interaction_mapped: { 81 | isPressed: (button: string | string[]) => boolean; 82 | }; 83 | gamepad: { 84 | id: number; 85 | index: number; 86 | }; 87 | keys: { 88 | UP: []; 89 | DOWN: []; 90 | LEFT: []; 91 | RIGHT: []; 92 | B1: []; 93 | B2: []; 94 | B3: []; 95 | B4: []; 96 | B5: []; 97 | B6: []; 98 | B7: []; 99 | B8: []; 100 | B9: []; 101 | B10: []; 102 | B11: []; 103 | B12: []; 104 | B13: []; 105 | B14: []; 106 | B15: []; 107 | B16: []; 108 | }; 109 | setPosition: (x: number, y: number) => void; 110 | internal: { 111 | fakedpadBuffer: string[]; 112 | fakedpadPressed: string[]; 113 | fakedpadReleased: string[]; 114 | }; 115 | } 116 | 117 | export default class MergedInput { 118 | /** 119 | * The Merged Input plugin is designed to run in the background and handle input. 120 | * Upon detecting a keypress or gamepad interaction, the plugin will update a player object and emit global events. 121 | * 122 | * @extends Phaser.Scene 123 | * @param {*} scene 124 | * @param {*} pluginManager 125 | */ 126 | constructor(scene: any, pluginManager: any); 127 | scene: any; 128 | players: Player[]; 129 | gamepads: any[]; 130 | keys: {}; 131 | bearings: { 132 | "-180": string; 133 | "-168.75": string; 134 | "-157.5": string; 135 | "-146.25": string; 136 | "-135": string; 137 | "-123.75": string; 138 | "-112.5": string; 139 | "-101.25": string; 140 | "-90": string; 141 | "-78.75": string; 142 | "-67.5": string; 143 | "-56.25": string; 144 | "-45": string; 145 | "-33.75": string; 146 | "-22.5": string; 147 | "-11.25": string; 148 | "0": string; 149 | "11.25": string; 150 | "22.5": string; 151 | "33.75": string; 152 | "45": string; 153 | "56.25": string; 154 | "67.5": string; 155 | "78.75": string; 156 | "90": string; 157 | "101.25": string; 158 | "112.5": string; 159 | "123.75": string; 160 | "135": string; 161 | "146.25": string; 162 | "157.5": string; 163 | "168.75": string; 164 | "180": string; 165 | }; 166 | refreshGamepads(): void; 167 | boot(): void; 168 | eventEmitter: any; 169 | preupdate(): void; 170 | postupdate(): void; 171 | /** 172 | * Set up the gamepad and associate with a player object 173 | */ 174 | setupGamepad(thisGamepad: any): void; 175 | /** 176 | * Add a new player object to the players array 177 | * @param {number} index Player index - if a player object at this index already exists, it will be returned instead of creating a new player object 178 | */ 179 | addPlayer(index: number): Player; 180 | /** 181 | * Get player object 182 | * @param {number} index Player index 183 | */ 184 | getPlayer(index: number): any; 185 | getPlayerIndexFromKey(key: any): any; 186 | getPlayerButtonFromKey(key): any; 187 | /** 188 | * Returns a struct to hold input control information 189 | * Set up a struct for each player in the game 190 | * Direction and Buttons contain the input from the devices 191 | * The keys struct contains arrays of keyboard characters that will trigger the action 192 | */ 193 | setupControls(): { 194 | direction: { 195 | UP: number; 196 | DOWN: number; 197 | LEFT: number; 198 | RIGHT: number; 199 | BEARING: string; 200 | BEARING_LAST: string; 201 | DEGREES: number; 202 | DEGREES_LAST: number; 203 | TIMESTAMP: number; 204 | }; 205 | direction_secondary: { 206 | UP: number; 207 | DOWN: number; 208 | LEFT: number; 209 | RIGHT: number; 210 | BEARING: string; 211 | DEGREES: number; 212 | BEARING_LAST: string; 213 | DEGREES_LAST: number; 214 | TIMESTAMP: number; 215 | }; 216 | buttons: {}; 217 | pointer: { 218 | M1: number; 219 | M2: number; 220 | M3: number; 221 | M4: number; 222 | M5: number; 223 | BEARING: string; 224 | BEARING_DEGREES: number; 225 | ANGLE: number; 226 | TIMESTAMP: number; 227 | }; 228 | position: {}; 229 | interaction: { 230 | buffer: string[]; 231 | device: string; 232 | pressed: string[]; 233 | released: string[]; 234 | lastPressed: string; 235 | lastReleased: string; 236 | }; 237 | gamepad: {}; 238 | keys: { 239 | UP: any[]; 240 | DOWN: any[]; 241 | LEFT: any[]; 242 | RIGHT: any[]; 243 | }; 244 | }; 245 | /** 246 | * Define a key for a player/action combination 247 | * @param {number} player The player on which we're defining a key 248 | * @param {string} action The action to define 249 | * @param {string} value The key to use 250 | * @param {boolean} append When true, this key definition will be appended to the existing key(s) for this action 251 | */ 252 | defineKey( 253 | player: number, 254 | action: string, 255 | value: KeyCode, 256 | append?: boolean 257 | ): MergedInput; 258 | /** 259 | * Iterate through players and check for interaction with defined keys 260 | */ 261 | checkKeyboardInput(): void; 262 | /** 263 | * When a keyboard button is pressed down, this function will emit a mergedInput event in the global registry. 264 | * The event contains a reference to the player assigned to the key, and passes a mapped action and value 265 | */ 266 | keyboardKeyDown(event: KeyboardEvent): void; 267 | /** 268 | * When a keyboard button is released, this function will emit a mergedInput event in the global registry. 269 | * The event contains a reference to the player assigned to the key, and passes a mapped action and value 270 | */ 271 | keyboardKeyUp(event: KeyboardEvent): void; 272 | /** 273 | * Iterate through players and check for interaction with defined pointer buttons 274 | */ 275 | checkPointerInput(): void; 276 | /** 277 | * When a gamepad button is pressed down, this function will emit a mergedInput event in the global registry. 278 | * The event contains a reference to the player assigned to the gamepad, and passes a mapped action and value 279 | * @param {number} index Button index 280 | * @param {number} value Button value 281 | * @param {Phaser.Input.Gamepad.Button} button Phaser Button object 282 | */ 283 | gamepadButtonDown( 284 | pad: any, 285 | button: Phaser.Input.Gamepad.Button, 286 | value: number 287 | ): void; 288 | /** 289 | * When a gamepad button is released, this function will emit a mergedInput event in the global registry. 290 | * The event contains a reference to the player assigned to the gamepad, and passes a mapped action and value 291 | * @param {number} index Button index 292 | * @param {number} value Button value 293 | * @param {Phaser.Input.Gamepad.Button} button Phaser Button object 294 | */ 295 | gamepadButtonUp( 296 | pad: any, 297 | button: Phaser.Input.Gamepad.Button, 298 | value: number 299 | ): void; 300 | /** 301 | * Iterate through gamepads and handle interactions 302 | */ 303 | checkGamepadInput(): void; 304 | /** 305 | * Function to run on pointer move. 306 | * @param {*} pointer - The pointer object 307 | */ 308 | pointerMove(pointer: any, threshold: any): void; 309 | /** 310 | * Function to run on pointer down. Indicates that Mx has been pressed, which should be listened to by the player object 311 | * @param {*} pointer - The pointer object 312 | */ 313 | pointerDown(pointer: any): void; 314 | /** 315 | * Function to run on pointer up. Indicates that Mx has been released, which should be listened to by the player object 316 | * @param {*} pointer - The pointer object 317 | */ 318 | pointerUp(pointer: any): void; 319 | /** 320 | * Get the bearing from a given angle 321 | * @param {float} angle - Angle to use 322 | * @param {number} numDirections - Number of possible directions (e.g. 4 for N/S/E/W) 323 | */ 324 | getBearingFromAngle( 325 | angle: number, 326 | numDirections: number, 327 | threshold: any 328 | ): any; 329 | /** 330 | * Given a bearing, return a direction object containing boolean flags for the four directions 331 | * @param {*} bearing 332 | */ 333 | mapBearingToDirections(bearing: any): { 334 | UP: number; 335 | DOWN: number; 336 | LEFT: number; 337 | RIGHT: number; 338 | BEARING: any; 339 | }; 340 | /** 341 | * Given a directions object, return the applicable bearing (8 way only) 342 | * @param {*} directions 343 | */ 344 | mapDirectionsToBearing(directions: any, threshold: any): Bearing; 345 | /** 346 | * Given a bearing, return the snapped angle in degrees 347 | * @param {*} bearing 348 | */ 349 | mapBearingToDegrees(bearing: any): any; 350 | destroy(): void; 351 | /** 352 | * Return debug object 353 | */ 354 | debug(): { 355 | input: {}; 356 | }; 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phaser3-merged-input", 3 | "version": "1.9.0", 4 | "description": "A Phaser 3 plugin to handle input from keyboard, gamepad & mouse, allowing for easy key definition and multiplayer input", 5 | "main": "src/main.js", 6 | "scripts": { 7 | "build": "webpack --config webpack.build.config.js", 8 | "demo": "webpack serve --config webpack.demo.config.js", 9 | "dev": "webpack serve --config webpack.demo.config.js", 10 | "test": "echo \"No test specified\"" 11 | }, 12 | "types": "main.d.ts", 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/garystanton/phaser3-merged-input.git" 16 | }, 17 | "keywords": [ 18 | "phaser", 19 | "phaser-plugin", 20 | "phaser3", 21 | "phaser3-plugin", 22 | "phaser3-gamepad", 23 | "gamepad" 24 | ], 25 | "author": { 26 | "name": "Gary Stanton", 27 | "url": "https://sleepyada.com" 28 | }, 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/garystanton/phaser3-merged-input/issues" 32 | }, 33 | "homepage": "https://github.com/garystanton/phaser3-merged-input#readme", 34 | "devDependencies": { 35 | "@babel/core": "^7.22.0", 36 | "@babel/preset-env": "^7.22.0", 37 | "babel-loader": "^8.3.0", 38 | "copy-webpack-plugin": "^13.0.0", 39 | "core-js": "^3.41.0", 40 | "dat.gui": "^0.7.9", 41 | "html-webpack-plugin": "^5.5.0", 42 | "phaser": "^3.60.0", 43 | "regenerator-runtime": "^0.14.1", 44 | "terser-webpack-plugin": "^5.3.14", 45 | "webpack": "^5.88.0", 46 | "webpack-cli": "^5.1.4", 47 | "webpack-dev-server": "^5.2.1" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/ButtonCombo.js: -------------------------------------------------------------------------------- 1 | import AdvanceKeyCombo from 'phaser/src/input/keyboard/combo/AdvanceKeyCombo.js'; 2 | import ResetKeyCombo from 'phaser/src/input/keyboard/combo/ResetKeyCombo.js'; 3 | 4 | export default class ButtonCombo extends Phaser.Input.Keyboard.KeyCombo { 5 | constructor(mergedInput, player, buttons, config) { 6 | super(mergedInput.systems.input.keyboard, buttons, config); 7 | 8 | this.player = player; 9 | this.mergedInput = mergedInput; 10 | this.keyCodes = buttons; // KeyCombo expects this to be an array of keycodes, we'll be checking against button names 11 | 12 | mergedInput.events.on('gamepad_buttondown', this.onButtonDown, this); 13 | this.current = this.keyCodes[0]; 14 | } 15 | 16 | onButtonDown(event) { 17 | if (this.matched || !this.enabled) { 18 | return; 19 | } 20 | 21 | var matched = this.ProcessButtonCombo(event, this); 22 | if (matched) { 23 | this.mergedInput.eventEmitter.emit('mergedInput', { combo: this, player: this.player, action: 'Button combo matched' }); 24 | this.mergedInput.events.emit('buttoncombomatch', { player: this.player, combo: this }); 25 | 26 | if (this.resetOnMatch) { 27 | ResetKeyCombo(this); 28 | } 29 | else if (this.deleteOnMatch) { 30 | this.destroy(); 31 | } 32 | } 33 | } 34 | 35 | ProcessButtonCombo (event, combo) { 36 | // Set a timestamp from the gamepad 37 | event.timeStamp = this.mergedInput.systems.time.now 38 | 39 | // Don't check buttons on a different pad 40 | if (combo.player.index !== event.player) { 41 | return false; 42 | } 43 | 44 | // Check matched 45 | if (combo.matched) { 46 | return true; 47 | } 48 | 49 | // Compare the current action with the button pressed 50 | let buttonMatch = false; 51 | if (event.button === combo.current) { 52 | buttonMatch = true; 53 | } 54 | 55 | let mappedButton = this.mergedInput.getMappedButton(combo.player, event.button); 56 | if (mappedButton === combo.current) { 57 | buttonMatch = true; 58 | } 59 | 60 | let unMappedButton = this.mergedInput.getUnmappedButton(combo.player, mappedButton); 61 | if (unMappedButton === combo.current) { 62 | buttonMatch = true; 63 | } 64 | 65 | var comboMatched = false; 66 | var keyMatched = false; 67 | 68 | if (buttonMatch) { 69 | // Button was correct 70 | 71 | if (combo.index > 0 && combo.maxKeyDelay > 0) { 72 | // We have to check to see if the delay between 73 | // the new key and the old one was too long (if enabled) 74 | 75 | var timeLimit = combo.timeLastMatched + combo.maxKeyDelay; 76 | 77 | // Check if they pressed it in time or not 78 | if (event.timeStamp <= timeLimit) { 79 | keyMatched = true; 80 | comboMatched = AdvanceKeyCombo(event, combo); 81 | } 82 | } 83 | else { 84 | keyMatched = true; 85 | 86 | // We don't check the time for the first key pressed, so just advance it 87 | comboMatched = AdvanceKeyCombo(event, combo); 88 | } 89 | } 90 | 91 | if (!keyMatched && combo.resetOnWrongKey) { 92 | // Wrong key was pressed 93 | combo.index = 0; 94 | combo.current = combo.keyCodes[0]; 95 | } 96 | 97 | if (comboMatched) { 98 | combo.timeLastMatched = event.timeStamp; 99 | combo.matched = true; 100 | combo.timeMatched = event.timeStamp; 101 | } 102 | 103 | return comboMatched; 104 | }; 105 | 106 | 107 | destroy() { 108 | this.mergedInput.events.off('gamepad_buttondown', this.onButtonDown); 109 | super.destroy(); 110 | } 111 | } -------------------------------------------------------------------------------- /src/configs/bearings.js: -------------------------------------------------------------------------------- 1 | const bearings = { 2 | '-180': 'W', 3 | '-168.75': 'WBN', 4 | '-157.5': 'WNW', 5 | '-146.25': 'NWBW', 6 | '-135': 'NW', 7 | '-123.75': 'NWBN', 8 | '-112.5': 'NNW', 9 | '-101.25': 'NBW', 10 | '-90': 'N', 11 | '-78.75': 'NBE', 12 | '-67.5': 'NNE', 13 | '-56.25': 'NEBN', 14 | '-45': 'NE', 15 | '-33.75': 'NEBE', 16 | '-22.5': 'ENE', 17 | '-11.25': 'EBN', 18 | '0': 'E', 19 | '11.25': 'EBS', 20 | '22.5': 'ESE', 21 | '33.75': 'SEBE', 22 | '45': 'SE', 23 | '56.25': 'SEBS', 24 | '67.5': 'SSE', 25 | '78.75': 'SBE', 26 | '90': 'S', 27 | '101.25': 'SBW', 28 | '112.5': 'SSW', 29 | '123.75': 'SWBS', 30 | '135': 'SW', 31 | '146.25': 'SWBW', 32 | '157.5': 'WSW', 33 | '168.75': 'WBS', 34 | '180': 'W' 35 | }; 36 | 37 | module.exports = bearings; -------------------------------------------------------------------------------- /src/configs/pad_dualshock.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dualshock mapping 3 | */ 4 | module.exports = { 5 | padID: 'Dualshock', 6 | padType: 'Sony', 7 | gamepadMapping: { 8 | RC_S: 0, 9 | RC_E: 1, 10 | RC_W: 2, 11 | RC_N: 3, 12 | START: 9, // Options 13 | SELECT: 8, // Share 14 | LB: 4, 15 | RB: 5, 16 | LT: 6, 17 | RT: 7, 18 | LS: 10, 19 | RS: 11, 20 | LC_N: 12, 21 | LC_S: 13, 22 | LC_W: 14, 23 | LC_E: 15, 24 | MENU: 16, 25 | TOUCH: 17 26 | }, 27 | } -------------------------------------------------------------------------------- /src/configs/pad_generic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generic pad mapping 3 | */ 4 | module.exports = { 5 | padID: 'Generic', 6 | padType: 'generic', 7 | gamepadMapping: { 8 | RC_S: 0, 9 | RC_E: 1, 10 | RC_W: 2, 11 | RC_N: 3, 12 | START: 9, 13 | SELECT: 8, 14 | LB: 4, 15 | RB: 5, 16 | LT: 6, 17 | RT: 7, 18 | LS: 10, 19 | RS: 11, 20 | LC_N: 12, 21 | LC_S: 13, 22 | LC_W: 14, 23 | LC_E: 15 24 | }, 25 | } -------------------------------------------------------------------------------- /src/configs/pad_unlicensedSNES.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 081f-e401 - UnlicensedSNES 3 | */ 4 | module.exports = { 5 | padID: '081f-e401', 6 | padType: 'snes', 7 | gamepadMapping : { 8 | RC_S: 2, 9 | RC_E: 1, 10 | RC_W: 3, 11 | RC_N: 0, 12 | START: 9, 13 | SELECT: 8, 14 | LB: 4, 15 | RB: 5, 16 | LC_N: 12, 17 | LC_S: 13, 18 | LC_W: 14, 19 | LC_E: 15 20 | } 21 | } -------------------------------------------------------------------------------- /src/configs/pad_xbox360.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generic pad mapping 3 | */ 4 | module.exports = { 5 | padID: 'Xbox 360 controller (XInput STANDARD GAMEPAD)', 6 | padType: 'xbox', 7 | gamepadMapping: { 8 | RC_S: 0, 9 | RC_E: 1, 10 | RC_W: 2, 11 | RC_N: 3, 12 | START: 9, 13 | SELECT: 8, 14 | LB: 4, 15 | RB: 5, 16 | LT: 6, 17 | RT: 7, 18 | LS: 10, 19 | RS: 11, 20 | LC_N: 12, 21 | LC_S: 13, 22 | LC_W: 14, 23 | LC_E: 15, 24 | MENU: 16 25 | }, 26 | } -------------------------------------------------------------------------------- /src/controlManager.js: -------------------------------------------------------------------------------- 1 | import pad_generic from './configs/pad_generic' 2 | import pad_unlicensedSNES from './configs/pad_unlicensedSNES' 3 | import pad_xbox360 from './configs/pad_xbox360' 4 | import pad_dualshock from './configs/pad_dualshock' 5 | 6 | export default class controlManager { 7 | constructor (){ 8 | } 9 | 10 | mapGamepad(id) { 11 | id = id.toLowerCase(); 12 | let padConfig = pad_generic; 13 | 14 | if (id.includes('081f') && id.includes('e401')) { 15 | padConfig = pad_unlicensedSNES; 16 | } 17 | else if (id.includes('xbox') && id.includes('360')) { 18 | padConfig = pad_xbox360; 19 | } 20 | else if (id.includes('054c')) { 21 | padConfig = pad_dualshock; 22 | } 23 | else { 24 | 25 | } 26 | 27 | return padConfig; 28 | } 29 | 30 | getBaseControls() { 31 | return { 32 | 'direction': { 33 | 'UP': 0, 34 | 'DOWN': 0, 35 | 'LEFT': 0, 36 | 'RIGHT': 0, 37 | 'BEARING': '', 38 | 'BEARING_LAST': '', 39 | 'DEGREES': 0, 40 | 'DEGREES_LAST': 0, 41 | 'TIMESTAMP': 0 42 | }, 43 | 'direction_secondary': { 44 | 'UP': 0, 45 | 'DOWN': 0, 46 | 'LEFT': 0, 47 | 'RIGHT': 0, 48 | 'BEARING': '', 49 | 'DEGREES': 0, 50 | 'BEARING_LAST': '', 51 | 'DEGREES_LAST': 0, 52 | 'TIMESTAMP': 0 53 | }, 54 | 'buttons': {}, 55 | 'timers' : {}, 56 | 'gamepadMapping': { 57 | RC_S: 0, 58 | RC_E: 1, 59 | RC_W: 2, 60 | RC_N: 3, 61 | START: 9, 62 | SELECT: 8, 63 | LB: 4, 64 | RB: 5, 65 | LT: 6, 66 | RT: 7, 67 | LS: 10, 68 | RS: 11, 69 | LC_N: 12, 70 | LC_S: 13, 71 | LC_W: 14, 72 | LC_E: 15, 73 | MENU: 16 74 | }, 75 | 'pointer': { 76 | 'M1': 0, 77 | 'M2': 0, 78 | 'M3': 0, 79 | 'M4': 0, 80 | 'M5': 0, 81 | 'BEARING': '', 82 | 'BEARING_DEGREES': 0, 83 | 'ANGLE': 0, 84 | 'TIMESTAMP': 0 85 | }, 86 | 'position': {x:0,y:0}, 87 | 'position_last': {x:0,y:0}, 88 | 'gamepad': {}, 89 | 'keys': { 90 | 'UP': [], 91 | 'DOWN': [], 92 | 'LEFT': [], 93 | 'RIGHT': [], 94 | }, 95 | 'internal': { 96 | 'fakedpadBuffer': [], 97 | 'fakedpadPressed': [], 98 | 'fakedpadReleased': [], 99 | }, 100 | 'interaction': { 101 | 'buffer': [], 102 | 'pressed': [], 103 | 'released': [], 104 | 'last': '', 105 | 'lastPressed': '', 106 | 'lastReleased': '', 107 | 'device': '', 108 | }, 109 | 'interaction_mapped': { 110 | 'pressed': [], 111 | 'released': [], 112 | 'last': '', 113 | 'lastPressed': '', 114 | 'lastReleased': '', 115 | 'gamepadType': '', 116 | }, 117 | 'buttons_mapped': { 118 | RC_S: 0, 119 | RC_E: 0, 120 | RC_W: 0, 121 | RC_N: 0, 122 | START: 0, 123 | SELECT: 0, 124 | MENU: 0, 125 | LB: 0, 126 | RB: 0, 127 | LT: 0, 128 | RT: 0, 129 | LS: 0, 130 | RS: 0, 131 | LC_N: 0, 132 | LC_S: 0, 133 | LC_W: 0, 134 | LC_E: 0, 135 | } 136 | } 137 | } 138 | 139 | 140 | 141 | /** 142 | * Returns a struct to hold input control information 143 | * Set up a struct for each player in the game 144 | * Direction and Buttons contain the input from the devices 145 | * The keys struct contains arrays of keyboard characters that will trigger the action 146 | */ 147 | setupControls(numberOfButtons) { 148 | numberOfButtons = numberOfButtons || 16; 149 | 150 | let controls = this.getBaseControls(); 151 | 152 | // Add buttons 153 | for (let i = 0; i <= numberOfButtons; i++) { 154 | controls.buttons['B' + i] = 0; 155 | controls.keys['B' + i] = []; 156 | } 157 | 158 | // Add timers 159 | for (let i = 0; i <= numberOfButtons; i++) { 160 | controls.timers['B' + i] = { 161 | 'pressed': 0, 162 | 'released': 0, 163 | 'duration': 0 164 | }; 165 | } 166 | for (let thisDirection of ['UP', 'DOWN', 'LEFT', 'RIGHT', 'ALT_UP', 'ALT_DOWN', 'ALT_LEFT', 'ALT_RIGHT']) { 167 | controls.timers[thisDirection] = { 168 | 'pressed': 0, 169 | 'released': 0, 170 | 'duration': 0 171 | }; 172 | } 173 | 174 | for (let thisPointer of ['M1', 'M2', 'M3', 'M4', 'M5']) { 175 | controls.timers[thisPointer] = { 176 | 'pressed': 0, 177 | 'released': 0, 178 | 'duration': 0 179 | }; 180 | } 181 | 182 | 183 | controls.setPosition = function(x,y) { 184 | this.position.x = x; 185 | this.position.y = y; 186 | } 187 | 188 | 189 | return controls; 190 | } 191 | 192 | 193 | } 194 | -------------------------------------------------------------------------------- /src/demo/assets/gamepad.json: -------------------------------------------------------------------------------- 1 | { 2 | "textures": [ 3 | { 4 | "image": "gamepad.png", 5 | "format": "RGBA8888", 6 | "size": { 7 | "w": 96, 8 | "h": 1454 9 | }, 10 | "scale": 1, 11 | "frames": [ 12 | { 13 | "filename": "XboxOne_Dpad", 14 | "rotated": false, 15 | "trimmed": true, 16 | "sourceSize": { 17 | "w": 100, 18 | "h": 100 19 | }, 20 | "spriteSourceSize": { 21 | "x": 3, 22 | "y": 3, 23 | "w": 94, 24 | "h": 94 25 | }, 26 | "frame": { 27 | "x": 1, 28 | "y": 1, 29 | "w": 94, 30 | "h": 94 31 | } 32 | }, 33 | { 34 | "filename": "XboxOne_Dpad_Down", 35 | "rotated": false, 36 | "trimmed": true, 37 | "sourceSize": { 38 | "w": 100, 39 | "h": 100 40 | }, 41 | "spriteSourceSize": { 42 | "x": 3, 43 | "y": 3, 44 | "w": 94, 45 | "h": 94 46 | }, 47 | "frame": { 48 | "x": 1, 49 | "y": 97, 50 | "w": 94, 51 | "h": 94 52 | } 53 | }, 54 | { 55 | "filename": "XboxOne_Dpad_Left", 56 | "rotated": false, 57 | "trimmed": true, 58 | "sourceSize": { 59 | "w": 100, 60 | "h": 100 61 | }, 62 | "spriteSourceSize": { 63 | "x": 3, 64 | "y": 3, 65 | "w": 94, 66 | "h": 94 67 | }, 68 | "frame": { 69 | "x": 1, 70 | "y": 193, 71 | "w": 94, 72 | "h": 94 73 | } 74 | }, 75 | { 76 | "filename": "XboxOne_Dpad_Right", 77 | "rotated": false, 78 | "trimmed": true, 79 | "sourceSize": { 80 | "w": 100, 81 | "h": 100 82 | }, 83 | "spriteSourceSize": { 84 | "x": 3, 85 | "y": 3, 86 | "w": 94, 87 | "h": 94 88 | }, 89 | "frame": { 90 | "x": 1, 91 | "y": 289, 92 | "w": 94, 93 | "h": 94 94 | } 95 | }, 96 | { 97 | "filename": "XboxOne_Dpad_Up", 98 | "rotated": false, 99 | "trimmed": true, 100 | "sourceSize": { 101 | "w": 100, 102 | "h": 100 103 | }, 104 | "spriteSourceSize": { 105 | "x": 3, 106 | "y": 3, 107 | "w": 94, 108 | "h": 94 109 | }, 110 | "frame": { 111 | "x": 1, 112 | "y": 385, 113 | "w": 94, 114 | "h": 94 115 | } 116 | }, 117 | { 118 | "filename": "XboxOne_Left_Stick", 119 | "rotated": false, 120 | "trimmed": true, 121 | "sourceSize": { 122 | "w": 100, 123 | "h": 100 124 | }, 125 | "spriteSourceSize": { 126 | "x": 4, 127 | "y": 5, 128 | "w": 92, 129 | "h": 91 130 | }, 131 | "frame": { 132 | "x": 1, 133 | "y": 481, 134 | "w": 92, 135 | "h": 91 136 | } 137 | }, 138 | { 139 | "filename": "XboxOne_Right_Stick", 140 | "rotated": false, 141 | "trimmed": true, 142 | "sourceSize": { 143 | "w": 100, 144 | "h": 100 145 | }, 146 | "spriteSourceSize": { 147 | "x": 4, 148 | "y": 5, 149 | "w": 92, 150 | "h": 91 151 | }, 152 | "frame": { 153 | "x": 1, 154 | "y": 574, 155 | "w": 92, 156 | "h": 91 157 | } 158 | }, 159 | { 160 | "filename": "XboxOne_LB", 161 | "rotated": false, 162 | "trimmed": true, 163 | "sourceSize": { 164 | "w": 100, 165 | "h": 100 166 | }, 167 | "spriteSourceSize": { 168 | "x": 5, 169 | "y": 26, 170 | "w": 90, 171 | "h": 49 172 | }, 173 | "frame": { 174 | "x": 1, 175 | "y": 667, 176 | "w": 90, 177 | "h": 49 178 | } 179 | }, 180 | { 181 | "filename": "XboxOne_RB", 182 | "rotated": false, 183 | "trimmed": true, 184 | "sourceSize": { 185 | "w": 100, 186 | "h": 100 187 | }, 188 | "spriteSourceSize": { 189 | "x": 5, 190 | "y": 26, 191 | "w": 90, 192 | "h": 49 193 | }, 194 | "frame": { 195 | "x": 1, 196 | "y": 718, 197 | "w": 90, 198 | "h": 49 199 | } 200 | }, 201 | { 202 | "filename": "XboxOne_A", 203 | "rotated": false, 204 | "trimmed": true, 205 | "sourceSize": { 206 | "w": 100, 207 | "h": 100 208 | }, 209 | "spriteSourceSize": { 210 | "x": 8, 211 | "y": 8, 212 | "w": 84, 213 | "h": 84 214 | }, 215 | "frame": { 216 | "x": 1, 217 | "y": 769, 218 | "w": 84, 219 | "h": 84 220 | } 221 | }, 222 | { 223 | "filename": "XboxOne_B", 224 | "rotated": false, 225 | "trimmed": true, 226 | "sourceSize": { 227 | "w": 100, 228 | "h": 100 229 | }, 230 | "spriteSourceSize": { 231 | "x": 8, 232 | "y": 8, 233 | "w": 84, 234 | "h": 84 235 | }, 236 | "frame": { 237 | "x": 1, 238 | "y": 855, 239 | "w": 84, 240 | "h": 84 241 | } 242 | }, 243 | { 244 | "filename": "XboxOne_X", 245 | "rotated": false, 246 | "trimmed": true, 247 | "sourceSize": { 248 | "w": 100, 249 | "h": 100 250 | }, 251 | "spriteSourceSize": { 252 | "x": 8, 253 | "y": 8, 254 | "w": 84, 255 | "h": 84 256 | }, 257 | "frame": { 258 | "x": 1, 259 | "y": 941, 260 | "w": 84, 261 | "h": 84 262 | } 263 | }, 264 | { 265 | "filename": "XboxOne_Y", 266 | "rotated": false, 267 | "trimmed": true, 268 | "sourceSize": { 269 | "w": 100, 270 | "h": 100 271 | }, 272 | "spriteSourceSize": { 273 | "x": 8, 274 | "y": 8, 275 | "w": 84, 276 | "h": 84 277 | }, 278 | "frame": { 279 | "x": 1, 280 | "y": 1027, 281 | "w": 84, 282 | "h": 84 283 | } 284 | }, 285 | { 286 | "filename": "XboxOne_LT", 287 | "rotated": false, 288 | "trimmed": true, 289 | "sourceSize": { 290 | "w": 100, 291 | "h": 100 292 | }, 293 | "spriteSourceSize": { 294 | "x": 9, 295 | "y": 6, 296 | "w": 82, 297 | "h": 88 298 | }, 299 | "frame": { 300 | "x": 1, 301 | "y": 1113, 302 | "w": 82, 303 | "h": 88 304 | } 305 | }, 306 | { 307 | "filename": "XboxOne_RT", 308 | "rotated": false, 309 | "trimmed": true, 310 | "sourceSize": { 311 | "w": 100, 312 | "h": 100 313 | }, 314 | "spriteSourceSize": { 315 | "x": 9, 316 | "y": 6, 317 | "w": 82, 318 | "h": 88 319 | }, 320 | "frame": { 321 | "x": 1, 322 | "y": 1203, 323 | "w": 82, 324 | "h": 88 325 | } 326 | }, 327 | { 328 | "filename": "XboxOne_Menu", 329 | "rotated": false, 330 | "trimmed": true, 331 | "sourceSize": { 332 | "w": 100, 333 | "h": 100 334 | }, 335 | "spriteSourceSize": { 336 | "x": 10, 337 | "y": 11, 338 | "w": 80, 339 | "h": 79 340 | }, 341 | "frame": { 342 | "x": 1, 343 | "y": 1293, 344 | "w": 80, 345 | "h": 79 346 | } 347 | }, 348 | { 349 | "filename": "XboxOne_Windows", 350 | "rotated": false, 351 | "trimmed": true, 352 | "sourceSize": { 353 | "w": 100, 354 | "h": 100 355 | }, 356 | "spriteSourceSize": { 357 | "x": 10, 358 | "y": 11, 359 | "w": 80, 360 | "h": 79 361 | }, 362 | "frame": { 363 | "x": 1, 364 | "y": 1374, 365 | "w": 80, 366 | "h": 79 367 | } 368 | } 369 | ] 370 | } 371 | ], 372 | "meta": { 373 | "app": "https://www.codeandweb.com/texturepacker", 374 | "version": "3.0", 375 | "smartupdate": "$TexturePacker:SmartUpdate:f5f80bd0d6597954858ec9f87b373854:fe77e1ba7f34b0c980b2916d93901cd0:1c9586ece4449c8026f8e901f073f4a8$" 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /src/demo/assets/gamepad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GaryStanton/phaser3-merged-input/7366b5aa1be9241b841a93bbe274130442c2fb5b/src/demo/assets/gamepad.png -------------------------------------------------------------------------------- /src/demo/button.js: -------------------------------------------------------------------------------- 1 | export default class circleButton extends Phaser.GameObjects.Container { 2 | /** 3 | * Generic button graphic 4 | * 5 | * @constructor 6 | * @param {object} config - The configuration for the object 7 | * @param {Phaser.Scene} config.scene - The scene on which to add the button 8 | * @param {number} config.x - The horizontal coordinate relative to the scene viewport 9 | * @param {number} config.y - The vertical coordinate relative to the scene viewport 10 | * @param {number} config.text - Text to sit inside the button 11 | */ 12 | 13 | constructor(config) { 14 | // Assign some defaults for anything not passed 15 | let defaults = { 16 | x: 0 17 | , y: 0 18 | , text: '' 19 | }; 20 | config = Object.assign({}, defaults, config); 21 | 22 | super(config.scene, config.x, config.y, []); 23 | this.config = config; 24 | 25 | this.create(); 26 | } 27 | 28 | create() { 29 | // circle 30 | this.circle = this.scene.add.circle(0, 0, 20, 0x6666ff); 31 | this.add(this.circle); 32 | 33 | // Text 34 | if (this.config.text.toString.length > 0) { 35 | this.setText(this.config.text) 36 | } 37 | } 38 | 39 | /** 40 | * Set the text content of this button 41 | * @param {string} text 42 | */ 43 | setText(text) { 44 | if (typeof this.text !== 'undefined') { 45 | this.text.setText(text) 46 | } 47 | else { 48 | var style = { 49 | fontSize: '18px', 50 | fontFamily: 'Arial', 51 | color: '#ffffff', 52 | }; 53 | // The same again but specified in an object 54 | this.text = this.scene.add.text(0,0, text, style).setPadding({ x: 10, y: 10 }).setOrigin(0.5, 0.5) 55 | this.add(this.text) 56 | } 57 | 58 | return this; 59 | } 60 | } -------------------------------------------------------------------------------- /src/demo/debug.js: -------------------------------------------------------------------------------- 1 | import * as dat from 'dat.gui'; 2 | export default class Debug extends Phaser.Scene { 3 | /** 4 | * Debug scene 5 | * 6 | * @extends Phaser.Scene 7 | */ 8 | constructor() { 9 | super({ key: 'Debug' }); 10 | } 11 | 12 | /** 13 | * Init 14 | * @param {*} data 15 | */ 16 | init(data) { 17 | 18 | } 19 | 20 | preload() { 21 | 22 | } 23 | 24 | /** 25 | * @protected 26 | * @param {object} data Initialization parameters. 27 | */ 28 | create() { 29 | // Debug gui object 30 | this.gui = new dat.GUI(); 31 | 32 | // Reference to systems events emitter 33 | this.eventEmitter = this.scene.events; 34 | 35 | // Input 36 | this.inputController = this.scene.get('InputController'); 37 | this.inputType = ''; 38 | 39 | // Add controls to dat.gui 40 | this.buildGUI(this.inputController.mergedInput.debug().players[0], 'Player 1'); 41 | this.buildGUI(this.inputController.mergedInput.debug().players[1], 'Player 2'); 42 | 43 | // Merged input events occur on the scene that the plugin is associated with 44 | this.inputController.events.on('mergedInput', function(MIEvent){ 45 | if (MIEvent.action == 'Connected') { 46 | this.buildGUI(this.inputController.mergedInput.debug().input.gamepads[MIEvent.player], `Gamepad ${MIEvent.player}`); 47 | } 48 | }, this) 49 | } 50 | 51 | 52 | update() { 53 | } 54 | 55 | buildGUI(thisObject, folderName) { 56 | this.guiFolder = this.gui.addFolder(folderName); 57 | this.gui.remember(thisObject); 58 | this.addToGui(thisObject, this.guiFolder); 59 | } 60 | 61 | addToGui(obj, folder, parent) { 62 | if (parent !== undefined) { 63 | folder = parent.addFolder(folder); 64 | } 65 | for (const key in obj) { //for each key in your object 66 | if (obj.hasOwnProperty(key)) { 67 | let val = obj[key]; 68 | if (typeof val == 'number') { //if the value of the object key is a number, establish limits and step 69 | const numDigits = this.getNumDigits(val); 70 | let step, limit; 71 | if (val > -1 && val < 1) { //if it's a small decimal number, give it a GUI range of -1,1 with a step of 0.1... 72 | step = 0.1; 73 | limit = 1; 74 | } else { //otherwise, calculate the limits and step based on # of digits in the number 75 | const numDigits = this.getNumDigits(Math.round(val)); //to establish a step and limit, we'll use a base number that is an integer 76 | limit = Math.pow(10, numDigits); //make the limit one digit higher than the number of digits of the itself, i.e. '150' would have a range of -1000 to 1000... 77 | step = Math.pow(10, numDigits - 2); //...with a step one less than the number of digits, i.e. '10' 78 | } 79 | folder.add(obj, key, -limit, limit).step(step).listen(); //add the value to your GUI folder 80 | } else if (typeof val === 'object') { 81 | this.addToGui(val, key, folder); //if the key is an object itself, call this function again to loop through that subobject, assigning it to the same folder 82 | } else { 83 | folder.add(obj, key).listen(); //...this would include things like boolean values as checkboxes, and strings as text fields 84 | } 85 | } 86 | } 87 | } 88 | 89 | getNumDigits(val) { 90 | return (`${val}`.match(/\d/g) || []).length //a regex to compute the number of digits in a number. Note that decimals will get counted as digits, which is why to establish our limit and step we rounded 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/demo/demo.js: -------------------------------------------------------------------------------- 1 | import circlebutton from './button'; 2 | export default class Demo extends Phaser.Scene { 3 | 4 | preload() { 5 | // Input controller scene 6 | this.scene.launch('InputController') 7 | this.load.multiatlas('gamepad', 'assets/gamepad.json', 'assets'); 8 | } 9 | 10 | create() { 11 | // Get the input controller scene, which contains our player objects 12 | this.inputController = this.scene.get('InputController'); 13 | 14 | // Get our player objects 15 | this.player1 = this.inputController.player1; 16 | this.player2 = this.inputController.player2; 17 | 18 | // Set up an array of player objects 19 | this.players = [this.player1, this.player2] 20 | 21 | 22 | // Set up some gamepad sprites for testing 23 | // We can check for button numbers, or mapped button names. For the sprites, we'll use mapped button names. 24 | this.player1.sprites = { 25 | 'dpad': this.add.image(100, 350, 'gamepad', 'XboxOne_Dpad').setScale(2), 26 | 'RC_S': this.add.image(370, 420, 'gamepad', 'XboxOne_A'), 27 | 'RC_E': this.add.image(440, 350, 'gamepad', 'XboxOne_B'), 28 | 'RC_W': this.add.image(300, 350, 'gamepad', 'XboxOne_X'), 29 | 'RC_N': this.add.image(370, 280, 'gamepad', 'XboxOne_Y'), 30 | 'LB': this.add.image(70, 200, 'gamepad', 'XboxOne_LB'), 31 | 'RB': this.add.image(430, 200, 'gamepad', 'XboxOne_RB'), 32 | 'LT': this.add.image(70, 130, 'gamepad', 'XboxOne_LT'), 33 | 'RT': this.add.image(430, 130, 'gamepad', 'XboxOne_RT') 34 | } 35 | 36 | this.player2.sprites = { 37 | 'dpad': this.add.image(800, 350, 'gamepad', 'XboxOne_Dpad').setScale(2), 38 | 'RC_S': this.add.image(1070, 420, 'gamepad', 'XboxOne_A'), 39 | 'RC_E': this.add.image(1140, 350, 'gamepad', 'XboxOne_B'), 40 | 'RC_W': this.add.image(1000, 350, 'gamepad', 'XboxOne_X'), 41 | 'RC_N': this.add.image(1070, 280, 'gamepad', 'XboxOne_Y'), 42 | 'LB': this.add.image(770, 200, 'gamepad', 'XboxOne_LB'), 43 | 'RB': this.add.image(1130, 200, 'gamepad', 'XboxOne_RB'), 44 | 'LT': this.add.image(770, 130, 'gamepad', 'XboxOne_LT'), 45 | 'RT': this.add.image(1130, 130, 'gamepad', 'XboxOne_RT') 46 | } 47 | 48 | // Now we'll add some graphics to represent the actual button numbers. 49 | // How these map to friendly button names may differ per device, so the plugin attempts to map them according to the pad ID. 50 | this.player1.buttonGraphics = {} 51 | for (let i=0; i<=16; i++) { 52 | this.player1.buttonGraphics['B' + i] = new circlebutton({scene: this, x:100 + (i > 7 ? (i - 8.5) * 50 : (i * 50)), y:i > 7 ? 550 : 500, text:i}); 53 | this.add.existing(this.player1.buttonGraphics['B' + i]) 54 | } 55 | 56 | this.player2.buttonGraphics = {} 57 | for (let i=0; i<=16; i++) { 58 | this.player2.buttonGraphics['B' + i] = new circlebutton({scene: this, x:790 + (i > 7 ? (i - 8.5) * 50 : (i * 50)), y:i > 7 ? 550 : 500, text:i}); 59 | this.add.existing(this.player2.buttonGraphics['B' + i]) 60 | } 61 | 62 | 63 | // Set up some debug text 64 | this.player1Text = this.add.text(50, 600, '', { 65 | fontFamily: 'Arial', 66 | fontSize: 14, 67 | color: '#00ff00' 68 | }); 69 | 70 | this.player1Combos = this.add.text(50, 730, '', { 71 | fontFamily: 'Arial', 72 | fontSize: 16, 73 | color: '#00ff00' 74 | }); 75 | 76 | this.player2Text = this.add.text(50, 800, '', { 77 | fontFamily: 'Arial', 78 | fontSize: 14, 79 | color: '#00ff00' 80 | }); 81 | 82 | this.player2Combos = this.add.text(50, 930, '', { 83 | fontFamily: 'Arial', 84 | fontSize: 16, 85 | color: '#00ff00' 86 | }); 87 | 88 | 89 | // Instructions 90 | this.instructions1 = this.add.text(50, 20, ['Directions: WASD', 'Buttons: 1-0'], { 91 | fontFamily: 'Arial', 92 | fontSize: 14, 93 | color: '#00ff00' 94 | }); 95 | this.instructions1 = this.add.text(740, 20, ['Directions: Cursors', 'Buttons: Numpad 1-0'], { 96 | fontFamily: 'Arial', 97 | fontSize: 14, 98 | color: '#00ff00' 99 | }); 100 | 101 | // Set a position for the player 102 | this.player1.setPosition(this.cameras.main.centerX, this.cameras.main.centerY); 103 | 104 | 105 | 106 | /** 107 | * Some examples of creating button combos 108 | */ 109 | this.input.keyboard.createCombo([38, 38, 40, 40, 37, 39, 37, 39, 66, 65], { resetOnMatch: true }).name = 'Konami code - Keyboard'; 110 | this.inputController.mergedInput.createButtonCombo(this.player1, ['UP', 'UP', 'DOWN', 'DOWN', 'LEFT', 'RIGHT', 'LEFT', 'RIGHT', 'RC_E', 'RC_S'], { resetOnMatch: true }).name = 'Konami code - Gamepad'; 111 | this.inputController.mergedInput.createButtonCombo(this.player1, ['B12', 'B13'], { resetOnMatch: true, maxKeyDelay: 1000, }).name = 'Button ID test'; 112 | 113 | this.input.keyboard.on('keycombomatch', event => { 114 | this.player1Combos.setText(`KEY COMBO: ${event.name}`) 115 | console.log(`${event.name} entered!`); 116 | }); 117 | 118 | this.inputController.mergedInput.events.on('buttoncombomatch', event => { 119 | this[`player${event.player.index + 1}Combos`].setText(`BUTTON COMBO: ${event.combo.name}`) 120 | console.log(`${event.combo.name} entered! - Player: ${event.player.index}`); 121 | }); 122 | 123 | } 124 | 125 | update() { 126 | // Loop through player objects 127 | for (let thisPlayer of this.players) { 128 | // Reset dpad frame 129 | thisPlayer.sprites.dpad.setFrame('XboxOne_Dpad'); 130 | 131 | // Show dpad frame for direction input. (Diagonal input is supported, but can't easily be shown with these sprites) 132 | if (thisPlayer.direction.UP > 0) { 133 | thisPlayer.sprites.dpad.setFrame('XboxOne_Dpad_Up'); 134 | } 135 | if (thisPlayer.direction.RIGHT > 0) { 136 | thisPlayer.sprites.dpad.setFrame('XboxOne_Dpad_Right'); 137 | } 138 | if (thisPlayer.direction.DOWN > 0) { 139 | thisPlayer.sprites.dpad.setFrame('XboxOne_Dpad_Down'); 140 | } 141 | if (thisPlayer.direction.LEFT > 0) { 142 | thisPlayer.sprites.dpad.setFrame('XboxOne_Dpad_Left'); 143 | } 144 | 145 | 146 | // Check the button NUMBER values to correspond with the button graphics 147 | for (let thisButton in thisPlayer.buttons) { 148 | if (typeof thisPlayer.buttonGraphics[thisButton] !== 'undefined') { 149 | if (thisPlayer.buttons[thisButton] > 0) { 150 | thisPlayer.buttonGraphics[thisButton].circle.setFillStyle(0xcc0000, 1) 151 | } 152 | else { 153 | thisPlayer.buttonGraphics[thisButton].circle.setFillStyle(0x6666ff, 1) 154 | } 155 | } 156 | } 157 | 158 | // Check the MAPPED button values to correspond with the sprites we created 159 | for (let thisButton in thisPlayer.buttons_mapped) { 160 | if (typeof thisPlayer.sprites[thisButton] !== 'undefined') { 161 | if (thisPlayer.buttons_mapped[thisButton] > 0) { 162 | this.tintButton(thisPlayer, thisButton); 163 | } 164 | else { 165 | thisPlayer.sprites[thisButton].clearTint(); 166 | } 167 | } 168 | } 169 | } 170 | 171 | this.player1Text.setText([ 172 | 'Player 1', 'Gamepad: ' + (typeof this.player1.gamepad.index === 'undefined' ? 'Press a button to connect' : this.player1.gamepad.id), 173 | 'Directions: ' + JSON.stringify(this.player1.direction), 174 | 'Buttons: ' + JSON.stringify(this.player1.buttons), 175 | 'Mouse: ' + JSON.stringify(this.player1.pointer), 176 | 'Timers: ' + JSON.stringify(this.player1.timers), 177 | 'Interaction: ' + JSON.stringify(this.player1.interaction), 178 | `isDown: ${this.player1.isDown(Object.keys(this.player1.buttons_mapped))}, ${this.player1.isDown(Object.keys(this.player1.buttons))}`, 179 | 'Internal: ' + JSON.stringify(this.player1.internal) 180 | ]); 181 | this.player2Text.setText([ 182 | 'Player 2', 'Gamepad: ' + (typeof this.player2.gamepad.index === 'undefined' ? 'Press a button to connect' : this.player2.gamepad.id), 183 | 'Directions: ' + JSON.stringify(this.player2.direction), 184 | 'Buttons: ' + JSON.stringify(this.player2.buttons), 185 | 'Mouse: ' + JSON.stringify(this.player2.pointer), 186 | 'Timers: ' + JSON.stringify(this.player2.timers), 187 | 'Interaction: ' + JSON.stringify(this.player2.interaction), 188 | `isDown: ${this.player2.isDown(Object.keys(this.player2.buttons_mapped))}, ${this.player2.isDown(Object.keys(this.player2.buttons))}`, 189 | 'Internal: ' + JSON.stringify(this.player2.internal) 190 | ]); 191 | 192 | 193 | /** 194 | * Some logging of player helper functions 195 | */ 196 | /* 197 | // Here we check if certain buttons were pressed in this update step. 198 | if (this.player1.interaction_mapped.isPressed(['LC_N','START','RC_S','RC_N'])) { 199 | console.log(`mapped - isPressed: ${this.player1.interaction_mapped.isPressed(['LC_N', 'START', 'RC_S', 'RC_N'])}`) 200 | } 201 | 202 | // Here we check if certain buttons are held down in this update step. 203 | if (this.player1.interaction_mapped.isDown(['LC_N', 'RC_S', 'RC_N'])) { 204 | console.log(`mapped - isDown: ${this.player1.interaction_mapped.isDown(['LC_N', 'RC_S', 'RC_N'])}`) 205 | } 206 | 207 | // Here we check if certain buttons are held down for a given duration in this update step. 208 | if (this.player1.interaction_mapped.checkDown(['LC_N'], 1000)) { 209 | console.log(`mapped checkDown: ${this.player1.interaction_mapped.checkDown(['LC_N'], 1000)}`) 210 | } 211 | 212 | // Here we check if certain buttons are held down in this update step. 213 | if (this.player1.interaction.isPressed(['DOWN', 'B1'])) { 214 | console.log(`raw - isPressed: ${this.player1.interaction.isPressed(['DOWN', 'B1'])}`) 215 | } 216 | 217 | // Here we check if certain buttons are held down in this update step. 218 | if (this.player1.interaction.isDown(['DOWN', 'B1'])) { 219 | console.log(`raw - isDown: ${this.player1.interaction.isDown(['DOWN', 'B1'])}`) 220 | } 221 | 222 | // Here we check if certain buttons are held down for a given duration in this update step. 223 | if (this.player1.interaction.checkDown(['DOWN'], 1000, true)) { 224 | console.log(`raw checkDown: ${this.player1.interaction.checkDown(['DOWN'], 1000, true)}`) 225 | } 226 | 227 | 228 | // Generic button (mapped / unmapped) isPressed function 229 | if (this.player1.isPressed(['RIGHT', 'LC_W', 'B2'])) { 230 | console.log(`generic - isPressed: ${this.player1.isPressed(['RIGHT', 'LC_W', 'B2'])}`) 231 | } 232 | 233 | // Generic button (mapped / unmapped) isDown function 234 | if (this.player1.isDown(['RIGHT', 'LC_W', 'B2'])) { 235 | console.log(`generic - isDown: ${this.player1.isDown(['RIGHT', 'LC_W', 'B2'])}`) 236 | } 237 | 238 | // Generic button (mapped / unmapped) isReleased function 239 | if (this.player1.isReleased(['RIGHT', 'LC_W', 'B2'])) { 240 | console.log(`generic - isReleased: ${this.player1.isReleased(['RIGHT', 'LC_W', 'B2'])}`) 241 | } 242 | 243 | */ 244 | 245 | // Here we check if certain buttons are held down for a given duration in this update step. 246 | if (this.player1.checkDown(['M1','LEFT'], 1000, false)) { 247 | console.log(`generic checkDown: LEFT`) 248 | } 249 | 250 | // Mouse pointer check 251 | /* 252 | if (this.player1.isPressed(['M1', 'M2'])) { 253 | console.log(`isPressed: ${this.player1.interaction.isPressed(['M1', 'M2'])}`) 254 | } 255 | */ 256 | 257 | 258 | 259 | 260 | 261 | // this.debugView.value = this.inputController.mergedInput.debug().input; 262 | } 263 | 264 | tintButton(player, button){ 265 | player.sprites[button].setTint(0xff0000); 266 | } 267 | 268 | } 269 | -------------------------------------------------------------------------------- /src/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Merged input demo 7 | 8 | 9 | 16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /src/demo/inputController.js: -------------------------------------------------------------------------------- 1 | import MergedInput from "../main"; 2 | 3 | export default class InputController extends Phaser.Scene { 4 | /** 5 | * The Input scene is designed to run in the background and handle input. 6 | * We're thus able to reference the MergedInput plugin instance on this scene from multiple scenes within our project. 7 | * We could also use events in the global events registry to listen to input events on other scenes. 8 | * However, by making the plugin scene based, we're free to have multiple instances set up if we wish. 9 | * 10 | * @extends Phaser.Scene 11 | */ 12 | constructor() { 13 | super({key: 'InputController'}); 14 | } 15 | 16 | preload() { 17 | // Merged input plugin 18 | this.load.scenePlugin('mergedInput', MergedInput); 19 | 20 | /** 21 | * Here we'll launch a separate scene for debugging. 22 | **/ 23 | this.scene.launch('Debug') 24 | this.debugScene = this.scene.get('Debug'); 25 | } 26 | 27 | /** 28 | * @protected 29 | */ 30 | create() { 31 | // Setup player objects 32 | this.player1 = this.mergedInput.addPlayer(0); 33 | this.player2 = this.mergedInput.addPlayer(1); 34 | 35 | /** 36 | * Define keys (player, action, key, append) 37 | * This scene would also be a good place to handle updates of key definitions, so that a player can redefine keys or gamepad mappings. 38 | */ 39 | this.mergedInput 40 | .defineKey(0, 'UP', 'W') 41 | .defineKey(0, 'DOWN', 'S') 42 | .defineKey(0, 'LEFT', 'A') 43 | .defineKey(0, 'RIGHT', 'D') 44 | 45 | .defineKey(1, 'UP', 'UP') 46 | .defineKey(1, 'DOWN', 'DOWN') 47 | .defineKey(1, 'LEFT', 'LEFT') 48 | .defineKey(1, 'RIGHT', 'RIGHT') 49 | 50 | // We can define keys using friendly names - These map to button numbers behind the scenes, and attempt to do so for different types of gamepad 51 | .defineKey(0, 'RC_S', 'ONE') 52 | .defineKey(0, 'RC_E', 'TWO') 53 | .defineKey(0, 'RC_W', 'THREE') 54 | .defineKey(0, 'RC_N', 'FOUR') 55 | .defineKey(0, 'LB', 'FIVE') 56 | .defineKey(0, 'RB', 'SIX') 57 | .defineKey(0, 'LT', 'SEVEN') 58 | .defineKey(0, 'RT', 'EIGHT') 59 | .defineKey(0, 'START', 'NINE') 60 | .defineKey(0, 'SELECT', 'ZERO') 61 | 62 | .defineKey(0, 'B12', 'ESC') // Debug key 63 | 64 | // Or we can use the button numbers from our gamepad 65 | .defineKey(1, 'B0', 'NUMPAD_ONE') 66 | .defineKey(1, 'B1', 'NUMPAD_TWO') 67 | .defineKey(1, 'B2', 'NUMPAD_THREE') 68 | .defineKey(1, 'B3', 'NUMPAD_FOUR') 69 | .defineKey(1, 'B4', 'NUMPAD_FIVE') 70 | .defineKey(1, 'B5', 'NUMPAD_SIX') 71 | .defineKey(1, 'B6', 'NUMPAD_SEVEN') 72 | .defineKey(1, 'B7', 'NUMPAD_EIGHT') 73 | .defineKey(1, 'B8', 'NUMPAD_NINE') 74 | .defineKey(1, 'B9', 'NUMPAD_ZERO') 75 | ; 76 | } 77 | 78 | /** 79 | * We could handle input events in this scene's update method, or reference the instance of MergedInput from other scenes. 80 | **/ 81 | update(){ 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/demo/main.js: -------------------------------------------------------------------------------- 1 | import Demo from './demo.js'; 2 | import InputController from './inputController.js'; 3 | import Debug from './debug.js'; 4 | 5 | const config = { 6 | type: Phaser.AUTO, 7 | width: '100%', 8 | height: '100%', 9 | parent: 'game', 10 | input: { 11 | gamepad: true 12 | }, 13 | scene: [ 14 | Demo, InputController, Debug, 15 | ], 16 | dom: { 17 | createContainer: true 18 | } 19 | }; 20 | 21 | 22 | const game = new Phaser.Game(config); -------------------------------------------------------------------------------- /src/demo/merged-input-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GaryStanton/phaser3-merged-input/7366b5aa1be9241b841a93bbe274130442c2fb5b/src/demo/merged-input-demo.gif -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import bearings from './configs/bearings' 2 | import controlManager from './controlManager' 3 | import ButtonCombo from './ButtonCombo' 4 | 5 | export default class MergedInput extends Phaser.Plugins.ScenePlugin { 6 | 7 | /** 8 | * The Merged Input plugin is designed to run in the background and handle input. 9 | * Upon detecting a keypress or gamepad interaction, the plugin will update a player object and emit global events. 10 | * 11 | * @extends Phaser.Plugins.ScenePlugin 12 | * @param {*} scene 13 | * @param {*} pluginManager 14 | */ 15 | constructor(scene, pluginManager) { 16 | super(scene, pluginManager); 17 | this.scene = scene; 18 | 19 | // Players 20 | this.players = []; 21 | // Gamepads 22 | this.gamepads = []; 23 | // Keys object to store Phaser key objects. We'll check these during update 24 | this.keys = {}; 25 | 26 | this.bearings = bearings; 27 | 28 | this.dpadMappings = { 29 | 'UP': 12, 30 | 'DOWN': 13, 31 | 'LEFT': 14, 32 | 'RIGHT': 15 33 | } 34 | 35 | this.axisThreshold = this.axisThreshold; 36 | 37 | this.controlManager = new controlManager() 38 | } 39 | 40 | boot() { 41 | // Scene event emitter 42 | this.eventEmitter = this.systems.events; 43 | // Plugin event emitter 44 | this.events = new Phaser.Events.EventEmitter(); 45 | 46 | this.game.events.on(Phaser.Core.Events.PRE_STEP, this.preupdate, this); 47 | this.game.events.on(Phaser.Core.Events.POST_STEP, this.postupdate, this); 48 | // Handle the game losing focus 49 | this.game.events.on(Phaser.Core.Events.BLUR, () => { 50 | this.loseFocus() 51 | }) 52 | 53 | // Gamepad 54 | if (typeof this.systems.input.gamepad !== 'undefined') { 55 | this.systems.input.gamepad.on('connected', function (thisGamepad) { 56 | this.refreshGamepads(); 57 | this.setupGamepad(thisGamepad) 58 | }, this); 59 | 60 | // Check to see if the gamepad has already been setup by the browser 61 | this.systems.input.gamepad.refreshPads(); 62 | if (this.systems.input.gamepad.total) { 63 | this.refreshGamepads(); 64 | for (const thisGamepad of this.gamepads) { 65 | this.systems.input.gamepad.emit('connected', thisGamepad); 66 | } 67 | } 68 | 69 | this.systems.input.gamepad.on('down', this.gamepadButtonDown, this); 70 | this.systems.input.gamepad.on('up', this.gamepadButtonUp, this); 71 | } 72 | 73 | // Keyboard 74 | this.systems.input.keyboard.on('keydown', this.keyboardKeyDown, this); 75 | this.systems.input.keyboard.on('keyup', this.keyboardKeyUp, this); 76 | 77 | 78 | // Pointer 79 | this.systems.input.mouse.disableContextMenu(); 80 | } 81 | 82 | preupdate() { 83 | // Loop through players and handle input 84 | for (let thisPlayer of this.players) { 85 | // If the pointer hasn't moved, and the scene has changed, this can end up as undefined 86 | thisPlayer.pointer.BEARING = typeof thisPlayer.pointer.BEARING != 'undefined' ? thisPlayer.pointer.BEARING : ''; 87 | thisPlayer.pointer.BEARING_DEGREES = typeof thisPlayer.pointer.BEARING_DEGREES != 'undefined' ? thisPlayer.pointer.BEARING_DEGREES : 0; 88 | thisPlayer.pointer.ANGLE = typeof thisPlayer.pointer.ANGLE != 'undefined' ? thisPlayer.pointer.ANGLE : ''; 89 | thisPlayer.pointer.POINTERANGLE = typeof thisPlayer.pointer.POINTERANGLE != 'undefined' ? thisPlayer.pointer.POINTERANGLE : '' 90 | thisPlayer.pointer.POINTERDIRECTION = typeof thisPlayer.pointer.POINTERDIRECTION != 'undefined' ? thisPlayer.pointer.POINTERDIRECTION : '' 91 | thisPlayer.pointer.PLAYERPOS = typeof thisPlayer.pointer.PLAYERPOS != 'undefined' ? thisPlayer.pointer.PLAYERPOS : '' 92 | 93 | thisPlayer.direction.BEARING = this.mapDirectionsToBearing(thisPlayer.direction); 94 | thisPlayer.direction.BEARING_LAST = thisPlayer.direction.BEARING != '' ? thisPlayer.direction.BEARING : thisPlayer.direction.BEARING_LAST; 95 | thisPlayer.direction.DEGREES = thisPlayer.direction.BEARING != '' ? parseFloat(this.mapBearingToDegrees(thisPlayer.direction.BEARING)) : 0; 96 | thisPlayer.direction.DEGREES_LAST = thisPlayer.direction.BEARING_LAST != '' ? parseFloat(this.mapBearingToDegrees(thisPlayer.direction.BEARING_LAST)) : 0; 97 | thisPlayer.direction_secondary.BEARING = this.mapDirectionsToBearing(thisPlayer.direction_secondary); 98 | thisPlayer.direction_secondary.BEARING_LAST = thisPlayer.direction_secondary.BEARING != '' ? thisPlayer.direction_secondary.BEARING : thisPlayer.direction_secondary.BEARING_LAST; 99 | thisPlayer.direction_secondary.DEGREES = thisPlayer.direction_secondary.BEARING != '' ? parseFloat(this.mapBearingToDegrees(thisPlayer.direction_secondary.BEARING)) : 0; 100 | thisPlayer.direction_secondary.DEGREES_LAST = thisPlayer.direction_secondary.BEARING_LAST != '' ? parseFloat(this.mapBearingToDegrees(thisPlayer.direction_secondary.BEARING_LAST)) : 0; 101 | } 102 | 103 | 104 | // If the first player has moved, we want to update the pointer position 105 | if (typeof this.players[0] !== 'undefined') { 106 | if (this.players[0].position.x !== this.players[0].position_last.x || this.players[0].position.y !== this.players[0].position_last.y) { 107 | this.pointerMove(this.systems.input.activePointer); 108 | } 109 | } 110 | this.players[0].position_last.x = this.players[0].position.x; 111 | this.players[0].position_last.y = this.players[0].position.y; 112 | 113 | 114 | this.checkKeyboardInput(); 115 | this.checkGamepadInput(); 116 | this.checkPointerInput(); 117 | } 118 | 119 | postupdate() { 120 | // Loop through players and manage buffered input 121 | for (let thisPlayer of this.players) { 122 | // Clear the interaction buffer 123 | this.clearBuffer(thisPlayer); 124 | } 125 | } 126 | 127 | /** 128 | * Clear the interaction buffer for the given player 129 | * In the case of 'fake' DPad presses, we're using some convoluted buffers to keep the 'pressed' and 'released' values around for an extra tick 130 | * As they're created in this update loop, they're otherwise cleared before the consumer can use them. 131 | * @param {*} thisPlayer 132 | */ 133 | clearBuffer(thisPlayer) { 134 | if (thisPlayer.interaction.pressed.length > 0 && thisPlayer.internal.fakedpadPressed.length == 0) { 135 | thisPlayer.interaction.buffer = []; 136 | } 137 | if (thisPlayer.interaction.buffer.length == 0) { 138 | thisPlayer.interaction.pressed = []; 139 | thisPlayer.interaction_mapped.pressed = []; 140 | if (thisPlayer.internal.fakedpadReleased.length == 0) { 141 | thisPlayer.interaction.released = []; 142 | thisPlayer.interaction_mapped.released = []; 143 | } 144 | } 145 | 146 | thisPlayer.internal.fakedpadPressed = []; 147 | thisPlayer.internal.fakedpadReleased = []; 148 | } 149 | 150 | /** 151 | * Function to run when the game loses focus 152 | * We want to fake releasing the buttons here, so that they're not stuck down without an off event when focus returns to the game 153 | */ 154 | loseFocus() { 155 | // Loop through defined keys and reset them 156 | for (let thisKey in this.keys) { 157 | this.keys[thisKey].reset(); 158 | } 159 | } 160 | 161 | /** 162 | * Set up the gamepad and associate with a player object 163 | */ 164 | setupGamepad(thisGamepad) { 165 | this.eventEmitter.emit('mergedInput', { device: 'gamepad', id: thisGamepad.id, player: thisGamepad.index, action: 'Connected' }); 166 | this.events.emit('gamepad_connected', thisGamepad) 167 | 168 | if (typeof this.players[thisGamepad.index] === 'undefined') { 169 | this.addPlayer(); 170 | } 171 | 172 | let gamepadID = thisGamepad.id.toLowerCase(); 173 | this.players[thisGamepad.index].gamepad = thisGamepad; 174 | 175 | // Map the gamepad buttons 176 | let mappedPad = this.controlManager.mapGamepad(gamepadID); 177 | this.players[thisGamepad.index].gamepadMapping = mappedPad.gamepadMapping; 178 | this.players[thisGamepad.index].interaction_mapped.gamepadType = mappedPad.padType; 179 | for (let thisButton in this.players[thisGamepad.index].gamepadMapping) { 180 | this.players[thisGamepad.index].buttons_mapped[thisButton] = 0; 181 | } 182 | } 183 | 184 | /** 185 | * Set a threshold (between 0 and 1) below which analog stick input will be ignored 186 | * @param {*} value 187 | * @returns 188 | */ 189 | setAxisThreshold(value) { 190 | this.axisThreshold = value; 191 | return this; 192 | } 193 | 194 | 195 | refreshGamepads() { 196 | // Sometimes, gamepads are undefined. For some reason. 197 | this.gamepads = this.systems.input.gamepad.gamepads.filter(function (el) { 198 | return el != null; 199 | }); 200 | 201 | for (const [index, thisGamepad] of this.gamepads.entries()) { 202 | thisGamepad.index = index; // Overwrite the gamepad index, in case we had undefined gamepads earlier 203 | 204 | /** 205 | * Some cheap gamepads use the first axis as a dpad, in which case we won't have the dpad buttons 12-15 206 | */ 207 | thisGamepad.fakedpad = thisGamepad.buttons.length < 15; 208 | } 209 | } 210 | 211 | /** 212 | * Add a new player object to the players array 213 | * @param {number} index Player index - if a player object at this index already exists, it will be returned instead of creating a new player object 214 | * @param {number} numberOfButtons The number of buttons to assign to the player object. Defaults to 16. Fewer than 16 is not recommended, as gamepad DPads typically map to buttons 12-15 215 | */ 216 | addPlayer(index, numberOfButtons) { 217 | numberOfButtons = numberOfButtons || 16; 218 | if (typeof Number.isInteger(index) && typeof this.players[index] !== 'undefined') { 219 | return this.players[index]; 220 | } 221 | else { 222 | // Set up player object 223 | let newPlayer = this.controlManager.setupControls(numberOfButtons); 224 | 225 | // Add helper functions to the player object 226 | this.addPlayerHelperFunctions(newPlayer); 227 | 228 | // Push new player to players array 229 | this.players.push(newPlayer); 230 | 231 | this.players[this.players.length - 1].index = this.players.length - 1; 232 | 233 | // If this is the first player, add the pointer events 234 | if (this.players.length == 1) { 235 | this.systems.input.on('pointermove', function (pointer) { 236 | this.pointerMove(pointer); 237 | }, this); 238 | 239 | this.systems.input.on('pointerdown', function (pointer) { 240 | this.pointerDown(pointer); 241 | }, this); 242 | 243 | this.systems.input.on('pointerup', function (pointer) { 244 | this.pointerUp(pointer); 245 | }, this); 246 | } 247 | 248 | return this.players[this.players.length - 1]; 249 | } 250 | } 251 | 252 | /** 253 | * Add helper functions to the player object 254 | * @param {*} player 255 | */ 256 | addPlayerHelperFunctions(player) { 257 | /** 258 | * Pass a button name, or an array of button names to check if any were pressed in this update step. 259 | * This will only fire once per button press. If you need to check for a button being held down, use isDown instead. 260 | * Returns the name of the matched button(s), in case you need it. 261 | */ 262 | player.interaction.isPressed = (button) => { 263 | button = (typeof button === 'string') ? Array(button) : button; 264 | let matchedButtons = button.filter(x => player.interaction.pressed.includes(x)) 265 | return matchedButtons.length ? matchedButtons : false; 266 | }, 267 | 268 | /** 269 | * Pass a button name, or an array of button names to check if any are currently pressed in this update step. 270 | * This differs from the isPressed function in that it will return true if the button is currently pressed, even if it was pressed in a previous update step. 271 | * Returns the name of the matched button(s), in case you need it. 272 | */ 273 | player.interaction.isDown = (button) => { 274 | button = (typeof button === 'string') ? Array(button) : button; 275 | let matchedButtons = button.filter(x => player.buttons[x]) 276 | let matchedDirections = button.filter(x => player.direction[x]) 277 | let matchedPointer = button.filter(x => player.pointer[x]) 278 | let matchedAll = [...matchedButtons, ...matchedDirections, ...matchedPointer]; 279 | 280 | return matchedAll.length ? matchedAll : false; 281 | }, 282 | 283 | /** 284 | * Pass a button name, or an array of button names to check if any were released in this update step. 285 | * Returns the name of the matched button(s), in case you need it. 286 | */ 287 | player.interaction.isReleased = (button) => { 288 | button = (typeof button === 'string') ? Array(button) : button; 289 | let matchedButtons = button.filter(x => player.interaction.released.includes(x)) 290 | return matchedButtons.length ? matchedButtons : false; 291 | } 292 | 293 | /** 294 | * Pass a mapped button name, or an array of mapped button names to check if any were pressed in this update step. 295 | * This will only fire once per button press. If you need to check for a button being held down, use isDown instead. 296 | * Returns the name of the matched mapped button(s), in case you need it. 297 | */ 298 | player.interaction_mapped.isPressed = (button) => { 299 | button = (typeof button === 'string') ? Array(button) : button; 300 | let matchedButtons = button.filter(x => player.interaction_mapped.pressed.includes(x)) 301 | return matchedButtons.length ? matchedButtons : false; 302 | }, 303 | 304 | /** 305 | * Pass a mapped button name, or an array of mapped button names to check if any are currently pressed in this update step. 306 | * This differs from the isPressed function in that it will return true if the button is currently pressed, even if it was pressed in a previous update step. 307 | * Returns the name of the matched button(s), in case you need it. 308 | */ 309 | player.interaction_mapped.isDown = (button) => { 310 | button = (typeof button === 'string') ? Array(button) : button; 311 | let matchedButtons = button.filter(x => player.buttons_mapped[x]) 312 | return matchedButtons.length ? matchedButtons : false; 313 | }, 314 | 315 | /** 316 | * Pass a mapped button name, or an array of mapped button names to check if any were released in this update step. 317 | * Returns the name of the matched mapped button(s), in case you need it. 318 | */ 319 | player.interaction_mapped.isReleased = (button) => { 320 | button = (typeof button === 'string') ? Array(button) : button; 321 | let matchedButtons = button.filter(x => player.interaction_mapped.released.includes(x)) 322 | return matchedButtons.length ? matchedButtons : false; 323 | } 324 | 325 | /** 326 | * Pass a button name, or an array of button names to check if any are currently pressed in this update step. 327 | * Similar to Phaser's keyboard plugin, the checkDown function can accept a 'duration' parameter, and will only register a press once every X milliseconds. 328 | * Returns the name of the matched button(s) 329 | * 330 | * @param {string|array} button Array of buttons to check 331 | * @param {number} duration The duration which must have elapsed before this button is considered as being down. 332 | * @param {boolean} includeFirst - When true, the initial press of the button will be included in the results. Defaults to false. 333 | */ 334 | player.interaction.checkDown = (button, duration, includeFirst) => { 335 | if (includeFirst === undefined) { includeFirst = false; } 336 | if (duration === undefined) { duration = 0; } 337 | 338 | let matchedButtons = []; 339 | let downButtons = player.interaction.isDown(button) 340 | if (downButtons.length) { 341 | 342 | for (let thisButton of downButtons) { 343 | if (typeof player.timers[thisButton]._tick === 'undefined') { 344 | player.timers[thisButton]._tick = 0; 345 | if (includeFirst) { 346 | matchedButtons.push(thisButton); 347 | } 348 | } 349 | 350 | let t = Phaser.Math.Snap.Floor(this.scene.sys.time.now - player.timers[thisButton].pressed, duration); 351 | if (t > player.timers[thisButton]._tick) { 352 | this.game.events.once(Phaser.Core.Events.POST_STEP, ()=>{ 353 | player.timers[thisButton]._tick = t; 354 | }); 355 | matchedButtons.push(thisButton); 356 | } 357 | } 358 | } 359 | 360 | return matchedButtons.length ? matchedButtons : false; 361 | }, 362 | 363 | /** 364 | * Mapped version of the checkDown version - resolves mapped button names and calls the checkDown function 365 | */ 366 | player.interaction_mapped.checkDown = (button, duration, includeFirst) => { 367 | if (includeFirst === undefined) { includeFirst = false; } 368 | let unmappedButtons = []; 369 | 370 | // Resolve the unmapped button names to a new array 371 | for (let thisButton of button) { 372 | let unmappedButton = this.getUnmappedButton(player, thisButton); 373 | 374 | if (unmappedButton) { 375 | unmappedButtons.push(unmappedButton) 376 | } 377 | } 378 | 379 | let downButtons = player.interaction.checkDown(unmappedButtons, duration, includeFirst); 380 | return downButtons.length ? downButtons.map(x => this.getMappedButton(player, x)) : false; 381 | } 382 | 383 | 384 | /** 385 | * The previous functions are specific to the interaction and interaction_mapped definition of buttons. 386 | * In general you would pick a definition scheme and query that object (interaction or interaction_mapped), just for ease though, we'll add some functions that accept either type of convention 387 | */ 388 | 389 | /** 390 | * Pass a button name, or an array of button names to check if any were pressed in this update step. 391 | * This will only fire once per button press. If you need to check for a button being held down, use isDown instead. 392 | * Returns the name of the matched button(s), in case you need it. 393 | */ 394 | player.isPressed = (button) => { 395 | let interaction = player.interaction.isPressed(button) || []; 396 | let interaction_mapped = player.interaction_mapped.isPressed(button) || []; 397 | let matchedButtons = [...interaction, ...interaction_mapped]; 398 | return matchedButtons.length ? matchedButtons : false 399 | }, 400 | 401 | /** 402 | * Pass a button name, or an array of button names to check if any are currently pressed in this update step. 403 | * This differs from the isPressed function in that it will return true if the button is currently pressed, even if it was pressed in a previous update step. 404 | * Returns the name of the button(s), in case you need it. 405 | */ 406 | player.isDown = (button) => { 407 | let interaction = player.interaction.isDown(button) || []; 408 | let interaction_mapped = player.interaction_mapped.isDown(button) || []; 409 | let matchedButtons = [...interaction, ...interaction_mapped]; 410 | return matchedButtons.length ? matchedButtons : false 411 | }, 412 | 413 | /** 414 | * Pass a button name, or an array of button names to check if any were released in this update step. 415 | * Returns the name of the matched button(s), in case you need it. 416 | */ 417 | player.isReleased = (button) => { 418 | let interaction = player.interaction.isReleased(button) || []; 419 | let interaction_mapped = player.interaction_mapped.isReleased(button) || []; 420 | let matchedButtons = [...interaction, ...interaction_mapped]; 421 | return matchedButtons.length ? matchedButtons : false 422 | } 423 | 424 | 425 | /** 426 | * Pass a button name, or an array of button names to check if any are currently pressed in this update step. 427 | * Similar to Phaser's keyboard plugin, the checkDown function can accept a 'duration' parameter, and will only register a press once every X milliseconds. 428 | * Returns the name of the matched button(s) 429 | * 430 | * @param {string|array} button Array of buttons to check 431 | * @param {number} - The duration which must have elapsed before this button is considered as being down. 432 | */ 433 | player.checkDown = (button, duration, includeFirst) => { 434 | if (includeFirst === undefined) { includeFirst = false; } 435 | let interaction = player.interaction.checkDown(button, duration, includeFirst) || []; 436 | let interaction_mapped = player.interaction_mapped.checkDown(button, duration, includeFirst) || []; 437 | let matchedButtons = [...interaction, ...interaction_mapped]; 438 | return matchedButtons.length ? matchedButtons : false 439 | } 440 | 441 | 442 | player.setDevice = (device) => { 443 | if (player.interaction.device != device) { 444 | this.eventEmitter.emit('mergedInput', { device: device, player: player.index, action: 'Device Changed' }); 445 | this.events.emit('device_changed', { player: player.index, device: device }); 446 | } 447 | player.interaction.device = device; 448 | 449 | return this; 450 | } 451 | 452 | return this; 453 | } 454 | 455 | /** 456 | * Get player object 457 | * @param {number} index Player index 458 | */ 459 | getPlayer(index) { 460 | return typeof this.players[index] !== 'undefined' ? this.players[index] : '' 461 | } 462 | 463 | getPlayerIndexFromKey(key) { 464 | for (let thisPlayer of this.players) { 465 | // Loop through all the keys assigned to this player 466 | for (var thisKey in thisPlayer.keys) { 467 | for (var thisValue of thisPlayer.keys[thisKey]) { 468 | if (thisValue == key) { 469 | return thisPlayer.index; 470 | } 471 | } 472 | } 473 | } 474 | return -1; 475 | } 476 | 477 | getPlayerButtonFromKey(key) { 478 | for (let thisPlayer of this.players) { 479 | // Loop through all the keys assigned to this player 480 | for (var thisKey in thisPlayer.keys) { 481 | for (var thisValue of thisPlayer.keys[thisKey]) { 482 | if (thisValue == key) { 483 | // Now we have a matching button value, check to see if it's in our mapped buttons, in which case we want to return the button number it matches to 484 | if (typeof thisPlayer.gamepadMapping[thisKey] !== "undefined") { 485 | return 'B' + thisPlayer.gamepadMapping[thisKey]; 486 | } 487 | else { 488 | return thisKey; 489 | } 490 | } 491 | } 492 | } 493 | } 494 | return ''; 495 | } 496 | 497 | 498 | /** 499 | * Return an array of actions that a player may use 500 | * @param {number} player 501 | * @returns 502 | */ 503 | getPlayerActions(player) { 504 | let actions = ['UP', 'DOWN', 'LEFT', 'RIGHT', 'ALT_UP', 'ALT_DOWN', 'ALT_LEFT', 'ALT_RIGHT']; 505 | actions.push(...Object.keys(this.players[player].gamepadMapping)); 506 | actions.push(...Object.keys(this.players[player].buttons)); 507 | 508 | return actions; 509 | } 510 | 511 | /** 512 | * Given a player and a button ID, return the mapped button name, e.g. 0 = 'RC_S' (Right cluster, South - X on an xbox gamepad) 513 | * @param {*} player 514 | * @param {*} buttonID 515 | */ 516 | getMappedButton(player, buttonID) { 517 | buttonID = buttonID.toString().replace(/\D/g, ''); 518 | return Object.keys(player.gamepadMapping).find(key => player.gamepadMapping[key] == buttonID); 519 | } 520 | 521 | /** 522 | * Given a player and a mapped button name, return the button ID that it resolves to, e.g. 'RC_S' (Right cluster, South - X on an xbox gamepad) = B0. 523 | * This takes directions into account and will thus return 'LEFT' for LC_W, instead of the button ID that can be found in the gamepadMapping. 524 | * @param {*} player 525 | * @param {*} mappedButton 526 | */ 527 | getUnmappedButton(player, mappedButton) { 528 | let buttonNo = player.gamepadMapping[mappedButton]; 529 | let dpadMapping = this.dpadMappings; 530 | let direction = Object.keys(dpadMapping).find(key => dpadMapping[key] == buttonNo); 531 | 532 | return direction ? direction : 'B' + player.gamepadMapping[mappedButton]; 533 | } 534 | 535 | // Keyboard functions 536 | 537 | /** 538 | * Define a key for a player/action combination 539 | * @param {number} player The player on which we're defining a key 540 | * @param {string} action The action to define 541 | * @param {string} value The key to use 542 | * @param {boolean} append When true, this key definition will be appended to the existing key(s) for this action 543 | */ 544 | defineKey(player = 0, action, value, append = false) { 545 | // Set up a new player if none defined 546 | if (typeof this.players[player] === 'undefined') { 547 | this.addPlayer(); 548 | } 549 | 550 | if (this.getPlayerActions(player).includes(action)) { 551 | if (append && (typeof this.players[player].keys[action] !== 'undefined')) { 552 | this.players[player].keys[action].push([value]); 553 | } 554 | else { 555 | this.players[player].keys[action] = []; 556 | this.players[player].keys[action].push([value]); 557 | } 558 | 559 | this.keys[[value]] = this.systems.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes[value]); 560 | } 561 | 562 | return this; 563 | } 564 | 565 | /** 566 | * Iterate through players and check for interaction with defined keys 567 | */ 568 | checkKeyboardInput() { 569 | // Loop through players and check for keypresses 570 | for (let thisPlayer of this.players) { 571 | // Loop through all the keys assigned to this player 572 | for (var thisKey in thisPlayer.keys) { 573 | let action = 0; 574 | for (var thisValue of thisPlayer.keys[thisKey]) { 575 | // Check if the key is down 576 | action = (this.keys[thisValue].isDown) ? 1 : action; 577 | } 578 | 579 | // Set the action in the player object 580 | 581 | // Dpad 582 | if (['UP', 'DOWN', 'LEFT', 'RIGHT'].includes(thisKey)) { 583 | thisPlayer.direction[thisKey] = action; 584 | thisPlayer.direction.TIMESTAMP = this.scene.sys.time.now; 585 | } 586 | // Alternative direction 587 | else if (['ALT_UP', 'ALT_DOWN', 'ALT_LEFT', 'ALT_RIGHT'].includes(thisKey)) { 588 | thisPlayer.direction_secondary[thisKey.replace('ALT_', '')] = action; 589 | if (action == 1) { 590 | thisPlayer.direction_secondary.TIMESTAMP = this.scene.sys.time.now; 591 | } 592 | } 593 | // Friendly button names 594 | else if (thisKey in thisPlayer.gamepadMapping) { 595 | // Get the button number from the gamepad mapping 596 | thisPlayer.buttons['B' + thisPlayer.gamepadMapping[thisKey]] = action; 597 | thisPlayer.buttons_mapped[thisKey] = action; 598 | if (action == 1) { 599 | thisPlayer.buttons.TIMESTAMP = this.scene.sys.time.now; 600 | } 601 | } 602 | // Numbered buttons 603 | else { 604 | thisPlayer.buttons[thisKey] = action; 605 | if (action == 1) { 606 | thisPlayer.buttons.TIMESTAMP = this.scene.sys.time.now; 607 | } 608 | } 609 | 610 | // Set the latest interaction flag 611 | if (action == 1) { 612 | thisPlayer.setDevice('keyboard'); 613 | } 614 | } 615 | } 616 | } 617 | 618 | /** 619 | * When a keyboard button is pressed down, this function will emit a mergedInput event in the global registry. 620 | * The event contains a reference to the player assigned to the key, and passes a mapped action and value 621 | */ 622 | keyboardKeyDown(event) { 623 | let keyCode = Object.keys(Phaser.Input.Keyboard.KeyCodes).find(key => Phaser.Input.Keyboard.KeyCodes[key] === event.keyCode); 624 | let playerIndex = this.getPlayerIndexFromKey(keyCode); 625 | let playerAction = this.getPlayerButtonFromKey(keyCode); 626 | 627 | if (playerIndex > -1 && playerAction != '') { 628 | let thisPlayer = this.getPlayer(playerIndex); 629 | this.eventEmitter.emit('mergedInput', { device: 'keyboard', value: 1, player: playerIndex, action: keyCode, state: 'DOWN' }); 630 | this.events.emit('keyboard_keydown', { player: playerIndex, key: keyCode }); 631 | 632 | thisPlayer.setDevice('keyboard'); 633 | thisPlayer.interaction.pressed.push(playerAction); 634 | thisPlayer.interaction.buffer.push(playerAction); 635 | thisPlayer.interaction.last = playerAction; 636 | thisPlayer.interaction.lastPressed = playerAction; 637 | 638 | // Update timers 639 | thisPlayer.timers[playerAction].pressed = this.scene.sys.time.now; 640 | thisPlayer.timers[playerAction].released = 0; 641 | thisPlayer.timers[playerAction].duration = 0; 642 | 643 | // Update mapped button object 644 | if (typeof this.dpadMappings[playerAction] !== "undefined") { 645 | playerAction = 'B' + this.dpadMappings[playerAction]; 646 | } 647 | if (typeof thisPlayer.buttons[playerAction] !== "undefined") { 648 | let mappedButton = this.getMappedButton(thisPlayer, playerAction); 649 | if (typeof mappedButton !== "undefined") { 650 | thisPlayer.buttons_mapped[mappedButton] = 1; 651 | thisPlayer.interaction_mapped.pressed.push(mappedButton); 652 | thisPlayer.interaction_mapped.last = mappedButton; 653 | thisPlayer.interaction_mapped.lastPressed = mappedButton; 654 | thisPlayer.interaction_mapped.gamepadType = 'keyboard'; 655 | } 656 | } 657 | } 658 | } 659 | 660 | /** 661 | * When a keyboard button is released, this function will emit a mergedInput event in the global registry. 662 | * The event contains a reference to the player assigned to the key, and passes a mapped action and value 663 | */ 664 | keyboardKeyUp(event) { 665 | let keyCode = Object.keys(Phaser.Input.Keyboard.KeyCodes).find(key => Phaser.Input.Keyboard.KeyCodes[key] === event.keyCode); 666 | let playerIndex = this.getPlayerIndexFromKey(keyCode); 667 | let playerAction = this.getPlayerButtonFromKey(keyCode); 668 | 669 | if (playerIndex > -1 && playerAction != '') { 670 | let thisPlayer = this.getPlayer(playerIndex); 671 | this.eventEmitter.emit('mergedInput', { device: 'keyboard', value: 1, player: playerIndex, action: keyCode, state: 'UP' }); 672 | this.events.emit('keyboard_keyup', { player: playerIndex, key: keyCode }); 673 | 674 | thisPlayer.setDevice('keyboard'); 675 | thisPlayer.interaction.released.push(playerAction); 676 | thisPlayer.interaction.lastReleased = playerAction; 677 | 678 | // Update timers 679 | thisPlayer.timers[playerAction].released = this.scene.sys.time.now; 680 | thisPlayer.timers[playerAction].duration = thisPlayer.timers[playerAction].released - thisPlayer.timers[playerAction].pressed; 681 | delete thisPlayer.timers[playerAction]._tick; 682 | 683 | // Update mapped button object 684 | if (typeof this.dpadMappings[playerAction] !== "undefined") { 685 | playerAction = 'B' + this.dpadMappings[playerAction]; 686 | } 687 | if (typeof thisPlayer.buttons[playerAction] !== "undefined") { 688 | let mappedButton = this.getMappedButton(thisPlayer, playerAction); 689 | if (typeof mappedButton !== "undefined") { 690 | thisPlayer.buttons_mapped[mappedButton] = 0; 691 | thisPlayer.interaction_mapped.released = mappedButton; 692 | thisPlayer.interaction_mapped.lastReleased = mappedButton; 693 | thisPlayer.interaction_mapped.gamepadType = 'keyboard'; 694 | } 695 | } 696 | } 697 | } 698 | 699 | 700 | /** 701 | * Iterate through players and check for interaction with defined pointer buttons 702 | */ 703 | checkPointerInput() { 704 | // Loop through players and check for button presses 705 | for (let thisPlayer of this.players) { 706 | // Loop through all the keys assigned to this player 707 | for (var thisKey in thisPlayer.keys) { 708 | for (var thisValue of thisPlayer.keys[thisKey]) { // Each definition for this key action 709 | if (['M1', 'M2', 'M3', 'M4', 'M5'].includes(thisValue[0])) { 710 | // Check to see if button is pressed (stored in P1, can't have two mice...) 711 | if (this.players[0].pointer[thisValue] == 1) { 712 | thisPlayer.buttons[thisKey] = 1; 713 | } 714 | } 715 | } 716 | } 717 | } 718 | } 719 | 720 | 721 | // Gamepad functions 722 | 723 | /** 724 | * When a gamepad button is pressed down, this function will emit a mergedInput event in the global registry. 725 | * The event contains a reference to the player assigned to the gamepad, and passes a mapped action and value 726 | * @param {number} index Button index 727 | * @param {number} value Button value 728 | * @param {Phaser.Input.Gamepad.Button} button Phaser Button object 729 | */ 730 | gamepadButtonDown(pad, button, value) { 731 | this.players[pad.index].setDevice('gamepad'); 732 | this.players[pad.index].buttons.TIMESTAMP = this.scene.sys.time.now; 733 | this.eventEmitter.emit('mergedInput', { device: 'gamepad', value: value, player: pad.index, action: 'B' + button.index, state: 'DOWN' }); 734 | this.events.emit('gamepad_buttondown', { player: pad.index, button: `B${button.index}` }); 735 | 736 | // Buttons 737 | if (![12, 13, 14, 15].includes(button.index)) { 738 | let playerAction = 'B' + button.index; 739 | 740 | // Update the last button state 741 | this.players[pad.index].interaction.pressed.push(playerAction); 742 | this.players[pad.index].interaction.last = playerAction; 743 | this.players[pad.index].interaction.lastPressed = playerAction; 744 | this.players[pad.index].interaction.buffer.push(playerAction); 745 | 746 | // Update timers 747 | this.players[pad.index].timers[playerAction].pressed = this.scene.sys.time.now; 748 | this.players[pad.index].timers[playerAction].released = 0; 749 | this.players[pad.index].timers[playerAction].duration = 0; 750 | 751 | // Update mapped button object 752 | let mappedButton = this.getMappedButton(this.players[pad.index], button.index); 753 | if (typeof mappedButton !== "undefined") { 754 | this.players[pad.index].interaction_mapped.pressed.push(mappedButton); 755 | this.players[pad.index].interaction_mapped.last = mappedButton; 756 | this.players[pad.index].interaction_mapped.lastPressed = mappedButton; 757 | } 758 | } 759 | // DPad 760 | else { 761 | let dpadMapping = this.dpadMappings; 762 | let direction = Object.keys(dpadMapping).find(key => dpadMapping[key] == button.index); 763 | this.eventEmitter.emit('mergedInput', { device: 'gamepad', value: 1, player: pad.index, action: direction, state: 'DOWN' }); 764 | this.events.emit('gamepad_directiondown', { player: pad.index, button: direction }); 765 | 766 | this.players[pad.index].interaction.pressed.push(direction); 767 | this.players[pad.index].interaction.last = direction; 768 | this.players[pad.index].interaction.lastPressed = direction; 769 | this.players[pad.index].interaction.buffer.push(direction); 770 | this.players[pad.index].direction.TIMESTAMP = this.scene.sys.time.now; 771 | 772 | // Update timers 773 | this.players[pad.index].timers[direction].pressed = this.scene.sys.time.now; 774 | this.players[pad.index].timers[direction].released = 0; 775 | this.players[pad.index].timers[direction].duration = 0; 776 | 777 | 778 | // Update mapped button object 779 | let mappedButton = this.getMappedButton(this.players[pad.index], button.index); 780 | if (typeof mappedButton !== "undefined") { 781 | this.players[pad.index].interaction_mapped.pressed.push(mappedButton); 782 | this.players[pad.index].interaction_mapped.last = mappedButton; 783 | this.players[pad.index].interaction_mapped.lastPressed = mappedButton; 784 | } 785 | } 786 | } 787 | 788 | /** 789 | * When a gamepad button is released, this function will emit a mergedInput event in the global registry. 790 | * The event contains a reference to the player assigned to the gamepad, and passes a mapped action and value 791 | * @param {number} index Button index 792 | * @param {number} value Button value 793 | * @param {Phaser.Input.Gamepad.Button} button Phaser Button object 794 | */ 795 | gamepadButtonUp(pad, button, value) { 796 | this.players[pad.index].setDevice('gamepad'); 797 | this.players[pad.index].buttons.TIMESTAMP = this.scene.sys.time.now; 798 | 799 | this.eventEmitter.emit('mergedInput', { device: 'gamepad', value: value, player: pad.index, action: 'B' + button.index, state: 'UP' }); 800 | this.events.emit('gamepad_buttonup', { player: pad.index, button: `B${button.index}` }); 801 | 802 | // Buttons 803 | if (![12, 13, 14, 15].includes(button.index)) { 804 | let playerAction = 'B' + button.index; 805 | 806 | // Update the last button state 807 | this.players[pad.index].interaction.released.push(playerAction); 808 | this.players[pad.index].interaction.lastReleased = playerAction; 809 | 810 | // Update timers 811 | this.players[pad.index].timers[playerAction].released = this.scene.sys.time.now; 812 | this.players[pad.index].timers[playerAction].duration = this.players[pad.index].timers[playerAction].released - this.players[pad.index].timers[playerAction].pressed; 813 | delete this.players[pad.index].timers[playerAction]._tick; 814 | 815 | // Update mapped button object 816 | let mappedButton = this.getMappedButton(this.players[pad.index], button.index); 817 | if (typeof mappedButton !== "undefined") { 818 | this.players[pad.index].interaction_mapped.released = mappedButton; 819 | this.players[pad.index].interaction_mapped.lastReleased = mappedButton; 820 | } 821 | } 822 | // DPad 823 | else { 824 | let dpadMapping = this.dpadMappings; 825 | let direction = Object.keys(dpadMapping).find(key => dpadMapping[key] == button.index); 826 | this.eventEmitter.emit('mergedInput', { device: 'gamepad', value: 1, player: pad.index, action: direction, state: 'UP' }); 827 | this.events.emit('gamepad_directionup', { player: pad.index, button: direction }); 828 | 829 | this.players[pad.index].interaction.released.push(direction); 830 | this.players[pad.index].interaction.lastReleased = direction; 831 | 832 | // Update timers 833 | this.players[pad.index].timers[direction].released = this.scene.sys.time.now; 834 | this.players[pad.index].timers[direction].duration = this.players[pad.index].timers[direction].released - this.players[pad.index].timers[direction].pressed; 835 | delete this.players[pad.index].timers[direction]._tick; 836 | 837 | // Update mapped button object 838 | let mappedButton = this.getMappedButton(this.players[pad.index], button.index); 839 | if (typeof mappedButton !== "undefined") { 840 | this.players[pad.index].interaction_mapped.released = mappedButton; 841 | this.players[pad.index].interaction_mapped.lastReleased = mappedButton; 842 | } 843 | } 844 | } 845 | 846 | /** 847 | * Some gamepads map dpads to axis, which are handled differently to buttons. 848 | * This function mimics a gamepad push and fires an event. 849 | * We also insert the direction into a buffer so that we know what buttons are pressed in the gamepadFakeDPadRelease function 850 | * We use an array for the buffer and pressed vars, as more than one button may be pressed at the same time, within the same step. 851 | */ 852 | gamepadFakeDPadPress(gamepad, direction) { 853 | if (!this.players[gamepad.index].internal.fakedpadBuffer.includes(direction)) { 854 | this.players[gamepad.index].internal.fakedpadBuffer.push(direction); 855 | this.players[gamepad.index].internal.fakedpadPressed.push(direction); 856 | 857 | let thisButton = new Phaser.Input.Gamepad.Button(gamepad, this.dpadMappings[direction]) 858 | thisButton.value = 1; 859 | thisButton.pressed = true; 860 | thisButton.events.emit('down', gamepad, thisButton, 1) 861 | } 862 | } 863 | 864 | /** 865 | * When the axis is blank, we know we've released all buttons. 866 | */ 867 | gamepadFakeDPadRelease(gamepad) { 868 | if (this.players[gamepad.index].internal.fakedpadBuffer.length > 0) { 869 | 870 | for (let direction of this.players[gamepad.index].internal.fakedpadBuffer) { 871 | this.players[gamepad.index].internal.fakedpadReleased = direction; 872 | 873 | let thisButton = new Phaser.Input.Gamepad.Button(gamepad, this.dpadMappings[direction]) 874 | thisButton.value = 0; 875 | thisButton.pressed = false; 876 | thisButton.events.emit('up', gamepad, thisButton, 0) 877 | } 878 | 879 | this.players[gamepad.index].internal.fakedpadBuffer = []; 880 | } 881 | } 882 | 883 | /** 884 | * Iterate through gamepads and handle interactions 885 | */ 886 | checkGamepadInput() { 887 | // Check for gamepad input 888 | for (var thisGamepad of this.gamepads) { 889 | 890 | // Set up a player if we don't have one, presumably due to race conditions in detecting gamepads 891 | if (typeof this.players[thisGamepad.index] === 'undefined') { 892 | this.addPlayer(); 893 | } 894 | 895 | let direction = ''; 896 | 897 | // Directions 898 | if (thisGamepad.leftStick.y < -this.axisThreshold) { 899 | this.players[thisGamepad.index].direction.UP = Math.abs(thisGamepad.leftStick.y) 900 | this.players[thisGamepad.index].direction.TIMESTAMP = this.scene.sys.time.now; 901 | 902 | if (thisGamepad.fakedpad) { 903 | this.gamepadFakeDPadPress(thisGamepad, 'UP'); 904 | direction = 'UP' 905 | } 906 | } 907 | else if (thisGamepad.leftStick.y > this.axisThreshold) { 908 | this.players[thisGamepad.index].direction.DOWN = thisGamepad.leftStick.y 909 | this.players[thisGamepad.index].direction.TIMESTAMP = this.scene.sys.time.now; 910 | 911 | if (thisGamepad.fakedpad) { 912 | this.gamepadFakeDPadPress(thisGamepad, 'DOWN'); 913 | direction = 'DOWN' 914 | } 915 | } 916 | else if (this.players[thisGamepad.index].interaction.device === 'gamepad') { 917 | // DPad 918 | this.players[thisGamepad.index].direction.UP = thisGamepad.up ? 1 : 0; 919 | this.players[thisGamepad.index].direction.DOWN = thisGamepad.down ? 1 : 0; 920 | } 921 | 922 | if (thisGamepad.leftStick.x < -this.axisThreshold) { 923 | this.players[thisGamepad.index].direction.LEFT = Math.abs(thisGamepad.leftStick.x) 924 | this.players[thisGamepad.index].direction.TIMESTAMP = this.scene.sys.time.now; 925 | 926 | if (thisGamepad.fakedpad) { 927 | this.gamepadFakeDPadPress(thisGamepad, 'LEFT'); 928 | direction = 'LEFT' 929 | } 930 | } 931 | else if (thisGamepad.leftStick.x > this.axisThreshold) { 932 | this.players[thisGamepad.index].direction.RIGHT = thisGamepad.leftStick.x 933 | this.players[thisGamepad.index].direction.TIMESTAMP = this.scene.sys.time.now; 934 | 935 | if (thisGamepad.fakedpad) { 936 | this.gamepadFakeDPadPress(thisGamepad, 'RIGHT'); 937 | direction = 'RIGHT' 938 | } 939 | } 940 | else if (this.players[thisGamepad.index].interaction.device === 'gamepad') { 941 | // DPad 942 | this.players[thisGamepad.index].direction.LEFT = thisGamepad.left ? 1 : 0; 943 | this.players[thisGamepad.index].direction.RIGHT = thisGamepad.right ? 1 : 0; 944 | } 945 | 946 | if (thisGamepad.fakedpad && direction == '') { 947 | this.gamepadFakeDPadRelease(thisGamepad); 948 | } 949 | 950 | // Secondary 951 | if (thisGamepad.rightStick.y < -this.axisThreshold) { 952 | this.players[thisGamepad.index].direction_secondary.UP = Math.abs(thisGamepad.rightStick.y) 953 | this.players[thisGamepad.index].direction_secondary.TIMESTAMP = this.scene.sys.time.now; 954 | } 955 | else if (thisGamepad.rightStick.y > this.axisThreshold) { 956 | this.players[thisGamepad.index].direction_secondary.DOWN = thisGamepad.rightStick.y 957 | this.players[thisGamepad.index].direction_secondary.TIMESTAMP = this.scene.sys.time.now; 958 | } 959 | else { 960 | this.players[thisGamepad.index].direction_secondary.UP = 0; 961 | this.players[thisGamepad.index].direction_secondary.DOWN = 0; 962 | } 963 | 964 | if (thisGamepad.rightStick.x < -this.axisThreshold) { 965 | this.players[thisGamepad.index].direction_secondary.LEFT = Math.abs(thisGamepad.rightStick.x) 966 | this.players[thisGamepad.index].direction_secondary.TIMESTAMP = this.scene.sys.time.now; 967 | } 968 | else if (thisGamepad.rightStick.x > this.axisThreshold) { 969 | this.players[thisGamepad.index].direction_secondary.RIGHT = thisGamepad.rightStick.x 970 | this.players[thisGamepad.index].direction_secondary.TIMESTAMP = this.scene.sys.time.now; 971 | } 972 | else { 973 | this.players[thisGamepad.index].direction_secondary.LEFT = 0; 974 | this.players[thisGamepad.index].direction_secondary.RIGHT = 0; 975 | } 976 | 977 | if (this.players[thisGamepad.index].interaction.device === 'gamepad') { 978 | // Buttons 979 | for (var b = 0; b < thisGamepad.buttons.length; b++) { 980 | let button = thisGamepad.buttons[b]; 981 | this.players[thisGamepad.index].buttons['B' + b] = button.value; 982 | // Get mapped name for this button number and artificially update the relevant buttons_mapped key 983 | let mappedButton = this.getMappedButton(this.players[thisGamepad.index], b); 984 | if (typeof mappedButton !== "undefined") { 985 | this.players[thisGamepad.index].buttons_mapped[mappedButton] = button.value; 986 | } 987 | } 988 | 989 | // If we're faking the d-pad, we won't have the extra buttons so we'll have to manually update the button objects 990 | if (thisGamepad.fakedpad) { 991 | if (direction == '') { 992 | this.players[thisGamepad.index].buttons['B12'] = 0; 993 | this.players[thisGamepad.index].buttons['B13'] = 0; 994 | this.players[thisGamepad.index].buttons['B14'] = 0; 995 | this.players[thisGamepad.index].buttons['B15'] = 0; 996 | this.players[thisGamepad.index].buttons_mapped[this.getMappedButton(this.players[thisGamepad.index], 'B12')] = 0; 997 | this.players[thisGamepad.index].buttons_mapped[this.getMappedButton(this.players[thisGamepad.index], 'B13')] = 0; 998 | this.players[thisGamepad.index].buttons_mapped[this.getMappedButton(this.players[thisGamepad.index], 'B14')] = 0; 999 | this.players[thisGamepad.index].buttons_mapped[this.getMappedButton(this.players[thisGamepad.index], 'B15')] = 0; 1000 | } 1001 | else { 1002 | this.players[thisGamepad.index].buttons['B' + this.dpadMappings[direction]] = 1; 1003 | let mappedButton = this.getMappedButton(this.players[thisGamepad.index], 'B' + this.dpadMappings[direction]); 1004 | this.players[thisGamepad.index].buttons_mapped[mappedButton] = 1; 1005 | } 1006 | } 1007 | } 1008 | } 1009 | } 1010 | 1011 | 1012 | /** 1013 | * Function to run on pointer move. 1014 | * @param {*} pointer - The pointer object 1015 | */ 1016 | pointerMove(pointer, threshold) { 1017 | if (this.players.length) { 1018 | threshold = threshold || -1; 1019 | if (pointer.distance > threshold) { 1020 | let pointerDirection = this.getBearingFromAngle(pointer.angle, 8); 1021 | 1022 | // If we've been given a player position, return bearings and angles 1023 | if (typeof this.players[0] !== 'undefined' && this.players[0].position.x !== 'undefined') { 1024 | 1025 | let position = this.players[0].position; 1026 | let angleToPointer = Phaser.Math.Angle.Between(position.x, position.y, pointer.x, pointer.y); 1027 | pointerDirection = this.getBearingFromAngle(angleToPointer, 8); 1028 | let pointerAngle = Number(this.mapBearingToDegrees(pointerDirection)); 1029 | 1030 | this.players[0].pointer.BEARING = pointerDirection; 1031 | this.players[0].pointer.ANGLE = angleToPointer; 1032 | this.players[0].pointer.BEARING_DEGREES = pointerAngle; 1033 | this.players[0].pointer.TIMESTAMP = this.scene.sys.time.now; 1034 | 1035 | this.players[0].pointer.POINTERANGLE = pointerAngle; 1036 | this.players[0].pointer.POINTERDIRECTION = pointerDirection; 1037 | this.players[0].pointer.PLAYERPOS = position; 1038 | } 1039 | } 1040 | } 1041 | } 1042 | 1043 | 1044 | /** 1045 | * Function to run on pointer down. Indicates that Mx has been pressed, which should be listened to by the player object 1046 | * @param {*} pointer - The pointer object 1047 | */ 1048 | pointerDown(pointer) { 1049 | if (this.players.length) { 1050 | let action = ''; 1051 | this.players[0].setDevice('pointer'); 1052 | if (pointer.leftButtonDown()) { 1053 | action = 'M1'; 1054 | } 1055 | if (pointer.rightButtonDown()) { 1056 | action = 'M2'; 1057 | } 1058 | if (pointer.middleButtonDown()) { 1059 | action = 'M3'; 1060 | } 1061 | if (pointer.backButtonDown()) { 1062 | action = 'M4'; 1063 | } 1064 | if (pointer.forwardButtonDown()) { 1065 | action = 'M5'; 1066 | } 1067 | 1068 | this.eventEmitter.emit('mergedInput', { device: 'pointer', value: 1, player: 0, action: action, state: 'DOWN' }); 1069 | this.events.emit('pointer_down', action); 1070 | 1071 | this.players[0].pointer[action] = 1; 1072 | 1073 | // Update the last button state 1074 | this.players[0].interaction.pressed.push(action); 1075 | this.players[0].interaction.last = action; 1076 | this.players[0].interaction.lastPressed = action; 1077 | this.players[0].interaction.buffer.push(action); 1078 | this.players[0].pointer.TIMESTAMP = pointer.moveTime; 1079 | 1080 | // Update timers 1081 | this.players[0].timers[action].pressed = this.scene.sys.time.now; 1082 | this.players[0].timers[action].released = 0; 1083 | this.players[0].timers[action].duration = 0; 1084 | } 1085 | } 1086 | 1087 | 1088 | /** 1089 | * Function to run on pointer up. Indicates that Mx has been released, which should be listened to by the player object 1090 | * @param {*} pointer - The pointer object 1091 | */ 1092 | pointerUp(pointer) { 1093 | if (this.players.length) { 1094 | let action = ''; 1095 | if (pointer.leftButtonReleased()) { 1096 | action = 'M1'; 1097 | } 1098 | if (pointer.rightButtonReleased()) { 1099 | action = 'M2'; 1100 | } 1101 | if (pointer.middleButtonReleased()) { 1102 | action = 'M3'; 1103 | } 1104 | if (pointer.backButtonReleased()) { 1105 | action = 'M4'; 1106 | } 1107 | if (pointer.forwardButtonReleased()) { 1108 | action = 'M5'; 1109 | } 1110 | 1111 | this.eventEmitter.emit('mergedInput', { device: 'pointer', value: 1, player: 0, action: action, state: 'UP' }); 1112 | this.events.emit('pointer_up', action); 1113 | 1114 | this.players[0].pointer[action] = 0; 1115 | this.players[0].interaction.released.push(action); 1116 | this.players[0].interaction.lastReleased = action; 1117 | this.players[0].pointer.TIMESTAMP = this.scene.sys.time.now; 1118 | 1119 | // Update timers 1120 | this.players[0].timers[action].released = this.scene.sys.time.now; 1121 | this.players[0].timers[action].duration = this.players[0].timers[action].released - this.players[0].timers[action].pressed; 1122 | delete this.players[0].timers[action]._tick; 1123 | } 1124 | } 1125 | 1126 | 1127 | /** 1128 | * Create new button combo. 1129 | * Combos extend Phaser's keyboard combo and mimic their functionality for gamepad/player combinations. 1130 | * If you requrie a keyboard entered combo, use the native Phaser.Input.Keyboard.KeyboardPlugin.createCombo function. 1131 | * 1132 | * @param {player} player - A player object. If more than one player should be able to execute the combo, you should create multiple buttonCombo instances. 1133 | * @param {(object[])} buttons - An array of buttons that comprise this combo. Use button IDs, mapped buttons or directions, e.g. ['UP', 'UP', 'DOWN', 'DOWN', 'LEFT', 'RIGHT', 'LEFT', 'RIGHT', 'RC_E', 'RC_S'] 1134 | * @param {Phaser.Types.Input.Keyboard.KeyComboConfig} [config] - A Key Combo configuration object. 1135 | */ 1136 | createButtonCombo(player, buttons, config) { 1137 | return new ButtonCombo(this, player, buttons, config); 1138 | } 1139 | 1140 | 1141 | /** 1142 | * Get the bearing from a given angle 1143 | * @param {float} angle - Angle to use 1144 | * @param {number} numDirections - Number of possible directions (e.g. 4 for N/S/E/W) 1145 | */ 1146 | getBearingFromAngle(angle, numDirections) { 1147 | numDirections = numDirections || 8; 1148 | 1149 | var snap_interval = Phaser.Math.PI2 / numDirections; 1150 | 1151 | var angleSnap = Phaser.Math.Snap.To(angle, snap_interval); 1152 | var angleSnapDeg = Phaser.Math.RadToDeg(angleSnap); 1153 | var angleSnapDir = this.bearings[angleSnapDeg]; 1154 | 1155 | return angleSnapDir; 1156 | } 1157 | 1158 | 1159 | /** 1160 | * Given a bearing, return a direction object containing boolean flags for the four directions 1161 | * @param {*} bearing 1162 | */ 1163 | mapBearingToDirections(bearing) { 1164 | let thisDirection = { 1165 | 'UP': 0, 1166 | 'DOWN': 0, 1167 | 'LEFT': 0, 1168 | 'RIGHT': 0, 1169 | 'BEARING': bearing.toUpperCase() 1170 | } 1171 | 1172 | if (bearing.toUpperCase().includes('W')) { 1173 | thisDirection.LEFT = 1; 1174 | } 1175 | if (bearing.toUpperCase().includes('E')) { 1176 | thisDirection.RIGHT = 1; 1177 | } 1178 | if (bearing.toUpperCase().includes('S')) { 1179 | thisDirection.DOWN = 1; 1180 | } 1181 | if (bearing.toUpperCase().includes('N')) { 1182 | thisDirection.UP = 1; 1183 | } 1184 | 1185 | return thisDirection; 1186 | } 1187 | 1188 | 1189 | /** 1190 | * Given a directions object, return the applicable bearing (8 way only) 1191 | * @param {*} directions 1192 | */ 1193 | mapDirectionsToBearing(directions, threshold) { 1194 | var threshold = threshold || -.5 1195 | if (directions.UP && !(directions.LEFT || directions.RIGHT)) { 1196 | return 'N'; 1197 | } 1198 | if (directions.RIGHT && directions.UP) { 1199 | return 'NE'; 1200 | } 1201 | if (directions.RIGHT && !(directions.UP || directions.DOWN)) { 1202 | return 'E'; 1203 | } 1204 | if (directions.RIGHT && directions.DOWN) { 1205 | return 'SE'; 1206 | } 1207 | if (directions.DOWN && !(directions.LEFT || directions.RIGHT)) { 1208 | return 'S'; 1209 | } 1210 | if (directions.LEFT && directions.DOWN) { 1211 | return 'SW'; 1212 | } 1213 | if (directions.LEFT && !(directions.UP || directions.DOWN)) { 1214 | return 'W'; 1215 | } 1216 | if (directions.LEFT && directions.UP) { 1217 | return 'NW'; 1218 | } 1219 | return ''; 1220 | } 1221 | 1222 | /** 1223 | * Given a bearing, return the snapped angle in degrees 1224 | * @param {*} bearing 1225 | */ 1226 | mapBearingToDegrees(bearing) { 1227 | if (bearing != '') { 1228 | return Object.keys(this.bearings).find(key => this.bearings[key] === bearing); 1229 | } 1230 | else { 1231 | return ''; 1232 | } 1233 | } 1234 | 1235 | destroy() { 1236 | this.shutdown(); 1237 | this.scene = undefined; 1238 | } 1239 | 1240 | /** 1241 | * Return debug object 1242 | */ 1243 | debug() { 1244 | // Debug variables 1245 | var debug = { 1246 | 'input': {} 1247 | }; 1248 | debug.input.gamepads = []; 1249 | 1250 | for (var i = 0; i < this.gamepads.length; i++) { 1251 | let pad = this.gamepads[i]; 1252 | let buttons = {}; 1253 | let axes = {}; 1254 | 1255 | for (var b = 0; b < pad.buttons.length; b++) { 1256 | let button = pad.buttons[b]; 1257 | buttons['B' + button.index] = button.value; 1258 | } 1259 | 1260 | for (var a = 0; a < pad.axes.length; a++) { 1261 | let axis = pad.axes[a]; 1262 | axes['A' + axis.index] = axis.getValue(); 1263 | } 1264 | 1265 | debug.input.gamepads.push({ 1266 | 'ID': pad.id, 1267 | 'Index': pad.index, 1268 | 'Buttons': buttons, 1269 | 'Axes': axes 1270 | }); 1271 | } 1272 | 1273 | debug.players = []; 1274 | for (let thisPlayer of this.players) { 1275 | debug.players.push({ 1276 | 'interaction': thisPlayer.interaction, 1277 | 'interaction_mapped': thisPlayer.interaction_mapped, 1278 | // 'device': thisPlayer.interaction.device, 1279 | 'buttons': thisPlayer.buttons, 1280 | 'buttons_mapped': thisPlayer.buttons_mapped, 1281 | 'timers': thisPlayer.timers, 1282 | 'pointer': thisPlayer.pointer, 1283 | 'direction': thisPlayer.direction, 1284 | 'direction_secondary': thisPlayer.direction_secondary, 1285 | 'keys': thisPlayer.keys 1286 | }) 1287 | } 1288 | 1289 | return debug; 1290 | } 1291 | } 1292 | -------------------------------------------------------------------------------- /webpack.build.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const webpack = require('webpack'); 4 | const path = require('path'); 5 | const TerserPlugin = require('terser-webpack-plugin'); 6 | 7 | module.exports = { 8 | mode: 'production', 9 | watch: false, 10 | context: `${__dirname}/src/`, 11 | entry: { 12 | MergedInput: './main.js', 13 | 'MergedInput.min': './main.js' 14 | }, 15 | 16 | output: { 17 | path: `${__dirname}/dist/`, 18 | filename: '[name].js', 19 | library: 'MergedInput', 20 | libraryTarget: 'umd', 21 | umdNamedDefine: true 22 | }, 23 | 24 | optimization: { 25 | minimize: true, 26 | minimizer: [ 27 | new TerserPlugin({ 28 | parallel: true, 29 | terserOptions: { 30 | compress: true, 31 | ecma: 5, 32 | output: { 33 | comments: false 34 | } 35 | } 36 | }) 37 | ] 38 | }, 39 | 40 | module: { 41 | rules: [{ 42 | test: /\.js$/, 43 | exclude: /node_modules/, 44 | use: [ 45 | { 46 | loader: 'babel-loader', 47 | options: { 48 | presets: ['@babel/preset-env'] // Updated preset 49 | } 50 | } 51 | ] 52 | }] 53 | }, 54 | optimization: { 55 | minimize: false // Let Uglify do this job for min-build only 56 | }, 57 | devtool: 'source-map', 58 | 59 | }; -------------------------------------------------------------------------------- /webpack.demo.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | var CopyWebpackPlugin = require('copy-webpack-plugin'); 5 | 6 | var definePlugin = new webpack.DefinePlugin({ 7 | __DEV__: JSON.stringify(JSON.parse(process.env.BUILD_DEV || 'true')), 8 | WEBGL_RENDERER: true, // I did this to make webpack work, but I'm not really sure it should always be true 9 | CANVAS_RENDERER: true // I did this to make webpack work, but I'm not really sure it should always be true 10 | }); 11 | 12 | module.exports = { 13 | mode: 'development', 14 | devServer: { 15 | static: path.resolve(__dirname, 'dev'), 16 | port: 3000, 17 | open: true, 18 | hot: true, 19 | server: 'https' 20 | }, 21 | entry: { 22 | customPlugin: './src/main.js', 23 | demo: [ 24 | 'core-js/stable', 25 | 'regenerator-runtime/runtime', 26 | path.resolve(__dirname, 'src/demo/main.js') 27 | ], 28 | vendor: ['phaser'] 29 | }, 30 | devtool: 'cheap-source-map', 31 | output: { 32 | pathinfo: true, 33 | path: path.resolve(__dirname, 'dev'), 34 | publicPath: '/', // Serve files from the root 35 | library: '[name]', 36 | libraryTarget: 'umd', 37 | filename: '[name].js' 38 | }, 39 | watch: true, 40 | plugins: [ 41 | definePlugin, 42 | new HtmlWebpackPlugin({ 43 | filename: 'index.html', // This will be placed in the output.path directory 44 | template: './src/demo/index.html', 45 | chunks: ['vendor', 'customPlugin', 'demo'], 46 | chunksSortMode: 'manual', 47 | minify: false, 48 | hash: false 49 | }), 50 | new CopyWebpackPlugin({ 51 | patterns: [ 52 | { 53 | from: 'src/demo/assets', 54 | to: 'assets' 55 | } 56 | ] 57 | }) 58 | ], 59 | module: { 60 | rules: [ 61 | { 62 | test: /\.js$/, 63 | exclude: /node_modules/, 64 | use: [ 65 | { 66 | loader: 'babel-loader', 67 | options: { 68 | presets: ['@babel/preset-env'] // Updated preset 69 | } 70 | } 71 | ] 72 | }, 73 | { 74 | test: /phaser-split\.js$/, 75 | use: ['expose-loader?Phaser'] 76 | }, 77 | { 78 | test: [/\.vert$/, /\.frag$/], 79 | use: 'raw-loader' 80 | } 81 | ] 82 | }, 83 | performance: { 84 | hints: false // Ignore warnings about large bundles as it really don't apply to games 85 | } 86 | }; --------------------------------------------------------------------------------