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;