UNPKG

phaser3-merged-input

Version:

A Phaser 3 plugin to handle input from keyboard, gamepad & mouse, allowing for easy key definition and multiplayer input

1,237 lines (1,064 loc) 53.6 kB
import bearings from './configs/bearings' import controlManager from './controlManager' import ButtonCombo from './ButtonCombo' export default class MergedInput extends Phaser.Plugins.ScenePlugin { /** * The Merged Input plugin is designed to run in the background and handle input. * Upon detecting a keypress or gamepad interaction, the plugin will update a player object and emit global events. * * @extends Phaser.Plugins.ScenePlugin * @param {*} scene * @param {*} pluginManager */ constructor(scene, pluginManager) { super(scene, pluginManager); this.scene = scene; // Players this.players = []; // Gamepads this.gamepads = []; // Keys object to store Phaser key objects. We'll check these during update this.keys = {}; this.bearings = bearings; this.dpadMappings = { 'UP': 12, 'DOWN': 13, 'LEFT': 14, 'RIGHT': 15 } // A threshold (between 0 and 1) below which analog stick input will be ignored this.axisThreshold = 0; // The number of directions to snap to when mapping input to bearings (Defaults to 32) this.numDirections = Object.keys(this.bearings).length - 1; this.controlManager = new controlManager() } boot() { // Scene event emitter this.eventEmitter = this.systems.events; // Plugin event emitter this.events = new Phaser.Events.EventEmitter(); this.game.events.on(Phaser.Core.Events.PRE_STEP, this.preupdate, this); this.game.events.on(Phaser.Core.Events.POST_STEP, this.postupdate, this); // Handle the game losing focus this.game.events.on(Phaser.Core.Events.BLUR, () => { this.loseFocus() }) // Gamepad if (typeof this.systems.input.gamepad !== 'undefined') { this.systems.input.gamepad.on('connected', function (thisGamepad) { this.refreshGamepads(); this.setupGamepad(thisGamepad) }, this); // Check to see if the gamepad has already been setup by the browser this.systems.input.gamepad.refreshPads(); if (this.systems.input.gamepad.total) { this.refreshGamepads(); for (const thisGamepad of this.gamepads) { this.systems.input.gamepad.emit('connected', thisGamepad); } } this.systems.input.gamepad.on('down', this.gamepadButtonDown, this); this.systems.input.gamepad.on('up', this.gamepadButtonUp, this); } // Keyboard this.systems.input.keyboard.on('keydown', this.keyboardKeyDown, this); this.systems.input.keyboard.on('keyup', this.keyboardKeyUp, this); // Pointer this.systems.input.mouse.disableContextMenu(); } preupdate() { // If the first player has moved, we want to update the pointer position if (typeof this.players[0] !== 'undefined') { if (this.players[0].position.x !== this.players[0].position_last.x || this.players[0].position.y !== this.players[0].position_last.y) { this.pointerMove(this.systems.input.activePointer); } } this.players[0].position_last.x = this.players[0].position.x; this.players[0].position_last.y = this.players[0].position.y; this.checkKeyboardInput(); this.checkGamepadInput(); this.checkPointerInput(); // Loop through players and handle input for (let thisPlayer of this.players) { // If the pointer hasn't moved, and the scene has changed, this can end up as undefined thisPlayer.pointer.BEARING = typeof thisPlayer.pointer.BEARING != 'undefined' ? thisPlayer.pointer.BEARING : ''; thisPlayer.pointer.BEARING_DEGREES = typeof thisPlayer.pointer.BEARING_DEGREES != 'undefined' ? thisPlayer.pointer.BEARING_DEGREES : 0; thisPlayer.pointer.ANGLE = typeof thisPlayer.pointer.ANGLE != 'undefined' ? thisPlayer.pointer.ANGLE : ''; thisPlayer.pointer.DEGREES = typeof thisPlayer.pointer.DEGREES != 'undefined' ? thisPlayer.pointer.DEGREES : 0; thisPlayer.pointer.POINTERANGLE = typeof thisPlayer.pointer.POINTERANGLE != 'undefined' ? thisPlayer.pointer.POINTERANGLE : '' thisPlayer.pointer.POINTERDIRECTION = typeof thisPlayer.pointer.POINTERDIRECTION != 'undefined' ? thisPlayer.pointer.POINTERDIRECTION : '' thisPlayer.pointer.PLAYERPOS = typeof thisPlayer.pointer.PLAYERPOS != 'undefined' ? thisPlayer.pointer.PLAYERPOS : '' thisPlayer.direction.ANGLE = this.mapDirectionsToAngle(thisPlayer.direction); thisPlayer.direction.ANGLE_LAST = thisPlayer.direction.ANGLE != '' ? thisPlayer.direction.ANGLE : thisPlayer.direction.ANGLE_LAST; thisPlayer.direction.DEGREES = thisPlayer.direction.ANGLE !== -1 ? Math.round(Phaser.Math.RadToDeg(thisPlayer.direction.ANGLE) * 100) / 100 : -1; thisPlayer.direction.DEGREES_LAST = thisPlayer.direction.DEGREES != -1 ? thisPlayer.direction.DEGREES : thisPlayer.direction.DEGREES_LAST; thisPlayer.direction.BEARING = thisPlayer.direction.ANGLE !== -1 ? this.getBearingFromAngle(thisPlayer.direction.ANGLE) : ''; thisPlayer.direction.BEARING_LAST = thisPlayer.direction.BEARING != '' ? thisPlayer.direction.BEARING : thisPlayer.direction.BEARING_LAST; thisPlayer.direction.BEARING_DEGREES = thisPlayer.direction.BEARING != '' ? parseFloat(this.mapBearingToDegrees(thisPlayer.direction.BEARING)) : 0; thisPlayer.direction.BEARING_DEGREES_LAST = thisPlayer.direction.BEARING_LAST != '' ? parseFloat(this.mapBearingToDegrees(thisPlayer.direction.BEARING_LAST)) : 0; thisPlayer.direction_secondary.ANGLE = this.mapDirectionsToAngle(thisPlayer.direction_secondary); thisPlayer.direction_secondary.ANGLE_LAST = thisPlayer.direction_secondary.ANGLE != '' ? thisPlayer.direction_secondary.ANGLE : thisPlayer.direction_secondary.ANGLE_LAST; thisPlayer.direction_secondary.DEGREES = thisPlayer.direction_secondary.ANGLE !== -1 ? Math.round(Phaser.Math.RadToDeg(thisPlayer.direction_secondary.ANGLE) * 100) / 100 : -1; thisPlayer.direction_secondary.DEGREES_LAST = thisPlayer.direction_secondary.DEGREES != -1 ? thisPlayer.direction_secondary.DEGREES : thisPlayer.direction_secondary.DEGREES_LAST; thisPlayer.direction_secondary.BEARING = thisPlayer.direction_secondary.ANGLE !== -1 ? this.getBearingFromAngle(thisPlayer.direction_secondary.ANGLE) : ''; thisPlayer.direction_secondary.BEARING_LAST = thisPlayer.direction_secondary.BEARING != '' ? thisPlayer.direction_secondary.BEARING : thisPlayer.direction_secondary.BEARING_LAST; thisPlayer.direction_secondary.BEARING_DEGREES = thisPlayer.direction_secondary.BEARING != '' ? parseFloat(this.mapBearingToDegrees(thisPlayer.direction_secondary.BEARING)) : 0; thisPlayer.direction_secondary.BEARING_DEGREES_LAST = thisPlayer.direction_secondary.BEARING_LAST != '' ? parseFloat(this.mapBearingToDegrees(thisPlayer.direction_secondary.BEARING_LAST)) : 0; } } postupdate() { // Loop through players and manage buffered input for (let thisPlayer of this.players) { // Clear the interaction buffer this.clearBuffer(thisPlayer); } } /** * Clear the interaction buffer for the given player * 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 * As they're created in this update loop, they're otherwise cleared before the consumer can use them. * @param {*} thisPlayer */ clearBuffer(thisPlayer) { if (thisPlayer.interaction.pressed.length > 0 && thisPlayer.internal.fakedpadPressed.length == 0) { thisPlayer.interaction.buffer = []; } if (thisPlayer.interaction.buffer.length == 0) { thisPlayer.interaction.pressed = []; thisPlayer.interaction_mapped.pressed = []; if (thisPlayer.internal.fakedpadReleased.length == 0) { thisPlayer.interaction.released = []; thisPlayer.interaction_mapped.released = []; } } thisPlayer.internal.fakedpadPressed = []; thisPlayer.internal.fakedpadReleased = []; } /** * Function to run when the game loses focus * 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 */ loseFocus() { // Loop through defined keys and reset them for (let thisKey in this.keys) { this.keys[thisKey].reset(); } } /** * Set up the gamepad and associate with a player object */ setupGamepad(thisGamepad) { this.eventEmitter.emit('mergedInput', { device: 'gamepad', id: thisGamepad.id, player: thisGamepad.index, action: 'Connected' }); this.events.emit('gamepad_connected', thisGamepad) if (typeof this.players[thisGamepad.index] === 'undefined') { this.addPlayer(); } let gamepadID = thisGamepad.id.toLowerCase(); this.players[thisGamepad.index].gamepad = thisGamepad; // Map the gamepad buttons let mappedPad = this.controlManager.mapGamepad(gamepadID); this.players[thisGamepad.index].gamepadMapping = mappedPad.gamepadMapping; this.players[thisGamepad.index].interaction_mapped.gamepadType = mappedPad.padType; for (let thisButton in this.players[thisGamepad.index].gamepadMapping) { this.players[thisGamepad.index].buttons_mapped[thisButton] = 0; } } /** * Set a threshold (between 0 and 1) below which analog stick input will be ignored * @param {*} value * @returns */ setAxisThreshold(value) { this.axisThreshold = value; return this; } /** * Set the number of directions to snap to when mapping input to bearings */ setNumDirections(value) { if (typeof value === 'number' && value > 0) { this.numDirections = value; } return this; } refreshGamepads() { // Sometimes, gamepads are undefined. For some reason. this.gamepads = this.systems.input.gamepad.gamepads.filter(function (el) { return el != null; }); for (const [index, thisGamepad] of this.gamepads.entries()) { thisGamepad.index = index; // Overwrite the gamepad index, in case we had undefined gamepads earlier /** * Some cheap gamepads use the first axis as a dpad, in which case we won't have the dpad buttons 12-15 */ thisGamepad.fakedpad = thisGamepad.buttons.length < 15; } } /** * Add a new player object to the players array * @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 * @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 */ addPlayer(index, numberOfButtons) { numberOfButtons = numberOfButtons || 16; if (typeof Number.isInteger(index) && typeof this.players[index] !== 'undefined') { return this.players[index]; } else { // Set up player object let newPlayer = this.controlManager.setupControls(numberOfButtons); // Add helper functions to the player object this.addPlayerHelperFunctions(newPlayer); // Push new player to players array this.players.push(newPlayer); this.players[this.players.length - 1].index = this.players.length - 1; // If this is the first player, add the pointer events if (this.players.length == 1) { this.systems.input.on('pointermove', function (pointer) { this.pointerMove(pointer); }, this); this.systems.input.on('pointerdown', function (pointer) { this.pointerDown(pointer); }, this); this.systems.input.on('pointerup', function (pointer) { this.pointerUp(pointer); }, this); } return this.players[this.players.length - 1]; } } /** * Add helper functions to the player object * @param {*} player */ addPlayerHelperFunctions(player) { /** * Pass a button name, or an array of button names to check if any were pressed in this update step. * This will only fire once per button press. If you need to check for a button being held down, use isDown instead. * Returns the name of the matched button(s), in case you need it. */ player.interaction.isPressed = (button) => { button = (typeof button === 'string') ? Array(button) : button; let matchedButtons = button.filter(x => player.interaction.pressed.includes(x)) return matchedButtons.length ? matchedButtons : false; }, /** * Pass a button name, or an array of button names to check if any are currently pressed in this update step. * 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. * Returns the name of the matched button(s), in case you need it. */ player.interaction.isDown = (button) => { button = (typeof button === 'string') ? Array(button) : button; let matchedButtons = button.filter(x => player.buttons[x]) let matchedDirections = button.filter(x => player.direction[x]) let matchedPointer = button.filter(x => player.pointer[x]) let matchedAll = [...matchedButtons, ...matchedDirections, ...matchedPointer]; return matchedAll.length ? matchedAll : false; }, /** * Pass a button name, or an array of button names to check if any were released in this update step. * Returns the name of the matched button(s), in case you need it. */ player.interaction.isReleased = (button) => { button = (typeof button === 'string') ? Array(button) : button; let matchedButtons = button.filter(x => player.interaction.released.includes(x)) return matchedButtons.length ? matchedButtons : false; } /** * Pass a mapped button name, or an array of mapped button names to check if any were pressed in this update step. * This will only fire once per button press. If you need to check for a button being held down, use isDown instead. * Returns the name of the matched mapped button(s), in case you need it. */ player.interaction_mapped.isPressed = (button) => { button = (typeof button === 'string') ? Array(button) : button; let matchedButtons = button.filter(x => player.interaction_mapped.pressed.includes(x)) return matchedButtons.length ? matchedButtons : false; }, /** * Pass a mapped button name, or an array of mapped button names to check if any are currently pressed in this update step. * 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. * Returns the name of the matched button(s), in case you need it. */ player.interaction_mapped.isDown = (button) => { button = (typeof button === 'string') ? Array(button) : button; let matchedButtons = button.filter(x => player.buttons_mapped[x]) return matchedButtons.length ? matchedButtons : false; }, /** * Pass a mapped button name, or an array of mapped button names to check if any were released in this update step. * Returns the name of the matched mapped button(s), in case you need it. */ player.interaction_mapped.isReleased = (button) => { button = (typeof button === 'string') ? Array(button) : button; let matchedButtons = button.filter(x => player.interaction_mapped.released.includes(x)) return matchedButtons.length ? matchedButtons : false; } /** * Pass a button name, or an array of button names to check if any are currently pressed in this update step. * Similar to Phaser's keyboard plugin, the checkDown function can accept a 'duration' parameter, and will only register a press once every X milliseconds. * Returns the name of the matched button(s) * * @param {string|array} button Array of buttons to check * @param {number} duration The duration which must have elapsed before this button is considered as being down. * @param {boolean} includeFirst - When true, the initial press of the button will be included in the results. Defaults to false. */ player.interaction.checkDown = (button, duration, includeFirst) => { if (includeFirst === undefined) { includeFirst = false; } if (duration === undefined) { duration = 0; } let matchedButtons = []; let downButtons = player.interaction.isDown(button) if (downButtons.length) { for (let thisButton of downButtons) { if (typeof player.timers[thisButton]._tick === 'undefined') { player.timers[thisButton]._tick = 0; if (includeFirst) { matchedButtons.push(thisButton); } } let t = Phaser.Math.Snap.Floor(this.scene.sys.time.now - player.timers[thisButton].pressed, duration); if (t > player.timers[thisButton]._tick) { this.game.events.once(Phaser.Core.Events.POST_STEP, ()=>{ player.timers[thisButton]._tick = t; }); matchedButtons.push(thisButton); } } } return matchedButtons.length ? matchedButtons : false; }, /** * Mapped version of the checkDown version - resolves mapped button names and calls the checkDown function */ player.interaction_mapped.checkDown = (button, duration, includeFirst) => { if (includeFirst === undefined) { includeFirst = false; } let unmappedButtons = []; // Resolve the unmapped button names to a new array for (let thisButton of button) { let unmappedButton = this.getUnmappedButton(player, thisButton); if (unmappedButton) { unmappedButtons.push(unmappedButton) } } let downButtons = player.interaction.checkDown(unmappedButtons, duration, includeFirst); return downButtons.length ? downButtons.map(x => this.getMappedButton(player, x)) : false; } /** * The previous functions are specific to the interaction and interaction_mapped definition of buttons. * 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 */ /** * Pass a button name, or an array of button names to check if any were pressed in this update step. * This will only fire once per button press. If you need to check for a button being held down, use isDown instead. * Returns the name of the matched button(s), in case you need it. */ player.isPressed = (button) => { let interaction = player.interaction.isPressed(button) || []; let interaction_mapped = player.interaction_mapped.isPressed(button) || []; let matchedButtons = [...interaction, ...interaction_mapped]; return matchedButtons.length ? matchedButtons : false }, /** * Pass a button name, or an array of button names to check if any are currently pressed in this update step. * 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. * Returns the name of the button(s), in case you need it. */ player.isDown = (button) => { let interaction = player.interaction.isDown(button) || []; let interaction_mapped = player.interaction_mapped.isDown(button) || []; let matchedButtons = [...interaction, ...interaction_mapped]; return matchedButtons.length ? matchedButtons : false }, /** * Pass a button name, or an array of button names to check if any were released in this update step. * Returns the name of the matched button(s), in case you need it. */ player.isReleased = (button) => { let interaction = player.interaction.isReleased(button) || []; let interaction_mapped = player.interaction_mapped.isReleased(button) || []; let matchedButtons = [...interaction, ...interaction_mapped]; return matchedButtons.length ? matchedButtons : false } /** * Pass a button name, or an array of button names to check if any are currently pressed in this update step. * Similar to Phaser's keyboard plugin, the checkDown function can accept a 'duration' parameter, and will only register a press once every X milliseconds. * Returns the name of the matched button(s) * * @param {string|array} button Array of buttons to check * @param {number} - The duration which must have elapsed before this button is considered as being down. */ player.checkDown = (button, duration, includeFirst) => { if (includeFirst === undefined) { includeFirst = false; } let interaction = player.interaction.checkDown(button, duration, includeFirst) || []; let interaction_mapped = player.interaction_mapped.checkDown(button, duration, includeFirst) || []; let matchedButtons = [...interaction, ...interaction_mapped]; return matchedButtons.length ? matchedButtons : false } player.setDevice = (device) => { if (player.interaction.device != device) { this.eventEmitter.emit('mergedInput', { device: device, player: player.index, action: 'Device Changed' }); this.events.emit('device_changed', { player: player.index, device: device }); } player.interaction.device = device; return this; } return this; } /** * Get player object * @param {number} index Player index */ getPlayer(index) { return typeof this.players[index] !== 'undefined' ? this.players[index] : '' } getPlayerIndexFromKey(key) { for (let thisPlayer of this.players) { // Loop through all the keys assigned to this player for (var thisKey in thisPlayer.keys) { for (var thisValue of thisPlayer.keys[thisKey]) { if (thisValue == key) { return thisPlayer.index; } } } } return -1; } getPlayerButtonFromKey(key) { for (let thisPlayer of this.players) { // Loop through all the keys assigned to this player for (var thisKey in thisPlayer.keys) { for (var thisValue of thisPlayer.keys[thisKey]) { if (thisValue == key) { // 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 if (typeof thisPlayer.gamepadMapping[thisKey] !== "undefined") { return 'B' + thisPlayer.gamepadMapping[thisKey]; } else { return thisKey; } } } } } return ''; } /** * Return an array of actions that a player may use * @param {number} player * @returns */ getPlayerActions(player) { let actions = ['UP', 'DOWN', 'LEFT', 'RIGHT', 'ALT_UP', 'ALT_DOWN', 'ALT_LEFT', 'ALT_RIGHT']; actions.push(...Object.keys(this.players[player].gamepadMapping)); actions.push(...Object.keys(this.players[player].buttons)); return actions; } /** * 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) * @param {*} player * @param {*} buttonID */ getMappedButton(player, buttonID) { buttonID = buttonID.toString().replace(/\D/g, ''); return Object.keys(player.gamepadMapping).find(key => player.gamepadMapping[key] == buttonID); } /** * 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. * 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. * @param {*} player * @param {*} mappedButton */ getUnmappedButton(player, mappedButton) { let buttonNo = player.gamepadMapping[mappedButton]; let dpadMapping = this.dpadMappings; let direction = Object.keys(dpadMapping).find(key => dpadMapping[key] == buttonNo); return direction ? direction : 'B' + player.gamepadMapping[mappedButton]; } // Keyboard functions /** * Define a key for a player/action combination * @param {number} player The player on which we're defining a key * @param {string} action The action to define * @param {string} value The key to use * @param {boolean} append When true, this key definition will be appended to the existing key(s) for this action */ defineKey(player = 0, action, value, append = false) { // Set up a new player if none defined if (typeof this.players[player] === 'undefined') { this.addPlayer(); } if (this.getPlayerActions(player).includes(action)) { if (append && (typeof this.players[player].keys[action] !== 'undefined')) { this.players[player].keys[action].push([value]); } else { this.players[player].keys[action] = []; this.players[player].keys[action].push([value]); } this.keys[[value]] = this.systems.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes[value]); } return this; } /** * Iterate through players and check for interaction with defined keys */ checkKeyboardInput() { // Loop through players and check for keypresses for (let thisPlayer of this.players) { // Loop through all the keys assigned to this player for (var thisKey in thisPlayer.keys) { let action = 0; for (var thisValue of thisPlayer.keys[thisKey]) { // Check if the key is down action = (this.keys[thisValue].isDown) ? 1 : action; } // Set the action in the player object // Dpad if (['UP', 'DOWN', 'LEFT', 'RIGHT'].includes(thisKey)) { thisPlayer.direction[thisKey] = action; if (action == 1) { thisPlayer.direction.TIMESTAMP = this.scene.sys.time.now; } } // Alternative direction else if (['ALT_UP', 'ALT_DOWN', 'ALT_LEFT', 'ALT_RIGHT'].includes(thisKey)) { thisPlayer.direction_secondary[thisKey.replace('ALT_', '')] = action; if (action == 1) { thisPlayer.direction_secondary.TIMESTAMP = this.scene.sys.time.now; } } // Friendly button names else if (thisKey in thisPlayer.gamepadMapping) { // Get the button number from the gamepad mapping thisPlayer.buttons['B' + thisPlayer.gamepadMapping[thisKey]] = action; thisPlayer.buttons_mapped[thisKey] = action; if (action == 1) { thisPlayer.buttons.TIMESTAMP = this.scene.sys.time.now; } } // Numbered buttons else { thisPlayer.buttons[thisKey] = action; if (action == 1) { thisPlayer.buttons.TIMESTAMP = this.scene.sys.time.now; } } // Set the latest interaction flag if (action == 1) { thisPlayer.setDevice('keyboard'); } } } } /** * When a keyboard button is pressed down, this function will emit a mergedInput event in the global registry. * The event contains a reference to the player assigned to the key, and passes a mapped action and value */ keyboardKeyDown(event) { let keyCode = Object.keys(Phaser.Input.Keyboard.KeyCodes).find(key => Phaser.Input.Keyboard.KeyCodes[key] === event.keyCode); let playerIndex = this.getPlayerIndexFromKey(keyCode); let playerAction = this.getPlayerButtonFromKey(keyCode); if (playerIndex > -1 && playerAction != '') { let thisPlayer = this.getPlayer(playerIndex); this.eventEmitter.emit('mergedInput', { device: 'keyboard', value: 1, player: playerIndex, action: keyCode, state: 'DOWN' }); this.events.emit('keyboard_keydown', { player: playerIndex, key: keyCode }); thisPlayer.setDevice('keyboard'); thisPlayer.interaction.pressed.push(playerAction); thisPlayer.interaction.buffer.push(playerAction); thisPlayer.interaction.last = playerAction; thisPlayer.interaction.lastPressed = playerAction; // Update timers thisPlayer.timers[playerAction].pressed = this.scene.sys.time.now; thisPlayer.timers[playerAction].released = 0; thisPlayer.timers[playerAction].duration = 0; // Update mapped button object if (typeof this.dpadMappings[playerAction] !== "undefined") { playerAction = 'B' + this.dpadMappings[playerAction]; } if (typeof thisPlayer.buttons[playerAction] !== "undefined") { let mappedButton = this.getMappedButton(thisPlayer, playerAction); if (typeof mappedButton !== "undefined") { thisPlayer.buttons_mapped[mappedButton] = 1; thisPlayer.interaction_mapped.pressed.push(mappedButton); thisPlayer.interaction_mapped.last = mappedButton; thisPlayer.interaction_mapped.lastPressed = mappedButton; thisPlayer.interaction_mapped.gamepadType = 'keyboard'; } } } } /** * When a keyboard button is released, this function will emit a mergedInput event in the global registry. * The event contains a reference to the player assigned to the key, and passes a mapped action and value */ keyboardKeyUp(event) { let keyCode = Object.keys(Phaser.Input.Keyboard.KeyCodes).find(key => Phaser.Input.Keyboard.KeyCodes[key] === event.keyCode); let playerIndex = this.getPlayerIndexFromKey(keyCode); let playerAction = this.getPlayerButtonFromKey(keyCode); if (playerIndex > -1 && playerAction != '') { let thisPlayer = this.getPlayer(playerIndex); this.eventEmitter.emit('mergedInput', { device: 'keyboard', value: 1, player: playerIndex, action: keyCode, state: 'UP' }); this.events.emit('keyboard_keyup', { player: playerIndex, key: keyCode }); thisPlayer.setDevice('keyboard'); thisPlayer.interaction.released.push(playerAction); thisPlayer.interaction.lastReleased = playerAction; // Update timers thisPlayer.timers[playerAction].released = this.scene.sys.time.now; thisPlayer.timers[playerAction].duration = thisPlayer.timers[playerAction].released - thisPlayer.timers[playerAction].pressed; delete thisPlayer.timers[playerAction]._tick; // Update mapped button object if (typeof this.dpadMappings[playerAction] !== "undefined") { playerAction = 'B' + this.dpadMappings[playerAction]; } if (typeof thisPlayer.buttons[playerAction] !== "undefined") { let mappedButton = this.getMappedButton(thisPlayer, playerAction); if (typeof mappedButton !== "undefined") { thisPlayer.buttons_mapped[mappedButton] = 0; thisPlayer.interaction_mapped.released = mappedButton; thisPlayer.interaction_mapped.lastReleased = mappedButton; thisPlayer.interaction_mapped.gamepadType = 'keyboard'; } } } } /** * Iterate through players and check for interaction with defined pointer buttons */ checkPointerInput() { // Check for pointer movement if (this.systems.input.activePointer.velocity.x != 0 || this.systems.input.activePointer.velocity.y != 0) { this.players[0].setDevice('pointer'); } // Loop through players and check for button presses for (let thisPlayer of this.players) { // Loop through all the keys assigned to this player for (var thisKey in thisPlayer.keys) { for (var thisValue of thisPlayer.keys[thisKey]) { // Each definition for this key action if (['M1', 'M2', 'M3', 'M4', 'M5'].includes(thisValue[0])) { // Check to see if button is pressed (stored in P1, can't have two mice...) if (this.players[0].pointer[thisValue] == 1) { thisPlayer.buttons[thisKey] = 1; } } } } } } // Gamepad functions /** * When a gamepad button is pressed down, this function will emit a mergedInput event in the global registry. * The event contains a reference to the player assigned to the gamepad, and passes a mapped action and value * @param {number} index Button index * @param {number} value Button value * @param {Phaser.Input.Gamepad.Button} button Phaser Button object */ gamepadButtonDown(pad, button, value) { this.players[pad.index].setDevice('gamepad'); this.players[pad.index].buttons.TIMESTAMP = this.scene.sys.time.now; this.eventEmitter.emit('mergedInput', { device: 'gamepad', value: value, player: pad.index, action: 'B' + button.index, state: 'DOWN' }); this.events.emit('gamepad_buttondown', { player: pad.index, button: `B${button.index}` }); // Buttons if (![12, 13, 14, 15].includes(button.index)) { let playerAction = 'B' + button.index; // Update the last button state this.players[pad.index].interaction.pressed.push(playerAction); this.players[pad.index].interaction.last = playerAction; this.players[pad.index].interaction.lastPressed = playerAction; this.players[pad.index].interaction.buffer.push(playerAction); // Update timers this.players[pad.index].timers[playerAction].pressed = this.scene.sys.time.now; this.players[pad.index].timers[playerAction].released = 0; this.players[pad.index].timers[playerAction].duration = 0; // Update mapped button object let mappedButton = this.getMappedButton(this.players[pad.index], button.index); if (typeof mappedButton !== "undefined") { this.players[pad.index].interaction_mapped.pressed.push(mappedButton); this.players[pad.index].interaction_mapped.last = mappedButton; this.players[pad.index].interaction_mapped.lastPressed = mappedButton; } } // DPad else { let dpadMapping = this.dpadMappings; let direction = Object.keys(dpadMapping).find(key => dpadMapping[key] == button.index); this.eventEmitter.emit('mergedInput', { device: 'gamepad', value: 1, player: pad.index, action: direction, state: 'DOWN' }); this.events.emit('gamepad_directiondown', { player: pad.index, button: direction }); this.players[pad.index].interaction.pressed.push(direction); this.players[pad.index].interaction.last = direction; this.players[pad.index].interaction.lastPressed = direction; this.players[pad.index].interaction.buffer.push(direction); this.players[pad.index].direction.TIMESTAMP = this.scene.sys.time.now; // Update timers this.players[pad.index].timers[direction].pressed = this.scene.sys.time.now; this.players[pad.index].timers[direction].released = 0; this.players[pad.index].timers[direction].duration = 0; // Update mapped button object let mappedButton = this.getMappedButton(this.players[pad.index], button.index); if (typeof mappedButton !== "undefined") { this.players[pad.index].interaction_mapped.pressed.push(mappedButton); this.players[pad.index].interaction_mapped.last = mappedButton; this.players[pad.index].interaction_mapped.lastPressed = mappedButton; } } } /** * When a gamepad button is released, this function will emit a mergedInput event in the global registry. * The event contains a reference to the player assigned to the gamepad, and passes a mapped action and value * @param {number} index Button index * @param {number} value Button value * @param {Phaser.Input.Gamepad.Button} button Phaser Button object */ gamepadButtonUp(pad, button, value) { this.players[pad.index].setDevice('gamepad'); this.players[pad.index].buttons.TIMESTAMP = this.scene.sys.time.now; this.eventEmitter.emit('mergedInput', { device: 'gamepad', value: value, player: pad.index, action: 'B' + button.index, state: 'UP' }); this.events.emit('gamepad_buttonup', { player: pad.index, button: `B${button.index}` }); // Buttons if (![12, 13, 14, 15].includes(button.index)) { let playerAction = 'B' + button.index; // Update the last button state this.players[pad.index].interaction.released.push(playerAction); this.players[pad.index].interaction.lastReleased = playerAction; // Update timers this.players[pad.index].timers[playerAction].released = this.scene.sys.time.now; this.players[pad.index].timers[playerAction].duration = this.players[pad.index].timers[playerAction].released - this.players[pad.index].timers[playerAction].pressed; delete this.players[pad.index].timers[playerAction]._tick; // Update mapped button object let mappedButton = this.getMappedButton(this.players[pad.index], button.index); if (typeof mappedButton !== "undefined") { this.players[pad.index].interaction_mapped.released = mappedButton; this.players[pad.index].interaction_mapped.lastReleased = mappedButton; } } // DPad else { let dpadMapping = this.dpadMappings; let direction = Object.keys(dpadMapping).find(key => dpadMapping[key] == button.index); this.eventEmitter.emit('mergedInput', { device: 'gamepad', value: 1, player: pad.index, action: direction, state: 'UP' }); this.events.emit('gamepad_directionup', { player: pad.index, button: direction }); this.players[pad.index].interaction.released.push(direction); this.players[pad.index].interaction.lastReleased = direction; // Update timers this.players[pad.index].timers[direction].released = this.scene.sys.time.now; this.players[pad.index].timers[direction].duration = this.players[pad.index].timers[direction].released - this.players[pad.index].timers[direction].pressed; delete this.players[pad.index].timers[direction]._tick; // Update mapped button object let mappedButton = this.getMappedButton(this.players[pad.index], button.index); if (typeof mappedButton !== "undefined") { this.players[pad.index].interaction_mapped.released = mappedButton; this.players[pad.index].interaction_mapped.lastReleased = mappedButton; } } } /** * Some gamepads map dpads to axis, which are handled differently to buttons. * This function mimics a gamepad push and fires an event. * We also insert the direction into a buffer so that we know what buttons are pressed in the gamepadFakeDPadRelease function * 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. */ gamepadFakeDPadPress(gamepad, direction) { if (!this.players[gamepad.index].internal.fakedpadBuffer.includes(direction)) { this.players[gamepad.index].internal.fakedpadBuffer.push(direction); this.players[gamepad.index].internal.fakedpadPressed.push(direction); let thisButton = new Phaser.Input.Gamepad.Button(gamepad, this.dpadMappings[direction]) thisButton.value = 1; thisButton.pressed = true; thisButton.events.emit('down', gamepad, thisButton, 1) } } /** * When the axis is blank, we know we've released all buttons. */ gamepadFakeDPadRelease(gamepad) { if (this.players[gamepad.index].internal.fakedpadBuffer.length > 0) { for (let direction of this.players[gamepad.index].internal.fakedpadBuffer) { this.players[gamepad.index].internal.fakedpadReleased = direction; let thisButton = new Phaser.Input.Gamepad.Button(gamepad, this.dpadMappings[direction]) thisButton.value = 0; thisButton.pressed = false; thisButton.events.emit('up', gamepad, thisButton, 0) } this.players[gamepad.index].internal.fakedpadBuffer = []; } } /** * Iterate through gamepads and handle interactions */ checkGamepadInput() { // Check for gamepad input for (var thisGamepad of this.gamepads) { // Set up a player if we don't have one, presumably due to race conditions in detecting gamepads if (typeof this.players[thisGamepad.index] === 'undefined') { this.addPlayer(); } let direction = ''; // Directions if (thisGamepad.leftStick.y < -this.axisThreshold) { this.players[thisGamepad.index].direction.UP = Math.abs(thisGamepad.leftStick.y) this.players[thisGamepad.index].direction.TIMESTAMP = this.scene.sys.time.now; this.players[thisGamepad.index].setDevice('gamepad'); if (thisGamepad.fakedpad) { this.gamepadFakeDPadPress(thisGamepad, 'UP'); direction = 'UP' } } else if (thisGamepad.leftStick.y > this.axisThreshold) { this.players[thisGamepad.index].direction.DOWN = thisGamepad.leftStick.y this.players[thisGamepad.index].direction.TIMESTAMP = this.scene.sys.time.now; this.players[thisGamepad.index].setDevice('gamepad'); if (thisGamepad.fakedpad) { this.gamepadFakeDPadPress(thisGamepad, 'DOWN'); direction = 'DOWN' } } else if (this.players[thisGamepad.index].interaction.device === 'gamepad') { // DPad this.players[thisGamepad.index].direction.UP = thisGamepad.up ? 1 : 0; this.players[thisGamepad.index].direction.DOWN = thisGamepad.down ? 1 : 0; } if (thisGamepad.leftStick.x < -this.axisThreshold) { this.players[thisGamepad.index].direction.LEFT = Math.abs(thisGamepad.leftStick.x) this.players[thisGamepad.index].direction.TIMESTAMP = this.scene.sys.time.now; this.players[thisGamepad.index].setDevice('gamepad'); if (thisGamepad.fakedpad) { this.gamepadFakeDPadPress(thisGamepad, 'LEFT'); direction = 'LEFT' } } else if (thisGamepad.leftStick.x > this.axisThreshold) { this.players[thisGamepad.index].direction.RIGHT = thisGamepad.leftStick.x this.players[thisGamepad.index].direction.TIMESTAMP = this.scene.sys.time.now; this.players[thisGamepad.index].setDevice('gamepad'); if (thisGamepad.fakedpad) { this.gamepadFakeDPadPress(thisGamepad, 'RIGHT'); direction = 'RIGHT' } } else if (this.players[thisGamepad.index].interaction.device === 'gamepad') { // DPad this.players[thisGamepad.index].direction.LEFT = thisGamepad.left ? 1 : 0; this.players[thisGamepad.index].direction.RIGHT = thisGamepad.right ? 1 : 0; } if (thisGamepad.fakedpad && direction == '') { this.gamepadFakeDPadRelease(thisGamepad); } // Secondary if (thisGamepad.rightStick.y < -this.axisThreshold) { this.players[thisGamepad.index].direction_secondary.UP = Math.abs(thisGamepad.rightStick.y) this.players[thisGamepad.index].direction_secondary.TIMESTAMP = this.scene.sys.time.now; this.players[thisGamepad.index].setDevice('gamepad'); } else if (thisGamepad.rightStick.y > this.axisThreshold) { this.players[thisGamepad.index].direction_secondary.DOWN = thisGamepad.rightStick.y this.players[thisGamepad.index].direction_secondary.TIMESTAMP = this.scene.sys.time.now; this.players[thisGamepad.index].setDevice('gamepad'); } else { this.players[thisGamepad.index].direction_secondary.UP = 0; this.players[thisGamepad.index].direction_secondary.DOWN = 0; } if (thisGamepad.rightStick.x < -this.axisThreshold) { this.players[thisGamepad.index].direction_secondary.LEFT = Math.abs(thisGamepad.rightStick.x) this.players[thisGamepad.index].direction_secondary.TIMESTAMP = this.scene.sys.time.now; this.players[thisGamepad.index].setDevice('gamepad'); } else if (thisGamepad.rightStick.x > this.axisThreshold) { this.players[thisGamepad.index].direction_secondary.RIGHT = thisGamepad.rightStick.x this.players[thisGamepad.index].direction_secondary.TIMESTAMP = this.scene.sys.time.now; this.players[thisGamepad.index].setDevice('gamepad'); } else { this.players[thisGamepad.index].direction_secondary.LEFT = 0; this.players[thisGamepad.index].direction_secondary.RIGHT = 0; } if (this.players[thisGamepad.index].interaction.device === 'gamepad') { // Buttons for (var b = 0; b < thisGamepad.buttons.length; b++) { let button = thisGamepad.buttons[b]; this.players[thisGamepad.index].buttons['B' + b] = button.value; // Get mapped name for this button number and artificially update the relevant buttons_mapped key let mappedButton = this.getMappedButton(this.players[thisGamepad.index], b); if (typeof mappedButton !== "undefined") { this.players[thisGamepad.index].buttons_mapped[mappedButton] = button.value; } } // If we're faking the d-pad, we won't have the extra buttons so we'll have to manually update the button objects if (thisGamepad.fakedpad) { if (direction == '') { this.players[thisGamepad.index].buttons['B12'] = 0; this.players[thisGamepad.index].buttons['B13'] = 0; this.players[thisGamepad.index].buttons['B14'] = 0; this.players[thisGamepad.index].buttons['B15'] = 0; this.players[thisGamepad.index].buttons_mapped[this.getMappedButton(this.players[thisGamepad.index], 'B12')] = 0; this.players[thisGamepad.index].buttons_mapped[this.getMappedButton(this.players[thisGamepad.index], 'B13')] = 0; this.players[thisGamepad.index].buttons_mapped[this.getMappedButton(this.players[thisGamepad.index], 'B14')] = 0; this.players[thisGamepad.index].buttons_mapped[this.getMappedButton(this.players[thisGamepad.index], 'B15')] = 0; } else { this.players[thisGamepad.index].buttons['B' + this.dpadMappings[direction]] = 1; let mappedButton = this.getMappedButton(this.players[thisGamepad.index], 'B' + this.dpadMappings[direction]); this.players[thisGamepad.index].buttons_mapped[mappedButton] = 1; } } } } } /** * Function to run on pointer move. * @param {*} pointer - The pointer object */ pointerMove(pointer, threshold, numDirections) { if (this.players.length) { threshold = threshold || -1; numDirections = numDirections || this.numDirections; if (pointer.distance > threshold) { let pointerDirection = this.getBearingFromAngle(pointer.angle, numDirections); // If we've been given a player position, return bearings and angles if (typeof this.players[0] !== 'undefined' && this.players[0].position.x !== 'undefined') { let position = this.players[0].position; let angleToPointer = Math.round(Phaser.Math.Angle.Between(position.x, position.y, pointer.x, pointer.y) * 100) / 100; let angleDegrees = Math.round(Phaser.Math.RadToDeg(angleToPointer) * 100) / 100; pointerDirection = this.getBearingFromAngle(angleToPointer, numDirections); let pointerAngle = Number(this.mapBearingToDegrees(pointerDirection)); this.players[0].pointer.BEARING = pointerDirection; this.players[0].pointer.ANGLE = angleToPointer; this.players[0].pointer.DEGREES = angleDegrees; this.players[0].pointer.BEARING_DEGREES = pointerAngle; this.players[0].pointer.TIMESTAMP = this.scene.sys.time.now; this.players[0].pointer.POINTERANGLE = pointerAngle; this.players[0].pointer.POINTERDIRECTION = pointerDirection; this.players[0].pointer.PLAYERPOS = position; } } } } /** * Function to run on pointer down. Indicates that Mx has been pressed, which should be listened to by the player object * @param {*} pointer - The pointer object */ pointerDown(pointer) { if (this.players.length) { let action = ''; this.players[0].setDevice('pointer'); if (pointer.leftButtonDown()) { action = 'M1'; } if (pointer.rightButtonDown()) { action = 'M2'; } if (pointer.middleButtonDown()) { action = 'M3'; } if (pointer.backButtonDown()) { action = 'M4'; } if (pointer.forwardButtonDown()) { action = 'M5'; } this.eventEmitter.emit('mergedInput', { device: 'pointer', value: 1, player: 0, action: action, state: 'DOWN' }); this.events.emit('pointer_down', action); this.players[0].pointer[action] = 1; // Update the last button state this.players[0].interaction.pressed.push(action); this.players[0].interaction.last = action; this.players[0].interaction.lastPressed = action; this.players[0].interaction.buffer.push(action); this.players[0].pointer.TIMESTAMP = pointer.moveTime; // Update timers this.players[0].timers[action].pressed = this.scene.sys.time.now; this.players[0].timers[action].released = 0; this.players[0].timers[action].duration = 0; } } /** * Function to run on pointer up. Indicates that Mx has been released, which should be listened to by the player object * @param {*} pointer - The pointer object */ pointerUp(pointer) { if (this.players.length) { let action = ''; if (pointer.leftButtonReleased()) { action = 'M1'; } if (pointer.rightButtonReleased()) { action = 'M2'; } if (pointer.middleButtonReleased()) { action = 'M3'; } if (pointer.backButtonReleased()) { action = 'M4'; } if (pointer.forwardButtonReleased()) { action = 'M5'; } this.eventEmitter.emit('mergedInput', { device: 'pointer', value: 1, player: 0, action: action, state: 'UP' }); this.events.emit('pointer_up', action); this.players[0].pointer[action] = 0; this.players[0].interaction.released.push(action); this.players[0].interaction.lastReleased = action; this.players[0].pointer.TIMESTAMP = this.scene.sys.time.now; // Update timers this.players[0].timers[action].released = this.scene.sys.time.now; this.players[0].timers[action].duration = this.players[0].timers[action].released - this.players[0].timers[action].pressed; delete this.players[0].timers[action]._tick; } } /** * Create new button combo. * Combos extend Phaser's keyboard combo and mimic their functionality for gamepad/player combinations. * If you requrie a keyboard entered combo, use the native Phaser.Input.Keyboard.KeyboardPlugin.createCombo function. * * @param {player} player - A player object. If more than one player should be able to execute the combo, you should create multiple buttonCombo instances. * @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'] * @param {Phaser.Types.Input.Keyboard.KeyComboConfig} [config] - A Key Combo configuration object. */ createButtonCombo(player, buttons, config) { return new ButtonCombo(this, player, buttons, config); } /** * Get the bearing from a given angle * @param {float} angle - Angle to use * @param {number} numDirections - Number of possible directions (e.g. 4 for N/S/E/W) */ getBearingFromAngle(angle, numDirections) { numDirections = numDirections || this.numDirections; var snap_interval = Phaser.Math.PI2 / numDirections; var angleSnap = Phaser.Math.Snap.To(angle, snap_interval); var angleSnapDeg = Number(Phaser.Math.RadToDeg(angleSnap).toFixed(2)); var angleSnapDir = this.bearings[angleSnapDeg]; return angleSnapDir; } /** * Given a bearing, return a direction object containing boolean flags for the four directions * @param {*} bearing */ mapBearingToDirections(bearing) { let thisDirection = { 'UP': 0, 'DOWN': 0, 'LEFT': 0, 'RIGHT': 0, 'BEARING': bearing.toUpperCase() } if (bearing.toUpperCase().includes('W')) { thisDirection.LEFT = 1; } if (bearing.toUpperCase().includes('E')) { thisDirection.RIGHT = 1; } if (bearing.toUpperCase().includes('S')) { thisDirection.DOWN = 1; } if (bearing.toUpperCase().includes('N')) { thisDirection.UP = 1; } return thisDirection; } /** * Given a directions object corresponding to analogue input, return an angle * @param {*} directions - Direction object containing UP, DOWN, LEFT, RIGHT values * @param {number} threshold - Threshold for analog input, e.g. 0.1 * @returns {number} Calulated angle */ mapDirectionsToAngle(direct