phaser
Version:
A fast, free and fun HTML5 Game Framework for Desktop and Mobile web browsers from the team at Phaser Studio Inc.
1,485 lines (1,263 loc) • 118 kB
JavaScript
/**
* @author Richard Davey <rich@phaser.io>
* @copyright 2013-2025 Phaser Studio Inc.
* @license {@link https://opensource.org/licenses/MIT|MIT License}
*/
var Circle = require('../geom/circle/Circle');
var CircleContains = require('../geom/circle/Contains');
var Class = require('../utils/Class');
var CONST = require('./const');
var CreateInteractiveObject = require('./CreateInteractiveObject');
var CreatePixelPerfectHandler = require('./CreatePixelPerfectHandler');
var DistanceBetween = require('../math/distance/DistanceBetween');
var Ellipse = require('../geom/ellipse/Ellipse');
var EllipseContains = require('../geom/ellipse/Contains');
var Events = require('./events');
var EventEmitter = require('eventemitter3');
var GetFastValue = require('../utils/object/GetFastValue');
var GEOM_CONST = require('../geom/const');
var InputPluginCache = require('./InputPluginCache');
var IsPlainObject = require('../utils/object/IsPlainObject');
var PluginCache = require('../plugins/PluginCache');
var Rectangle = require('../geom/rectangle/Rectangle');
var RectangleContains = require('../geom/rectangle/Contains');
var SceneEvents = require('../scene/events');
var Triangle = require('../geom/triangle/Triangle');
var TriangleContains = require('../geom/triangle/Contains');
/**
* @classdesc
* The Input Plugin belongs to a Scene and handles all input related events and operations for it.
*
* You can access it from within a Scene using `this.input`.
*
* It emits events directly. For example, you can do:
*
* ```javascript
* this.input.on('pointerdown', callback, context);
* ```
*
* To listen for a pointer down event anywhere on the game canvas.
*
* Game Objects can be enabled for input by calling their `setInteractive` method. After which they
* will directly emit input events:
*
* ```javascript
* var sprite = this.add.sprite(x, y, texture);
* sprite.setInteractive();
* sprite.on('pointerdown', callback, context);
* ```
*
* There are lots of game configuration options available relating to input.
* See the [Input Config object]{@linkcode Phaser.Types.Core.InputConfig} for more details, including how to deal with Phaser
* listening for input events outside of the canvas, how to set a default number of pointers, input
* capture settings and more.
*
* Please also see the Input examples and tutorials for further information.
*
* **Incorrect input coordinates with Angular**
*
* If you are using Phaser within Angular, and use nglf or the router, to make the component in which the Phaser game resides
* change state (i.e. appear or disappear) then you'll need to notify the Scale Manager about this, as Angular will mess with
* the DOM in a way in which Phaser can't detect directly. Call `this.scale.updateBounds()` as part of your game init in order
* to refresh the canvas DOM bounds values, which Phaser uses for input point position calculations.
*
* @class InputPlugin
* @extends Phaser.Events.EventEmitter
* @memberof Phaser.Input
* @constructor
* @since 3.0.0
*
* @param {Phaser.Scene} scene - A reference to the Scene that this Input Plugin is responsible for.
*/
var InputPlugin = new Class({
Extends: EventEmitter,
initialize:
function InputPlugin (scene)
{
EventEmitter.call(this);
/**
* A reference to the Scene that this Input Plugin is responsible for.
*
* @name Phaser.Input.InputPlugin#scene
* @type {Phaser.Scene}
* @since 3.0.0
*/
this.scene = scene;
/**
* A reference to the Scene Systems class.
*
* @name Phaser.Input.InputPlugin#systems
* @type {Phaser.Scenes.Systems}
* @since 3.0.0
*/
this.systems = scene.sys;
/**
* A reference to the Scene Systems Settings.
*
* @name Phaser.Input.InputPlugin#settings
* @type {Phaser.Types.Scenes.SettingsObject}
* @since 3.5.0
*/
this.settings = scene.sys.settings;
/**
* A reference to the Game Input Manager.
*
* @name Phaser.Input.InputPlugin#manager
* @type {Phaser.Input.InputManager}
* @since 3.0.0
*/
this.manager = scene.sys.game.input;
/**
* Internal event queue used for plugins only.
*
* @name Phaser.Input.InputPlugin#pluginEvents
* @type {Phaser.Events.EventEmitter}
* @private
* @since 3.10.0
*/
this.pluginEvents = new EventEmitter();
/**
* If `true` this Input Plugin will process DOM input events.
*
* @name Phaser.Input.InputPlugin#enabled
* @type {boolean}
* @default true
* @since 3.5.0
*/
this.enabled = true;
/**
* A reference to the Scene Display List. This property is set during the `boot` method.
*
* @name Phaser.Input.InputPlugin#displayList
* @type {Phaser.GameObjects.DisplayList}
* @since 3.0.0
*/
this.displayList;
/**
* A reference to the Scene Cameras Manager. This property is set during the `boot` method.
*
* @name Phaser.Input.InputPlugin#cameras
* @type {Phaser.Cameras.Scene2D.CameraManager}
* @since 3.0.0
*/
this.cameras;
// Inject the available input plugins into this class
InputPluginCache.install(this);
/**
* A reference to the Mouse Manager.
*
* This property is only set if Mouse support has been enabled in your Game Configuration file.
*
* If you just wish to get access to the mouse pointer, use the `mousePointer` property instead.
*
* @name Phaser.Input.InputPlugin#mouse
* @type {?Phaser.Input.Mouse.MouseManager}
* @since 3.0.0
*/
this.mouse = this.manager.mouse;
/**
* When set to `true` (the default) the Input Plugin will emulate DOM behavior by only emitting events from
* the top-most Game Objects in the Display List.
*
* If set to `false` it will emit events from all Game Objects below a Pointer, not just the top one.
*
* @name Phaser.Input.InputPlugin#topOnly
* @type {boolean}
* @default true
* @since 3.0.0
*/
this.topOnly = true;
/**
* How often should the Pointers be checked?
*
* The value is a time, given in ms, and is the time that must have elapsed between game steps before
* the Pointers will be polled again. When a pointer is polled it runs a hit test to see which Game
* Objects are currently below it, or being interacted with it.
*
* Pointers will *always* be checked if they have been moved by the user, or press or released.
*
* This property only controls how often they will be polled if they have not been updated.
* You should set this if you want to have Game Objects constantly check against the pointers, even
* if the pointer didn't itself move.
*
* Set to 0 to poll constantly. Set to -1 to only poll on user movement.
*
* @name Phaser.Input.InputPlugin#pollRate
* @type {number}
* @default -1
* @since 3.0.0
*/
this.pollRate = -1;
/**
* Internal poll timer value.
*
* @name Phaser.Input.InputPlugin#_pollTimer
* @type {number}
* @private
* @default 0
* @since 3.0.0
*/
this._pollTimer = 0;
var _eventData = { cancelled: false };
/**
* Internal event propagation callback container.
*
* @name Phaser.Input.InputPlugin#_eventContainer
* @type {Phaser.Types.Input.EventData}
* @private
* @since 3.13.0
*/
this._eventContainer = {
stopPropagation: function ()
{
_eventData.cancelled = true;
}
};
/**
* Internal event propagation data object.
*
* @name Phaser.Input.InputPlugin#_eventData
* @type {object}
* @private
* @since 3.13.0
*/
this._eventData = _eventData;
/**
* The distance, in pixels, a pointer has to move while being held down, before it thinks it is being dragged.
*
* @name Phaser.Input.InputPlugin#dragDistanceThreshold
* @type {number}
* @default 0
* @since 3.0.0
*/
this.dragDistanceThreshold = 0;
/**
* The amount of time, in ms, a pointer has to be held down before it thinks it is dragging.
*
* The default polling rate is to poll only on move so once the time threshold is reached the
* drag event will not start until you move the mouse. If you want it to start immediately
* when the time threshold is reached, you must increase the polling rate by calling
* [setPollAlways]{@linkcode Phaser.Input.InputPlugin#setPollAlways} or
* [setPollRate]{@linkcode Phaser.Input.InputPlugin#setPollRate}.
*
* @name Phaser.Input.InputPlugin#dragTimeThreshold
* @type {number}
* @default 0
* @since 3.0.0
*/
this.dragTimeThreshold = 0;
/**
* Used to temporarily store the results of the Hit Test
*
* @name Phaser.Input.InputPlugin#_temp
* @type {array}
* @private
* @default []
* @since 3.0.0
*/
this._temp = [];
/**
* Used to temporarily store the results of the Hit Test dropZones
*
* @name Phaser.Input.InputPlugin#_tempZones
* @type {array}
* @private
* @default []
* @since 3.0.0
*/
this._tempZones = [];
/**
* A list of all Game Objects that have been set to be interactive in the Scene this Input Plugin is managing.
*
* @name Phaser.Input.InputPlugin#_list
* @type {Phaser.GameObjects.GameObject[]}
* @private
* @default []
* @since 3.0.0
*/
this._list = [];
/**
* Objects waiting to be inserted to the list on the next call to 'begin'.
*
* @name Phaser.Input.InputPlugin#_pendingInsertion
* @type {Phaser.GameObjects.GameObject[]}
* @private
* @default []
* @since 3.0.0
*/
this._pendingInsertion = [];
/**
* Objects waiting to be removed from the list on the next call to 'begin'.
*
* @name Phaser.Input.InputPlugin#_pendingRemoval
* @type {Phaser.GameObjects.GameObject[]}
* @private
* @default []
* @since 3.0.0
*/
this._pendingRemoval = [];
/**
* A list of all Game Objects that have been enabled for dragging.
*
* @name Phaser.Input.InputPlugin#_draggable
* @type {Phaser.GameObjects.GameObject[]}
* @private
* @default []
* @since 3.0.0
*/
this._draggable = [];
/**
* A list of all Interactive Objects currently considered as being 'draggable' by any pointer, indexed by pointer ID.
*
* @name Phaser.Input.InputPlugin#_drag
* @type {{0:Array,1:Array,2:Array,3:Array,4:Array,5:Array,6:Array,7:Array,8:Array,9:Array,10:Array}}
* @private
* @since 3.0.0
*/
this._drag = { 0: [], 1: [], 2: [], 3: [], 4: [], 5: [], 6: [], 7: [], 8: [], 9: [], 10: [] };
/**
* A array containing the dragStates, for this Scene, index by the Pointer ID.
*
* @name Phaser.Input.InputPlugin#_dragState
* @type {number[]}
* @private
* @since 3.16.0
*/
this._dragState = [];
/**
* A list of all Interactive Objects currently considered as being 'over' by any pointer, indexed by pointer ID.
*
* @name Phaser.Input.InputPlugin#_over
* @type {{0:Array,1:Array,2:Array,3:Array,4:Array,5:Array,6:Array,7:Array,8:Array,9:Array,10:Array}}
* @private
* @since 3.0.0
*/
this._over = { 0: [], 1: [], 2: [], 3: [], 4: [], 5: [], 6: [], 7: [], 8: [], 9: [], 10: [] };
/**
* A list of valid DOM event types.
*
* @name Phaser.Input.InputPlugin#_validTypes
* @type {string[]}
* @private
* @since 3.0.0
*/
this._validTypes = [ 'onDown', 'onUp', 'onOver', 'onOut', 'onMove', 'onDragStart', 'onDrag', 'onDragEnd', 'onDragEnter', 'onDragLeave', 'onDragOver', 'onDrop' ];
/**
* Internal property that tracks frame event state.
*
* @name Phaser.Input.InputPlugin#_updatedThisFrame
* @type {boolean}
* @private
* @since 3.18.0
*/
this._updatedThisFrame = false;
scene.sys.events.once(SceneEvents.BOOT, this.boot, this);
scene.sys.events.on(SceneEvents.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.InputPlugin#boot
* @fires Phaser.Input.Events#BOOT
* @private
* @since 3.5.1
*/
boot: function ()
{
this.cameras = this.systems.cameras;
this.displayList = this.systems.displayList;
this.systems.events.once(SceneEvents.DESTROY, this.destroy, this);
// Registered input plugins listen for this
this.pluginEvents.emit(Events.BOOT);
},
/**
* 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.InputPlugin#start
* @fires Phaser.Input.Events#START
* @private
* @since 3.5.0
*/
start: function ()
{
var eventEmitter = this.systems.events;
eventEmitter.on(SceneEvents.TRANSITION_START, this.transitionIn, this);
eventEmitter.on(SceneEvents.TRANSITION_OUT, this.transitionOut, this);
eventEmitter.on(SceneEvents.TRANSITION_COMPLETE, this.transitionComplete, this);
eventEmitter.on(SceneEvents.PRE_UPDATE, this.preUpdate, this);
eventEmitter.once(SceneEvents.SHUTDOWN, this.shutdown, this);
this.manager.events.on(Events.GAME_OUT, this.onGameOut, this);
this.manager.events.on(Events.GAME_OVER, this.onGameOver, this);
this.enabled = true;
// Populate the pointer drag states
this._dragState = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ];
// Registered input plugins listen for this
this.pluginEvents.emit(Events.START);
},
/**
* Game Over handler.
*
* @method Phaser.Input.InputPlugin#onGameOver
* @fires Phaser.Input.Events#GAME_OVER
* @private
* @since 3.16.2
*/
onGameOver: function (event)
{
if (this.isActive())
{
this.emit(Events.GAME_OVER, event.timeStamp, event);
}
},
/**
* Game Out handler.
*
* @method Phaser.Input.InputPlugin#onGameOut
* @fires Phaser.Input.Events#GAME_OUT
* @private
* @since 3.16.2
*/
onGameOut: function (event)
{
if (this.isActive())
{
this.emit(Events.GAME_OUT, event.timeStamp, event);
}
},
/**
* The pre-update handler is responsible for checking the pending removal and insertion lists and
* deleting old Game Objects.
*
* @method Phaser.Input.InputPlugin#preUpdate
* @private
* @fires Phaser.Input.Events#PRE_UPDATE
* @since 3.0.0
*/
preUpdate: function ()
{
// Registered input plugins listen for this
this.pluginEvents.emit(Events.PRE_UPDATE);
var removeList = this._pendingRemoval;
var insertList = this._pendingInsertion;
var toRemove = removeList.length;
var toInsert = insertList.length;
if (toRemove === 0 && toInsert === 0)
{
// Quick bail
return;
}
var current = this._list;
// Delete old gameObjects
for (var i = 0; i < toRemove; i++)
{
var gameObject = removeList[i];
var index = current.indexOf(gameObject);
if (index > -1)
{
current.splice(index, 1);
this.clear(gameObject, true);
}
}
// Clear the removal list
this._pendingRemoval.length = 0;
// Move pendingInsertion to list (also clears pendingInsertion at the same time)
this._list = current.concat(insertList.splice(0));
},
/**
* Checks to see if the Input Manager, this plugin and the Scene to which it belongs are all active and input enabled.
*
* @method Phaser.Input.InputPlugin#isActive
* @since 3.10.0
*
* @return {boolean} `true` if the plugin and the Scene it belongs to is active.
*/
isActive: function ()
{
return (this.manager && this.manager.enabled && this.enabled && this.scene.sys.canInput());
},
/**
* Sets a custom cursor on the parent canvas element of the game, based on the `cursor`
* setting of the given Interactive Object (i.e. a Sprite).
*
* See the CSS property `cursor` for more information on MDN:
*
* https://developer.mozilla.org/en-US/docs/Web/CSS/cursor
*
* @method Phaser.Input.InputPlugin#setCursor
* @since 3.85.0
*
* @param {Phaser.Types.Input.InteractiveObject} interactiveObject - The Interactive Object that will set the cursor on the canvas.
*/
setCursor: function (interactiveObject)
{
if (this.manager)
{
this.manager.setCursor(interactiveObject);
}
},
/**
* Forces the Input Manager to clear the custom or hand cursor, regardless of the
* interactive state of any Game Objects.
*
* @method Phaser.Input.InputPlugin#resetCursor
* @since 3.85.0
*/
resetCursor: function ()
{
if (this.manager)
{
this.manager.resetCursor(null, true);
}
},
/**
* This is called automatically by the Input Manager.
* It emits events for plugins to listen to and also handles polling updates, if enabled.
*
* @method Phaser.Input.InputPlugin#updatePoll
* @since 3.18.0
*
* @param {number} time - The current time. Either a High Resolution Timer value if it comes from Request Animation Frame, or Date.now if using SetTimeout.
* @param {number} delta - The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate.
*
* @return {boolean} `true` if the plugin and the Scene it belongs to is active.
*/
updatePoll: function (time, delta)
{
if (!this.isActive())
{
return false;
}
// The plugins should update every frame, regardless if there has been
// any DOM input events or not (such as the Gamepad and Keyboard)
this.pluginEvents.emit(Events.UPDATE, time, delta);
// We can leave now if we've already updated once this frame via the immediate DOM event handlers
if (this._updatedThisFrame)
{
this._updatedThisFrame = false;
return false;
}
var i;
var manager = this.manager;
var pointers = manager.pointers;
for (i = 0; i < pointers.length; i++)
{
pointers[i].updateMotion();
}
// No point going any further if there aren't any interactive objects
if (this._list.length === 0)
{
return false;
}
var rate = this.pollRate;
if (rate === -1)
{
return false;
}
else if (rate > 0)
{
this._pollTimer -= delta;
if (this._pollTimer < 0)
{
// Discard timer diff, we're ready to poll again
this._pollTimer = this.pollRate;
}
else
{
// Not enough time has elapsed since the last poll, so abort now
return false;
}
}
// We got this far? Then we should poll for movement
var captured = false;
for (i = 0; i < pointers.length; i++)
{
var total = 0;
var pointer = pointers[i];
// Always reset this array
this._tempZones = [];
// _temp contains a hit tested and camera culled list of IO objects
this._temp = this.hitTestPointer(pointer);
this.sortGameObjects(this._temp, pointer);
this.sortDropZones(this._tempZones);
if (this.topOnly)
{
// Only the top-most one counts now, so safely ignore the rest
if (this._temp.length)
{
this._temp.splice(1);
}
if (this._tempZones.length)
{
this._tempZones.splice(1);
}
}
total += this.processOverOutEvents(pointer);
if (this.getDragState(pointer) === 2)
{
this.processDragThresholdEvent(pointer, time);
}
if (total > 0)
{
// We interacted with an event in this Scene, so block any Scenes below us from doing the same this frame
captured = true;
}
}
return captured;
},
/**
* This method is called when a DOM Event is received by the Input Manager. It handles dispatching the events
* to relevant input enabled Game Objects in this scene.
*
* @method Phaser.Input.InputPlugin#update
* @private
* @fires Phaser.Input.Events#UPDATE
* @since 3.0.0
*
* @param {number} type - The type of event to process.
* @param {Phaser.Input.Pointer[]} pointers - An array of Pointers on which the event occurred.
*
* @return {boolean} `true` if this Scene has captured the input events from all other Scenes, otherwise `false`.
*/
update: function (type, pointers)
{
if (!this.isActive())
{
return false;
}
var captured = false;
for (var i = 0; i < pointers.length; i++)
{
var total = 0;
var pointer = pointers[i];
// Always reset this array
this._tempZones = [];
// _temp contains a hit tested and camera culled list of IO objects
this._temp = this.hitTestPointer(pointer);
this.sortGameObjects(this._temp, pointer);
this.sortDropZones(this._tempZones);
if (this.topOnly)
{
// Only the top-most one counts now, so safely ignore the rest
if (this._temp.length)
{
this._temp.splice(1);
}
if (this._tempZones.length)
{
this._tempZones.splice(1);
}
}
switch (type)
{
case CONST.MOUSE_DOWN:
total += this.processDragDownEvent(pointer);
total += this.processDownEvents(pointer);
total += this.processOverOutEvents(pointer);
break;
case CONST.MOUSE_UP:
total += this.processDragUpEvent(pointer);
total += this.processUpEvents(pointer);
total += this.processOverOutEvents(pointer);
break;
case CONST.TOUCH_START:
total += this.processDragDownEvent(pointer);
total += this.processDownEvents(pointer);
total += this.processOverEvents(pointer);
break;
case CONST.TOUCH_END:
case CONST.TOUCH_CANCEL:
total += this.processDragUpEvent(pointer);
total += this.processUpEvents(pointer);
total += this.processOutEvents(pointer);
break;
case CONST.MOUSE_MOVE:
case CONST.TOUCH_MOVE:
total += this.processDragMoveEvent(pointer);
total += this.processMoveEvents(pointer);
total += this.processOverOutEvents(pointer);
break;
case CONST.MOUSE_WHEEL:
total += this.processWheelEvent(pointer);
break;
}
if (total > 0)
{
// We interacted with an event in this Scene, so block any Scenes below us from doing the same this frame
captured = true;
}
}
this._updatedThisFrame = true;
return captured;
},
/**
* Clears a Game Object so it no longer has an Interactive Object associated with it.
* The Game Object is then queued for removal from the Input Plugin on the next update.
*
* @method Phaser.Input.InputPlugin#clear
* @since 3.0.0
*
* @param {Phaser.GameObjects.GameObject} gameObject - The Game Object that will have its Interactive Object removed.
* @param {boolean} [skipQueue=false] - Skip adding this Game Object into the removal queue?
*
* @return {Phaser.GameObjects.GameObject} The Game Object that had its Interactive Object removed.
*/
clear: function (gameObject, skipQueue)
{
if (skipQueue === undefined) { skipQueue = false; }
this.disable(gameObject);
var input = gameObject.input;
// If GameObject.input already cleared from higher class
if (input)
{
this.removeDebug(gameObject);
this.manager.resetCursor(input);
input.gameObject = undefined;
input.target = undefined;
input.hitArea = undefined;
input.hitAreaCallback = undefined;
input.callbackContext = undefined;
gameObject.input = null;
}
if (!skipQueue)
{
this.queueForRemoval(gameObject);
}
var index = this._draggable.indexOf(gameObject);
if (index > -1)
{
this._draggable.splice(index, 1);
}
return gameObject;
},
/**
* Disables Input on a single Game Object.
*
* An input disabled Game Object still retains its Interactive Object component and can be re-enabled
* at any time, by passing it to `InputPlugin.enable`.
*
* @method Phaser.Input.InputPlugin#disable
* @since 3.0.0
*
* @param {Phaser.GameObjects.GameObject} gameObject - The Game Object to have its input system disabled.
* @param {boolean} [resetCursor=false] - Reset the cursor to the default?
*
* @return {this} This Input Plugin.
*/
disable: function (gameObject, resetCursor)
{
if (resetCursor === undefined) { resetCursor = false; }
var input = gameObject.input;
if (input)
{
input.enabled = false;
input.dragState = 0;
}
// Clear from _drag and _over
var drag = this._drag;
var over = this._over;
var manager = this.manager;
for (var i = 0, index; i < manager.pointers.length; i++)
{
index = drag[i].indexOf(gameObject);
if (index > -1)
{
drag[i].splice(index, 1);
}
index = over[i].indexOf(gameObject);
if (index > -1)
{
over[i].splice(index, 1);
}
}
if (resetCursor)
{
this.resetCursor();
}
return this;
},
/**
* Enable a Game Object for interaction.
*
* If the Game Object already has an Interactive Object component, it is enabled and returned.
*
* Otherwise, a new Interactive Object component is created and assigned to the Game Object's `input` property.
*
* Input works by using hit areas, these are nearly always geometric shapes, such as rectangles or circles, that act as the hit area
* for the Game Object. However, you can provide your own hit area shape and callback, should you wish to handle some more advanced
* input detection.
*
* If no arguments are provided it will try and create a rectangle hit area based on the texture frame the Game Object is using. If
* this isn't a texture-bound object, such as a Graphics or BitmapText object, this will fail, and you'll need to provide a specific
* shape for it to use.
*
* You can also provide an Input Configuration Object as the only argument to this method.
*
* @method Phaser.Input.InputPlugin#enable
* @since 3.0.0
*
* @param {Phaser.GameObjects.GameObject} gameObject - The Game Object to be enabled for input.
* @param {(Phaser.Types.Input.InputConfiguration|any)} [hitArea] - Either an input configuration object, or a geometric shape that defines the hit area for the Game Object. If not specified a Rectangle will be used.
* @param {Phaser.Types.Input.HitAreaCallback} [hitAreaCallback] - The 'contains' function to invoke to check if the pointer is within the hit area.
* @param {boolean} [dropZone=false] - Is this Game Object a drop zone or not?
*
* @return {this} This Input Plugin.
*/
enable: function (gameObject, hitArea, hitAreaCallback, dropZone)
{
if (dropZone === undefined) { dropZone = false; }
if (gameObject.input)
{
// If it already has an InteractiveObject then just enable it and return
gameObject.input.enabled = true;
}
else
{
// Create an InteractiveObject and enable it
this.setHitArea(gameObject, hitArea, hitAreaCallback);
}
if (gameObject.input && dropZone && !gameObject.input.dropZone)
{
gameObject.input.dropZone = dropZone;
}
return this;
},
/**
* Takes the given Pointer and performs a hit test against it, to see which interactive Game Objects
* it is currently above.
*
* The hit test is performed against which-ever Camera the Pointer is over. If it is over multiple
* cameras, it starts checking the camera at the top of the camera list, and if nothing is found, iterates down the list.
*
* @method Phaser.Input.InputPlugin#hitTestPointer
* @since 3.0.0
*
* @param {Phaser.Input.Pointer} pointer - The Pointer to check against the Game Objects.
*
* @return {Phaser.GameObjects.GameObject[]} An array of all the interactive Game Objects the Pointer was above.
*/
hitTestPointer: function (pointer)
{
var cameras = this.cameras.getCamerasBelowPointer(pointer);
for (var c = 0; c < cameras.length; c++)
{
var camera = cameras[c];
// Get a list of all objects that can be seen by the camera below the pointer in the scene and store in 'over' array.
// All objects in this array are input enabled, as checked by the hitTest method, so we don't need to check later on as well.
var over = this.manager.hitTest(pointer, this._list, camera);
// Filter out the drop zones
for (var i = 0; i < over.length; i++)
{
var obj = over[i];
if (obj.input.dropZone)
{
this._tempZones.push(obj);
}
}
if (over.length > 0)
{
pointer.camera = camera;
return over;
}
}
// If we got this far then there were no Game Objects below the pointer, but it was still over
// a camera, so set that the top-most one into the pointer
pointer.camera = cameras[0];
return [];
},
/**
* An internal method that handles the Pointer down event.
*
* @method Phaser.Input.InputPlugin#processDownEvents
* @private
* @fires Phaser.Input.Events#GAMEOBJECT_POINTER_DOWN
* @fires Phaser.Input.Events#GAMEOBJECT_DOWN
* @fires Phaser.Input.Events#POINTER_DOWN
* @fires Phaser.Input.Events#POINTER_DOWN_OUTSIDE
* @since 3.0.0
*
* @param {Phaser.Input.Pointer} pointer - The Pointer being tested.
*
* @return {number} The total number of objects interacted with.
*/
processDownEvents: function (pointer)
{
var total = 0;
var currentlyOver = this._temp;
var _eventData = this._eventData;
var _eventContainer = this._eventContainer;
_eventData.cancelled = false;
// Go through all objects the pointer was over and fire their events / callbacks
for (var i = 0; i < currentlyOver.length; i++)
{
var gameObject = currentlyOver[i];
if (!gameObject.input || !gameObject.input.enabled)
{
continue;
}
total++;
// 1) GAMEOBJECT_POINTER_DOWN
gameObject.emit(Events.GAMEOBJECT_POINTER_DOWN, pointer, gameObject.input.localX, gameObject.input.localY, _eventContainer);
if (_eventData.cancelled || !this.isActive())
{
// They cancelled the whole event, it can't go any further
break;
}
// Check that the game object wasn't input disabled or destroyed as a result of its input event
if (gameObject.input && gameObject.input.enabled)
{
// 2) GAMEOBJECT_DOWN
this.emit(Events.GAMEOBJECT_DOWN, pointer, gameObject, _eventContainer);
if (_eventData.cancelled || !this.isActive())
{
// They cancelled the whole event, it can't go any further
break;
}
}
}
// If they pressed down outside the canvas, dispatch that event.
if (!_eventData.cancelled && this.isActive())
{
if (pointer.downElement === this.manager.game.canvas)
{
// 3) POINTER_DOWN
this.emit(Events.POINTER_DOWN, pointer, currentlyOver);
}
else
{
// 4) POINTER_DOWN_OUTSIDE
this.emit(Events.POINTER_DOWN_OUTSIDE, pointer);
}
}
return total;
},
/**
* Returns the drag state of the given Pointer for this Input Plugin.
*
* The state will be one of the following:
*
* 0 = Not dragging anything
* 1 = Primary button down and objects below, so collect a draglist
* 2 = Pointer being checked if meets drag criteria
* 3 = Pointer meets criteria, notify the draglist
* 4 = Pointer actively dragging the draglist and has moved
* 5 = Pointer actively dragging but has been released, notify draglist
*
* @method Phaser.Input.InputPlugin#getDragState
* @since 3.16.0
*
* @param {Phaser.Input.Pointer} pointer - The Pointer to get the drag state for.
*
* @return {number} The drag state of the given Pointer.
*/
getDragState: function (pointer)
{
return this._dragState[pointer.id];
},
/**
* Sets the drag state of the given Pointer for this Input Plugin.
*
* The state must be one of the following values:
*
* 0 = Not dragging anything
* 1 = Primary button down and objects below, so collect a draglist
* 2 = Pointer being checked if meets drag criteria
* 3 = Pointer meets criteria, notify the draglist
* 4 = Pointer actively dragging the draglist and has moved
* 5 = Pointer actively dragging but has been released, notify draglist
*
* @method Phaser.Input.InputPlugin#setDragState
* @since 3.16.0
*
* @param {Phaser.Input.Pointer} pointer - The Pointer to set the drag state for.
* @param {number} state - The drag state value. An integer between 0 and 5.
*/
setDragState: function (pointer, state)
{
this._dragState[pointer.id] = state;
},
/**
* Checks to see if a Pointer is ready to drag the objects below it, based on either a distance
* or time threshold.
*
* @method Phaser.Input.InputPlugin#processDragThresholdEvent
* @private
* @since 3.18.0
*
* @param {Phaser.Input.Pointer} pointer - The Pointer to check the drag thresholds on.
* @param {number} time - The current time.
*/
processDragThresholdEvent: function (pointer, time)
{
var passed = false;
var timeThreshold = this.dragTimeThreshold;
var distanceThreshold = this.dragDistanceThreshold;
if (distanceThreshold > 0 && DistanceBetween(pointer.x, pointer.y, pointer.downX, pointer.downY) >= distanceThreshold)
{
// It has moved far enough to be considered a drag
passed = true;
}
else if (timeThreshold > 0 && (time >= pointer.downTime + timeThreshold))
{
// It has been held down long enough to be considered a drag
passed = true;
}
if (passed)
{
this.setDragState(pointer, 3);
return this.processDragStartList(pointer);
}
},
/**
* Processes the drag list for the given pointer and dispatches the start events for each object on it.
*
* @method Phaser.Input.InputPlugin#processDragStartList
* @private
* @fires Phaser.Input.Events#DRAG_START
* @fires Phaser.Input.Events#GAMEOBJECT_DRAG_START
* @since 3.18.0
*
* @param {Phaser.Input.Pointer} pointer - The Pointer to process the drag event on.
*
* @return {number} The number of items that DRAG_START was called on.
*/
processDragStartList: function (pointer)
{
// 3 = Pointer meets criteria and is freshly down, notify the draglist
if (this.getDragState(pointer) !== 3)
{
return 0;
}
var list = this._drag[pointer.id];
if (list.length > 1)
{
list = list.slice(0);
}
for (var i = 0; i < list.length; i++)
{
var gameObject = list[i];
var input = gameObject.input;
input.dragState = 2;
input.dragStartX = gameObject.x;
input.dragStartY = gameObject.y;
input.dragStartXGlobal = pointer.worldX;
input.dragStartYGlobal = pointer.worldY;
input.dragStartCamera = pointer.camera;
input.dragX = input.dragStartXGlobal - input.dragStartX;
input.dragY = input.dragStartYGlobal - input.dragStartY;
gameObject.emit(Events.GAMEOBJECT_DRAG_START, pointer, input.dragX, input.dragY);
this.emit(Events.DRAG_START, pointer, gameObject);
}
this.setDragState(pointer, 4);
return list.length;
},
/**
* Processes a 'drag down' event for the given pointer. Checks the pointer state, builds-up the drag list
* and prepares them all for interaction.
*
* @method Phaser.Input.InputPlugin#processDragDownEvent
* @private
* @since 3.18.0
*
* @param {Phaser.Input.Pointer} pointer - The Pointer to process the drag event on.
*
* @return {number} The number of items that were collected on the drag list.
*/
processDragDownEvent: function (pointer)
{
var currentlyOver = this._temp;
if (this._draggable.length === 0 || currentlyOver.length === 0 || !pointer.primaryDown || this.getDragState(pointer) !== 0)
{
// There are no draggable items, no over items or the pointer isn't down, so let's not even bother going further
return 0;
}
// 1 = Primary button down and objects below, so collect a draglist
this.setDragState(pointer, 1);
// Get draggable objects, sort them, pick the top (or all) and store them somewhere
var draglist = [];
for (var i = 0; i < currentlyOver.length; i++)
{
var gameObject = currentlyOver[i];
if (gameObject.input.draggable && (gameObject.input.dragState === 0))
{
draglist.push(gameObject);
}
}
if (draglist.length === 0)
{
this.setDragState(pointer, 0);
return 0;
}
else if (draglist.length > 1)
{
this.sortGameObjects(draglist, pointer);
if (this.topOnly)
{
draglist.splice(1);
}
}
// draglist now contains all potential candidates for dragging
this._drag[pointer.id] = draglist;
if (this.dragDistanceThreshold === 0 && this.dragTimeThreshold === 0)
{
// No drag criteria, so snap immediately to mode 3
this.setDragState(pointer, 3);
return this.processDragStartList(pointer);
}
else
{
// Check the distance / time on the next event
this.setDragState(pointer, 2);
return 0;
}
},
/**
* Processes a 'drag move' event for the given pointer.
*
* @method Phaser.Input.InputPlugin#processDragMoveEvent
* @private
* @fires Phaser.Input.Events#DRAG_ENTER
* @fires Phaser.Input.Events#DRAG
* @fires Phaser.Input.Events#DRAG_LEAVE
* @fires Phaser.Input.Events#DRAG_OVER
* @fires Phaser.Input.Events#GAMEOBJECT_DRAG_ENTER
* @fires Phaser.Input.Events#GAMEOBJECT_DRAG
* @fires Phaser.Input.Events#GAMEOBJECT_DRAG_LEAVE
* @fires Phaser.Input.Events#GAMEOBJECT_DRAG_OVER
* @since 3.18.0
*
* @param {Phaser.Input.Pointer} pointer - The Pointer to process the drag event on.
*
* @return {number} The number of items that were updated by this drag event.
*/
processDragMoveEvent: function (pointer)
{
// 2 = Pointer being checked if meets drag criteria
if (this.getDragState(pointer) === 2)
{
this.processDragThresholdEvent(pointer, this.manager.game.loop.now);
}
if (this.getDragState(pointer) !== 4)
{
return 0;
}
// 4 = Pointer actively dragging the draglist and has moved
var dropZones = this._tempZones;
var list = this._drag[pointer.id];
if (list.length > 1)
{
list = list.slice(0);
}
for (var i = 0; i < list.length; i++)
{
var gameObject = list[i];
var input = gameObject.input;
var target = input.target;
// If this GO has a target then let's check it
if (target)
{
var index = dropZones.indexOf(target);
// Got a target, are we still over it?
if (index === 0)
{
// We're still over it, and it's still the top of the display list, phew ...
gameObject.emit(Events.GAMEOBJECT_DRAG_OVER, pointer, target);
this.emit(Events.DRAG_OVER, pointer, gameObject, target);
}
else if (index > 0)
{
// Still over it but it's no longer top of the display list (targets must always be at the top)
gameObject.emit(Events.GAMEOBJECT_DRAG_LEAVE, pointer, target);
this.emit(Events.DRAG_LEAVE, pointer, gameObject, target);
input.target = dropZones[0];
target = input.target;
gameObject.emit(Events.GAMEOBJECT_DRAG_ENTER, pointer, target);
this.emit(Events.DRAG_ENTER, pointer, gameObject, target);
}
else
{
// Nope, we've moved on (or the target has!), leave the old target
gameObject.emit(Events.GAMEOBJECT_DRAG_LEAVE, pointer, target);
this.emit(Events.DRAG_LEAVE, pointer, gameObject, target);
// Anything new to replace it?
// Yup!
if (dropZones[0])
{
input.target = dropZones[0];
target = input.target;
gameObject.emit(Events.GAMEOBJECT_DRAG_ENTER, pointer, target);
this.emit(Events.DRAG_ENTER, pointer, gameObject, target);
}
else
{
// Nope
input.target = null;
}
}
}
else if (!target && dropZones[0])
{
input.target = dropZones[0];
target = input.target;
gameObject.emit(Events.GAMEOBJECT_DRAG_ENTER, pointer, target);
this.emit(Events.DRAG_ENTER, pointer, gameObject, target);
}
var dragX;
var dragY;
var dragWorldXY = pointer.positionToCamera(input.dragStartCamera);
if (!gameObject.parentContainer)
{
dragX = dragWorldXY.x - input.dragX;
dragY = dragWorldXY.y - input.dragY;
}
else
{
var dx = dragWorldXY.x - input.dragStartXGlobal;
var dy = dragWorldXY.y - input.dragStartYGlobal;
var rotation = gameObject.getParentRotation();
var dxRotated = dx * Math.cos(rotation) + dy * Math.sin(rotation);
var dyRotated = dy * Math.cos(rotation) - dx * Math.sin(rotation);
dxRotated *= (1 / gameObject.parentContainer.scaleX);
dyRotated *= (1 / gameObject.parentContainer.scaleY);
dragX = dxRotated + input.dragStartX;
dragY = dyRotated + input.dragStartY;
}
gameObject.emit(Events.GAMEOBJECT_DRAG, pointer, dragX, dragY);
this.emit(Events.DRAG, pointer, gameObject, dragX, dragY);
}
return list.length;
},
/**
* Processes a 'drag down' event for the given pointer. Checks the pointer state, builds-up the drag list
* and prepares them all for interaction.
*
* @method Phaser.Input.InputPlugin#processDragUpEvent
* @fires Phaser.Input.Events#DRAG_END
* @fires Phaser.Input.Events#DROP
* @fires Phaser.Input.Events#GAMEOBJECT_DRAG_END
* @fires Phaser.Input.Events#GAMEOBJECT_DROP
* @private
* @since 3.18.0
*
* @param {Phaser.Input.Pointer} pointer - The Pointer to process the drag event on.
*
* @return {number} The number of items that were updated by this drag event.
*/
processDragUpEvent: function (pointer)
{
// 5 = Pointer was actively dragging but has been released, notify draglist
var list = this._drag[pointer.id];
if (list.length > 1)
{
list = list.slice(0);
}
for (var i = 0; i < list.length; i++)
{
var gameObject = list[i];
var input = gameObject.input;
if (input && input.dragState === 2)
{
input.dragState = 0;
input.dragX = input.localX - gameObject.displayOriginX;