import Base from '../core/Base';
import Vector3 from '../math/Vector3';
import vendor from '../core/vendor';
/**
* Gamepad Control plugin.
*
* @constructor clay.plugin.GamepadControl
*
* @example
* init: function(app) {
* this._gamepadControl = new clay.plugin.GamepadControl({
* target: camera,
* onStandardGamepadReady: customCallback
* });
* },
*
* loop: function(app) {
* this._gamepadControl.update(app.frameTime);
* }
*/
var GamepadControl = Base.extend(function() {
return /** @lends clay.plugin.GamepadControl# */ {
/**
* Scene node to control, mostly it is a camera.
*
* @type {clay.Node}
*/
target: null,
/**
* Move speed.
*
* @type {number}
*/
moveSpeed: 0.1,
/**
* Look around speed.
*
* @type {number}
*/
lookAroundSpeed: 0.1,
/**
* Up axis.
*
* @type {clay.Vector3}
*/
up: new Vector3(0, 1, 0),
/**
* Timeline.
*
* @type {clay.Timeline}
*/
timeline: null,
/**
* Function to be called when a standard gamepad is ready to use.
*
* @type {function}
*/
onStandardGamepadReady: function(gamepad){},
/**
* Function to be called when a gamepad is disconnected.
*
* @type {function}
*/
onGamepadDisconnected: function(gamepad){},
// Private properties:
_moveForward: false,
_moveBackward: false,
_moveLeft: false,
_moveRight: false,
_offsetPitch: 0,
_offsetRoll: 0,
_connectedGamepadIndex: 0,
_standardGamepadAvailable: false,
_gamepadAxisThreshold: 0.3
};
}, function() {
this._checkGamepadCompatibility = this._checkGamepadCompatibility.bind(this);
this._disconnectGamepad = this._disconnectGamepad.bind(this);
this._getStandardGamepad = this._getStandardGamepad.bind(this);
this._scanPressedGamepadButtons = this._scanPressedGamepadButtons.bind(this);
this._scanInclinedGamepadAxes = this._scanInclinedGamepadAxes.bind(this);
this.update = this.update.bind(this);
// If browser supports Gamepad API:
if (typeof navigator.getGamepads === 'function') {
this.init();
}
},
/** @lends clay.plugin.GamepadControl.prototype */
{
/**
* Init. control.
*/
init: function() {
/**
* When user begins to interact with connected gamepad:
*
* @see https://w3c.github.io/gamepad/#dom-gamepadevent
*/
vendor.addEventListener(window, 'gamepadconnected', this._checkGamepadCompatibility);
if (this.timeline) {
this.timeline.on('frame', this.update);
}
vendor.addEventListener(window, 'gamepaddisconnected', this._disconnectGamepad);
},
/**
* Dispose control.
*/
dispose: function() {
vendor.removeEventListener(window, 'gamepadconnected', this._checkGamepadCompatibility);
if (this.timeline) {
this.timeline.off('frame', this.update);
}
vendor.removeEventListener(window, 'gamepaddisconnected', this._disconnectGamepad);
},
/**
* Control's update. Should be invoked every frame.
*
* @param {number} frameTime Frame time.
*/
update: function (frameTime) {
if (!this._standardGamepadAvailable) {
return;
}
this._scanPressedGamepadButtons();
this._scanInclinedGamepadAxes();
// Update target depending on user input.
var target = this.target;
var position = this.target.position;
var xAxis = target.localTransform.x.normalize();
var zAxis = target.localTransform.z.normalize();
var moveSpeed = this.moveSpeed * frameTime / 20;
if (this._moveForward) {
// Opposite direction of z.
position.scaleAndAdd(zAxis, -moveSpeed);
}
if (this._moveBackward) {
position.scaleAndAdd(zAxis, moveSpeed);
}
if (this._moveLeft) {
position.scaleAndAdd(xAxis, -moveSpeed);
}
if (this._moveRight) {
position.scaleAndAdd(xAxis, moveSpeed);
}
target.rotateAround(target.position, this.up, -this._offsetPitch * frameTime * Math.PI / 360);
var xAxis = target.localTransform.x;
target.rotateAround(target.position, xAxis, -this._offsetRoll * frameTime * Math.PI / 360);
/*
* If necessary: trigger `update` event.
* XXX This can economize rendering OPs.
*/
if (this._moveForward === true || this._moveBackward === true || this._moveLeft === true
|| this._moveRight === true || this._offsetPitch !== 0 || this._offsetRoll !== 0)
{
this.trigger('update');
}
// Reset values to avoid lost of control.
this._moveForward = this._moveBackward = this._moveLeft = this._moveRight = false;
this._offsetPitch = this._offsetRoll = 0;
},
// Private methods:
_checkGamepadCompatibility: function(event) {
/**
* If connected gamepad has a **standard** layout:
*
* @see https://w3c.github.io/gamepad/#remapping about standard.
*/
if (event.gamepad.mapping === 'standard') {
this._standardGamepadIndex = event.gamepad.index;
this._standardGamepadAvailable = true;
this.onStandardGamepadReady(event.gamepad);
}
},
_disconnectGamepad: function(event) {
this._standardGamepadAvailable = false;
this.onGamepadDisconnected(event.gamepad);
},
_getStandardGamepad: function() {
return navigator.getGamepads()[this._standardGamepadIndex];
},
_scanPressedGamepadButtons: function() {
var gamepadButtons = this._getStandardGamepad().buttons;
// For each gamepad button:
for (var gamepadButtonId = 0; gamepadButtonId < gamepadButtons.length; gamepadButtonId++) {
// Get user input.
var gamepadButton = gamepadButtons[gamepadButtonId];
if (gamepadButton.pressed) {
switch (gamepadButtonId) {
// D-pad Up
case 12:
this._moveForward = true;
break;
// D-pad Down
case 13:
this._moveBackward = true;
break;
// D-pad Left
case 14:
this._moveLeft = true;
break;
// D-pad Right
case 15:
this._moveRight = true;
break;
}
}
}
},
_scanInclinedGamepadAxes: function() {
var gamepadAxes = this._getStandardGamepad().axes;
// For each gamepad axis:
for (var gamepadAxisId = 0; gamepadAxisId < gamepadAxes.length; gamepadAxisId++) {
// Get user input.
var gamepadAxis = gamepadAxes[gamepadAxisId];
// XXX We use a threshold because axes are never neutral.
if (Math.abs(gamepadAxis) > this._gamepadAxisThreshold) {
switch (gamepadAxisId) {
// Left stick X±
case 0:
this._moveLeft = gamepadAxis < 0;
this._moveRight = gamepadAxis > 0;
break;
// Left stick Y±
case 1:
this._moveForward = gamepadAxis < 0;
this._moveBackward = gamepadAxis > 0;
break;
// Right stick X±
case 2:
this._offsetPitch += gamepadAxis * this.lookAroundSpeed;
break;
// Right stick Y±
case 3:
this._offsetRoll += gamepadAxis * this.lookAroundSpeed;
break;
}
}
}
}
});
export default GamepadControl;