phaser/src/input/keyboard/KeyboardPlugin.js
2019-02-13 15:38:50 +00:00

862 lines
27 KiB
JavaScript

/**
* @author Richard Davey <rich@photonstorm.com>
* @copyright 2019 Photon Storm Ltd.
* @license {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License}
*/
var Class = require('../../utils/Class');
var EventEmitter = require('eventemitter3');
var Events = require('./events');
var GameEvents = require('../../core/events');
var GetValue = require('../../utils/object/GetValue');
var InputEvents = require('../events');
var InputPluginCache = require('../InputPluginCache');
var Key = require('./keys/Key');
var KeyCodes = require('./keys/KeyCodes');
var KeyCombo = require('./combo/KeyCombo');
var KeyMap = require('./keys/KeyMap');
var SnapFloor = require('../../math/snap/SnapFloor');
/**
* @classdesc
* The Keyboard Plugin is an input plugin that belongs to the Scene-owned Input system.
*
* Its role is to listen for native DOM Keyboard Events and then process them.
*
* You do not need to create this class directly, the Input system will create an instance of it automatically.
*
* You can access it from within a Scene using `this.input.keyboard`. For example, you can do:
*
* ```javascript
* this.input.keyboard.on('keydown', callback, context);
* ```
*
* Or, to listen for a specific key:
*
* ```javascript
* this.input.keyboard.on('keydown-A', callback, context);
* ```
*
* You can also create Key objects, which you can then poll in your game loop:
*
* ```javascript
* var spaceBar = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);
* ```
*
* If you have multiple parallel Scenes, each trying to get keyboard input, be sure to disable capture on them to stop them from
* stealing input from another Scene in the list. You can do this with `this.input.keyboard.enabled = false` within the
* Scene to stop all input, or `this.input.keyboard.preventDefault = false` to stop a Scene halting input on another Scene.
*
* _Note_: Many keyboards are unable to process certain combinations of keys due to hardware limitations known as ghosting.
* See http://www.html5gamedevs.com/topic/4876-impossible-to-use-more-than-2-keyboard-input-buttons-at-the-same-time/ for more details.
*
* Also please be aware that certain browser extensions can disable or override Phaser keyboard handling.
* For example the Chrome extension vimium is known to disable Phaser from using the D key, while EverNote disables the backtick key.
* And there are others. So, please check your extensions before opening Phaser issues about keys that don't work.
*
* @class KeyboardPlugin
* @extends Phaser.Events.EventEmitter
* @memberof Phaser.Input.Keyboard
* @constructor
* @since 3.10.0
*
* @param {Phaser.Input.InputPlugin} sceneInputPlugin - A reference to the Scene Input Plugin that the KeyboardPlugin belongs to.
*/
var KeyboardPlugin = new Class({
Extends: EventEmitter,
initialize:
function KeyboardPlugin (sceneInputPlugin)
{
EventEmitter.call(this);
/**
* A reference to the core game, so we can listen for visibility events.
*
* @name Phaser.Input.Keyboard.KeyboardPlugin#game
* @type {Phaser.Game}
* @since 3.16.0
*/
this.game = sceneInputPlugin.systems.game;
/**
* A reference to the Scene that this Input Plugin is responsible for.
*
* @name Phaser.Input.Keyboard.KeyboardPlugin#scene
* @type {Phaser.Scene}
* @since 3.10.0
*/
this.scene = sceneInputPlugin.scene;
/**
* A reference to the Scene Systems Settings.
*
* @name Phaser.Input.Keyboard.KeyboardPlugin#settings
* @type {Phaser.Scenes.Types.SettingsObject}
* @since 3.10.0
*/
this.settings = this.scene.sys.settings;
/**
* A reference to the Scene Input Plugin that created this Keyboard Plugin.
*
* @name Phaser.Input.Keyboard.KeyboardPlugin#sceneInputPlugin
* @type {Phaser.Input.InputPlugin}
* @since 3.10.0
*/
this.sceneInputPlugin = sceneInputPlugin;
/**
* A reference to the global Keyboard Manager.
*
* @name Phaser.Input.Keyboard.KeyboardPlugin#manager
* @type {Phaser.Input.InputPlugin}
* @since 3.16.0
*/
this.manager = sceneInputPlugin.manager.keyboard;
/**
* A boolean that controls if this Keyboard Plugin is enabled or not.
* Can be toggled on the fly.
*
* @name Phaser.Input.Keyboard.KeyboardPlugin#enabled
* @type {boolean}
* @default true
* @since 3.10.0
*/
this.enabled = true;
/**
* An array of Key objects to process.
*
* @name Phaser.Input.Keyboard.KeyboardPlugin#keys
* @type {Phaser.Input.Keyboard.Key[]}
* @since 3.10.0
*/
this.keys = [];
/**
* An array of KeyCombo objects to process.
*
* @name Phaser.Input.Keyboard.KeyboardPlugin#combos
* @type {Phaser.Input.Keyboard.KeyCombo[]}
* @since 3.10.0
*/
this.combos = [];
sceneInputPlugin.pluginEvents.once(InputEvents.BOOT, this.boot, this);
sceneInputPlugin.pluginEvents.on(InputEvents.START, this.start, this);
},
/**
* This method is called automatically, only once, when the Scene is first created.
* Do not invoke it directly.
*
* @method Phaser.Input.Keyboard.KeyboardPlugin#boot
* @private
* @since 3.10.0
*/
boot: function ()
{
var settings = this.settings.input;
this.enabled = GetValue(settings, 'keyboard', true);
var captures = GetValue(settings, 'keyboard.capture', null);
if (captures)
{
this.addCaptures(captures);
}
this.sceneInputPlugin.pluginEvents.once(InputEvents.DESTROY, this.destroy, this);
},
/**
* This method is called automatically by the Scene when it is starting up.
* It is responsible for creating local systems, properties and listening for Scene events.
* Do not invoke it directly.
*
* @method Phaser.Input.Keyboard.KeyboardPlugin#start
* @private
* @since 3.10.0
*/
start: function ()
{
if (this.sceneInputPlugin.manager.useQueue)
{
this.sceneInputPlugin.pluginEvents.on(InputEvents.UPDATE, this.update, this);
}
else
{
this.sceneInputPlugin.manager.events.on(InputEvents.MANAGER_PROCESS, this.update, this);
}
this.sceneInputPlugin.pluginEvents.once(InputEvents.SHUTDOWN, this.shutdown, this);
this.game.events.on(GameEvents.BLUR, this.resetKeys, this);
},
/**
* Checks to see if both this plugin and the Scene to which it belongs is active.
*
* @method Phaser.Input.Keyboard.KeyboardPlugin#isActive
* @since 3.10.0
*
* @return {boolean} `true` if the plugin and the Scene it belongs to is active.
*/
isActive: function ()
{
return (this.enabled && this.scene.sys.isActive());
},
/**
* By default when a key is pressed Phaser will not stop the event from propagating up to the browser.
* There are some keys this can be annoying for, like the arrow keys or space bar, which make the browser window scroll.
*
* This `addCapture` method enables consuming keyboard events for specific keys, so they don't bubble up the browser
* and cause the default behaviors.
*
* Please note that keyboard captures are global. This means that if you call this method from within a Scene, to say prevent
* the SPACE BAR from triggering a page scroll, then it will prevent it for any Scene in your game, not just the calling one.
*
* You can pass a single key code value:
*
* ```javascript
* this.input.keyboard.addCapture(62);
* ```
*
* An array of key codes:
*
* ```javascript
* this.input.keyboard.addCapture([ 62, 63, 64 ]);
* ```
*
* Or, a comma-delimited string:
*
* ```javascript
* this.input.keyboard.addCapture('W,S,A,D');
* ```
*
* To use non-alpha numeric keys, use a string, such as 'UP', 'SPACE' or 'LEFT'.
*
* You can also provide an array mixing both strings and key code integers.
*
* @method Phaser.Input.Keyboard.KeyboardPlugin#addCapture
* @since 3.16.0
*
* @param {(string|integer|integer[]|any[])} keycode - The Key Codes to enable event capture for.
*
* @return {Phaser.Input.Keyboard.KeyboardPlugin} This KeyboardPlugin object.
*/
addCapture: function (keycode)
{
this.manager.addCapture(keycode);
return this;
},
/**
* Removes an existing key capture.
*
* Please note that keyboard captures are global. This means that if you call this method from within a Scene, to remove
* the capture of a key, then it will remove it for any Scene in your game, not just the calling one.
*
* You can pass a single key code value:
*
* ```javascript
* this.input.keyboard.removeCapture(62);
* ```
*
* An array of key codes:
*
* ```javascript
* this.input.keyboard.removeCapture([ 62, 63, 64 ]);
* ```
*
* Or, a comma-delimited string:
*
* ```javascript
* this.input.keyboard.removeCapture('W,S,A,D');
* ```
*
* To use non-alpha numeric keys, use a string, such as 'UP', 'SPACE' or 'LEFT'.
*
* You can also provide an array mixing both strings and key code integers.
*
* @method Phaser.Input.Keyboard.KeyboardPlugin#removeCapture
* @since 3.16.0
*
* @param {(string|integer|integer[]|any[])} keycode - The Key Codes to disable event capture for.
*
* @return {Phaser.Input.Keyboard.KeyboardPlugin} This KeyboardPlugin object.
*/
removeCapture: function (keycode)
{
this.manager.removeCapture(keycode);
return this;
},
/**
* Returns an array that contains all of the keyboard captures currently enabled.
*
* @method Phaser.Input.Keyboard.KeyboardPlugin#getCaptures
* @since 3.16.0
*
* @return {integer[]} An array of all the currently capturing key codes.
*/
getCaptures: function ()
{
return this.manager.captures;
},
/**
* Allows Phaser to prevent any key captures you may have defined from bubbling up the browser.
* You can use this to re-enable event capturing if you had paused it via `disableGlobalCapture`.
*
* @method Phaser.Input.Keyboard.KeyboardPlugin#enableGlobalCapture
* @since 3.16.0
*
* @return {Phaser.Input.Keyboard.KeyboardPlugin} This KeyboardPlugin object.
*/
enableGlobalCapture: function ()
{
this.manager.preventDefault = true;
return this;
},
/**
* Disables Phaser from preventing any key captures you may have defined, without actually removing them.
* You can use this to temporarily disable event capturing if, for example, you swap to a DOM element.
*
* @method Phaser.Input.Keyboard.KeyboardPlugin#disableGlobalCapture
* @since 3.16.0
*
* @return {Phaser.Input.Keyboard.KeyboardPlugin} This KeyboardPlugin object.
*/
disableGlobalCapture: function ()
{
this.manager.preventDefault = false;
return this;
},
/**
* Removes all keyboard captures.
*
* Note that this is a global change. It will clear all event captures across your game, not just for this specific Scene.
*
* @method Phaser.Input.Keyboard.KeyboardPlugin#clearCaptures
* @since 3.16.0
*
* @return {Phaser.Input.Keyboard.KeyboardPlugin} This KeyboardPlugin object.
*/
clearCaptures: function ()
{
this.manager.clearCaptures();
return this;
},
/**
* Creates and returns an object containing 4 hotkeys for Up, Down, Left and Right, and also Space Bar and shift.
*
* @method Phaser.Input.Keyboard.KeyboardPlugin#createCursorKeys
* @since 3.10.0
*
* @return {Phaser.Input.Keyboard.Types.CursorKeys} An object containing the properties: `up`, `down`, `left`, `right`, `space` and `shift`.
*/
createCursorKeys: function ()
{
return this.addKeys({
up: KeyCodes.UP,
down: KeyCodes.DOWN,
left: KeyCodes.LEFT,
right: KeyCodes.RIGHT,
space: KeyCodes.SPACE,
shift: KeyCodes.SHIFT
});
},
/**
* A practical way to create an object containing user selected hotkeys.
*
* For example:
*
* ```javascript
* this.input.keyboard.addKeys({ 'up': Phaser.Input.Keyboard.KeyCodes.W, 'down': Phaser.Input.Keyboard.KeyCodes.S });
* ```
*
* would return an object containing the properties (`up` and `down`) mapped to W and S {@link Phaser.Input.Keyboard.Key} objects.
*
* You can also pass in a comma-separated string:
*
* ```javascript
* this.input.keyboard.addKeys('W,S,A,D');
* ```
*
* Which will return an object with the properties W, S, A and D mapped to the relevant Key objects.
*
* To use non-alpha numeric keys, use a string, such as 'UP', 'SPACE' or 'LEFT'.
*
* @method Phaser.Input.Keyboard.KeyboardPlugin#addKeys
* @since 3.10.0
*
* @param {(object|string)} keys - An object containing Key Codes, or a comma-separated string.
* @param {boolean} [enableCapture=true] - Automatically call `preventDefault` on the native DOM browser event for the key codes being added.
* @param {boolean} [emitOnRepeat=false] - Controls if the Key will continuously emit a 'down' event while being held down (true), or emit the event just once (false, the default).
*
* @return {object} An object containing Key objects mapped to the input properties.
*/
addKeys: function (keys, enableCapture, emitOnRepeat)
{
if (enableCapture === undefined) { enableCapture = true; }
if (emitOnRepeat === undefined) { emitOnRepeat = false; }
var output = {};
if (typeof keys === 'string')
{
keys = keys.split(',');
for (var i = 0; i < keys.length; i++)
{
var currentKey = keys[i].trim();
if (currentKey)
{
output[currentKey] = this.addKey(currentKey, enableCapture, emitOnRepeat);
}
}
}
else
{
for (var key in keys)
{
output[key] = this.addKey(keys[key], enableCapture, emitOnRepeat);
}
}
return output;
},
/**
* Adds a Key object to this Keyboard Plugin.
*
* The given argument can be either an existing Key object, a string, such as `A` or `SPACE`, or a key code value.
*
* If a Key object is given, and one already exists matching the same key code, the existing one is replaced with the new one.
*
* @method Phaser.Input.Keyboard.KeyboardPlugin#addKey
* @since 3.10.0
*
* @param {(Phaser.Input.Keyboard.Key|string|integer)} key - Either a Key object, a string, such as `A` or `SPACE`, or a key code value.
* @param {boolean} [enableCapture=true] - Automatically call `preventDefault` on the native DOM browser event for the key codes being added.
* @param {boolean} [emitOnRepeat=false] - Controls if the Key will continuously emit a 'down' event while being held down (true), or emit the event just once (false, the default).
*
* @return {Phaser.Input.Keyboard.Key} The newly created Key object, or a reference to it if it already existed in the keys array.
*/
addKey: function (key, enableCapture, emitOnRepeat)
{
if (enableCapture === undefined) { enableCapture = true; }
if (emitOnRepeat === undefined) { emitOnRepeat = false; }
var keys = this.keys;
if (key instanceof Key)
{
var idx = keys.indexOf(key);
if (idx > -1)
{
keys[idx] = key;
}
else
{
keys[key.keyCode] = key;
}
if (enableCapture)
{
this.addCapture(key.keyCode);
}
key.setEmitOnRepeat(emitOnRepeat);
return key;
}
if (typeof key === 'string')
{
key = KeyCodes[key.toUpperCase()];
}
if (!keys[key])
{
keys[key] = new Key(key);
if (enableCapture)
{
this.addCapture(key);
}
keys[key].setEmitOnRepeat(emitOnRepeat);
}
return keys[key];
},
/**
* Removes a Key object from this Keyboard Plugin.
*
* The given argument can be either a Key object, a string, such as `A` or `SPACE`, or a key code value.
*
* @method Phaser.Input.Keyboard.KeyboardPlugin#removeKey
* @since 3.10.0
*
* @param {(Phaser.Input.Keyboard.Key|string|integer)} key - Either a Key object, a string, such as `A` or `SPACE`, or a key code value.
*
* @return {Phaser.Input.Keyboard.KeyboardPlugin} This KeyboardPlugin object.
*/
removeKey: function (key)
{
var keys = this.keys;
if (key instanceof Key)
{
var idx = keys.indexOf(key);
if (idx > -1)
{
this.keys[idx] = undefined;
}
}
else if (typeof key === 'string')
{
key = KeyCodes[key.toUpperCase()];
}
if (keys[key])
{
keys[key] = undefined;
}
return this;
},
/**
* Creates a new KeyCombo.
*
* A KeyCombo will listen for a specific string of keys from the Keyboard, and when it receives them
* it will emit a `keycombomatch` event from this Keyboard Plugin.
*
* The keys to be listened for can be defined as:
*
* A string (i.e. 'ATARI')
* An array of either integers (key codes) or strings, or a mixture of both
* An array of objects (such as Key objects) with a public 'keyCode' property
*
* For example, to listen for the Konami code (up, up, down, down, left, right, left, right, b, a, enter)
* you could pass the following array of key codes:
*
* ```javascript
* this.input.keyboard.createCombo([ 38, 38, 40, 40, 37, 39, 37, 39, 66, 65, 13 ], { resetOnMatch: true });
*
* this.input.keyboard.on('keycombomatch', function (event) {
* console.log('Konami Code entered!');
* });
* ```
*
* Or, to listen for the user entering the word PHASER:
*
* ```javascript
* this.input.keyboard.createCombo('PHASER');
* ```
*
* @method Phaser.Input.Keyboard.KeyboardPlugin#createCombo
* @since 3.10.0
*
* @param {(string|integer[]|object[])} keys - The keys that comprise this combo.
* @param {Phaser.Input.Keyboard.Types.KeyComboConfig} [config] - A Key Combo configuration object.
*
* @return {Phaser.Input.Keyboard.KeyCombo} The new KeyCombo object.
*/
createCombo: function (keys, config)
{
return new KeyCombo(this, keys, config);
},
/**
* Checks if the given Key object is currently being held down.
*
* The difference between this method and checking the `Key.isDown` property directly is that you can provide
* a duration to this method. For example, if you wanted a key press to fire a bullet, but you only wanted
* it to be able to fire every 100ms, then you can call this method with a `duration` of 100 and it
* will only return `true` every 100ms.
*
* If the Keyboard Plugin has been disabled, this method will always return `false`.
*
* @method Phaser.Input.Keyboard.KeyboardPlugin#checkDown
* @since 3.11.0
*
* @param {Phaser.Input.Keyboard.Key} key - A Key object.
* @param {number} [duration=0] - The duration which must have elapsed before this Key is considered as being down.
*
* @return {boolean} `true` if the Key is down within the duration specified, otherwise `false`.
*/
checkDown: function (key, duration)
{
if (this.enabled && key.isDown)
{
var t = SnapFloor(this.time - key.timeDown, duration);
if (t > key._tick)
{
key._tick = t;
return true;
}
}
return false;
},
/**
* Internal update handler called by the Input Plugin, which is in turn invoked by the Game step.
*
* @method Phaser.Input.Keyboard.KeyboardPlugin#update
* @private
* @since 3.10.0
*/
update: function ()
{
var queue = this.manager.queue;
var len = queue.length;
if (!this.isActive() || len === 0)
{
return;
}
var keys = this.keys;
// Process the event queue, dispatching all of the events that have stored up
for (var i = 0; i < len; i++)
{
var event = queue[i];
var code = event.keyCode;
var key = keys[code];
var repeat = false;
// Override the default functions (it's too late for the browser to use them anyway, so we may as well)
if (event.cancelled === undefined)
{
// Event allowed to flow across all handlers in this Scene, and any other Scene in the Scene list
event.cancelled = 0;
// Won't reach any more local (Scene level) handlers
event.stopImmediatePropagation = function ()
{
event.cancelled = 1;
};
// Won't reach any more handlers in any Scene further down the Scene list
event.stopPropagation = function ()
{
event.cancelled = -1;
};
}
if (event.cancelled === -1)
{
// This event has been stopped from broadcasting to any other Scene, so abort.
continue;
}
if (event.type === 'keydown')
{
// Key specific callback first
if (key)
{
repeat = key.isDown;
key.onDown(event);
}
if (!event.cancelled && (!key || !repeat))
{
if (KeyMap[code])
{
this.emit(Events.KEY_DOWN + KeyMap[code], event);
// Deprecated, kept in for compatibility with 3.15
// To be removed by 3.20.
this.emit('keydown_' + KeyMap[code], event);
}
if (!event.cancelled)
{
this.emit(Events.ANY_KEY_DOWN, event);
}
}
}
else
{
// Key specific callback first
if (key)
{
key.onUp(event);
}
if (!event.cancelled)
{
if (KeyMap[code])
{
this.emit(Events.KEY_UP + KeyMap[code], event);
// Deprecated, kept in for compatibility with 3.15
// To be removed by 3.20.
this.emit('keyup_' + KeyMap[code], event);
}
if (!event.cancelled)
{
this.emit(Events.ANY_KEY_UP, event);
}
}
}
// Reset the cancel state for other Scenes to use
if (event.cancelled === 1)
{
event.cancelled = 0;
}
}
},
/**
* Resets all Key objects created by _this_ Keyboard Plugin back to their default un-pressed states.
* This can only reset keys created via the `addKey`, `addKeys` or `createCursorKeys` methods.
* If you have created a Key object directly you'll need to reset it yourself.
*
* This method is called automatically when the Keyboard Plugin shuts down, but can be
* invoked directly at any time you require.
*
* @method Phaser.Input.Keyboard.KeyboardPlugin#resetKeys
* @since 3.15.0
*
* @return {Phaser.Input.Keyboard.KeyboardPlugin} This KeyboardPlugin object.
*/
resetKeys: function ()
{
var keys = this.keys;
for (var i = 0; i < keys.length; i++)
{
// Because it's a sparsely populated array
if (keys[i])
{
keys[i].reset();
}
}
return this;
},
/**
* Shuts this Keyboard Plugin down. This performs the following tasks:
*
* 1 - Resets all keys created by this Keyboard plugin.
* 2 - Stops and removes the keyboard event listeners.
* 3 - Clears out any pending requests in the queue, without processing them.
*
* @method Phaser.Input.Keyboard.KeyboardPlugin#shutdown
* @private
* @since 3.10.0
*/
shutdown: function ()
{
this.resetKeys();
if (this.sceneInputPlugin.manager.useQueue)
{
this.sceneInputPlugin.pluginEvents.off(InputEvents.UPDATE, this.update, this);
}
else
{
this.sceneInputPlugin.manager.events.off(InputEvents.MANAGER_PROCESS, this.update, this);
}
this.game.events.off(GameEvents.BLUR, this.resetKeys);
this.removeAllListeners();
this.queue = [];
},
/**
* Destroys this Keyboard Plugin instance and all references it holds, plus clears out local arrays.
*
* @method Phaser.Input.Keyboard.KeyboardPlugin#destroy
* @private
* @since 3.10.0
*/
destroy: function ()
{
this.shutdown();
var keys = this.keys;
for (var i = 0; i < keys.length; i++)
{
// Because it's a sparsely populated array
if (keys[i])
{
keys[i].destroy();
}
}
this.keys = [];
this.combos = [];
this.queue = [];
this.scene = null;
this.settings = null;
this.sceneInputPlugin = null;
this.manager = null;
},
/**
* Internal time value.
*
* @name Phaser.Input.Keyboard.KeyboardPlugin#time
* @type {number}
* @private
* @since 3.11.0
*/
time: {
get: function ()
{
return this.sceneInputPlugin.manager.time;
}
}
});
/**
* An instance of the Keyboard Plugin class, if enabled via the `input.keyboard` Scene or Game Config property.
* Use this to create Key objects and listen for keyboard specific events.
*
* @name Phaser.Input.InputPlugin#keyboard
* @type {?Phaser.Input.Keyboard.KeyboardPlugin}
* @since 3.10.0
*/
InputPluginCache.register('KeyboardPlugin', KeyboardPlugin, 'keyboard', 'keyboard', 'inputKeyboard');
module.exports = KeyboardPlugin;