aframe-extras
Version:
Add-ons and examples for A-Frame VR.
344 lines (281 loc) • 10.4 kB
JavaScript
/**
* Gamepad controls for A-Frame.
*
* Stripped-down version of: https://github.com/donmccurdy/aframe-gamepad-controls
*
* For more information about the Gamepad API, see:
* https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API/Using_the_Gamepad_API
*/
import GamepadButton from '../../lib/GamepadButton.js';
import GamepadButtonEvent from '../../lib/GamepadButtonEvent.js';
const JOYSTICK_EPS = 0.2;
const Hand = {
LEFT: 'left',
RIGHT: 'right'
};
const Joystick = {
MOVEMENT: 1,
ROTATION: 2
};
AFRAME.registerComponent('gamepad-controls', {
/*******************************************************************
* Statics
*/
GamepadButton: GamepadButton,
/*******************************************************************
* Schema
*/
schema: {
// Enable/disable gamepad-controls
enabled: { default: true },
// Rotation sensitivity
rotationSensitivity: { default: 2.0 },
},
/*******************************************************************
* Core
*/
/**
* Called once when component is attached. Generally for initial setup.
*/
init: function () {
const sceneEl = this.el.sceneEl;
// tracked-controls-webxr was renamed to tracked-controls in aframe 1.7.0
// tracked-controls-webxr is for aframe 1.6.0 and below
this.system = sceneEl.systems['tracked-controls'] || sceneEl.systems['tracked-controls-webxr'] || {controllers: []};
this.prevTime = window.performance.now();
// Button state
this.buttons = {};
// Rotation
const rotation = this.el.object3D.rotation;
this.pitch = new THREE.Object3D();
this.pitch.rotation.x = rotation.x;
this.yaw = new THREE.Object3D();
this.yaw.position.y = 10;
this.yaw.rotation.y = rotation.y;
this.yaw.add(this.pitch);
this._lookVector = new THREE.Vector2();
this._moveVector = new THREE.Vector2();
this._dpadVector = new THREE.Vector2();
sceneEl.addBehavior(this);
},
/**
* Called when component is attached and when component data changes.
* Generally modifies the entity based on the data.
*/
update: function () { this.tick(); },
/**
* Called on each iteration of main render loop.
*/
tick: function (t, dt) {
this.updateButtonState();
this.updateRotation(dt);
},
/**
* Called when a component is removed (e.g., via removeAttribute).
* Generally undoes all modifications to the entity.
*/
remove: function () { },
/*******************************************************************
* Movement
*/
isVelocityActive: function () {
if (!this.data.enabled || !this.isConnected()) return false;
const dpad = this._dpadVector;
const joystick = this._moveVector;
this.getDpad(dpad);
this.getJoystick(Joystick.MOVEMENT, joystick);
const inputX = dpad.x || joystick.x;
const inputY = dpad.y || joystick.y;
return Math.abs(inputX) > JOYSTICK_EPS || Math.abs(inputY) > JOYSTICK_EPS;
},
getVelocityDelta: function () {
const dpad = this._dpadVector;
const joystick = this._moveVector;
this.getDpad(dpad);
this.getJoystick(Joystick.MOVEMENT, joystick);
const inputX = dpad.x || joystick.x;
const inputY = dpad.y || joystick.y;
const dVelocity = new THREE.Vector3();
if (Math.abs(inputX) > JOYSTICK_EPS) {
dVelocity.x += inputX;
}
if (Math.abs(inputY) > JOYSTICK_EPS) {
dVelocity.z += inputY;
}
return dVelocity;
},
/*******************************************************************
* Rotation
*/
isRotationActive: function () {
if (!this.data.enabled || !this.isConnected()) return false;
const joystick = this._lookVector;
this.getJoystick(Joystick.ROTATION, joystick);
return Math.abs(joystick.x) > JOYSTICK_EPS || Math.abs(joystick.y) > JOYSTICK_EPS;
},
updateRotation: function (dt) {
if (!this.isRotationActive()) return;
const data = this.data;
const yaw = this.yaw;
const pitch = this.pitch;
// First copy camera rig pitch/yaw, it may have been changed from
// another component.
yaw.rotation.y = this.el.object3D.rotation.y;
pitch.rotation.x = this.el.object3D.rotation.x;
const lookVector = this._lookVector;
this.getJoystick(Joystick.ROTATION, lookVector);
if (Math.abs(lookVector.x) <= JOYSTICK_EPS) lookVector.x = 0;
if (Math.abs(lookVector.y) <= JOYSTICK_EPS) lookVector.y = 0;
lookVector.multiplyScalar(data.rotationSensitivity * dt / 1000);
yaw.rotation.y -= lookVector.x;
pitch.rotation.x -= lookVector.y;
pitch.rotation.x = Math.max(- Math.PI / 2, Math.min(Math.PI / 2, pitch.rotation.x));
this.el.object3D.rotation.set(pitch.rotation.x, yaw.rotation.y, 0);
},
/*******************************************************************
* Button events
*/
updateButtonState: function () {
const gamepad = this.getGamepad(Hand.RIGHT);
if (this.data.enabled && gamepad) {
// Fire DOM events for button state changes.
for (var i = 0; i < gamepad.buttons.length; i++) {
if (gamepad.buttons[i].pressed && !this.buttons[i]) {
this.emit(new GamepadButtonEvent('gamepadbuttondown', i, gamepad.buttons[i]));
} else if (!gamepad.buttons[i].pressed && this.buttons[i]) {
this.emit(new GamepadButtonEvent('gamepadbuttonup', i, gamepad.buttons[i]));
}
this.buttons[i] = gamepad.buttons[i].pressed;
}
} else {
// Reset state if controls are disabled or controller is lost.
for (const key in this.buttons) { this.buttons[key] = false; }
}
},
emit: function (event) {
// Emit original event.
this.el.emit(event.type, event);
// Emit convenience event, identifying button index.
this.el.emit(
event.type + ':' + event.index,
new GamepadButtonEvent(event.type, event.index, event)
);
},
/*******************************************************************
* Gamepad state
*/
/**
* Returns the Gamepad instance attached to the component. If connected,
* a proxy-controls component may provide access to Gamepad input from a
* remote device.
*
* @param {string} handPreference
* @return {Gamepad}
*/
getGamepad: (function () {
const _xrGamepads = [];
const _empty = [];
return function (handPreference) {
// https://github.com/donmccurdy/aframe-proxy-controls
const proxyControls = this.el.sceneEl.components['proxy-controls'];
const proxyGamepad = proxyControls && proxyControls.isConnected()
&& proxyControls.getGamepad(0);
if (proxyGamepad) return proxyGamepad;
// https://www.w3.org/TR/webxr/#dom-xrinputsource-handedness
_xrGamepads.length = 0;
for (let i = 0; i < this.system.controllers.length; i++) {
const xrController = this.system.controllers[i];
const xrGamepad = xrController ? xrController.gamepad : null;
_xrGamepads.push(xrGamepad);
if (xrGamepad && xrController.handedness === handPreference) return xrGamepad;
}
// https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/hand
const navGamepads = navigator.getGamepads ? navigator.getGamepads() : _empty;
for (let i = 0; i < navGamepads.length; i++) {
const navGamepad = navGamepads[i];
if (navGamepad && navGamepad.hand === handPreference) return navGamepad;
}
return _xrGamepads[0] || navGamepads[0];
};
})(),
/**
* Returns the state of the given button.
* @param {number} index The button (0-N) for which to find state.
* @return {GamepadButton}
*/
getButton: function (index) {
return this.getGamepad(Hand.RIGHT).buttons[index];
},
/**
* Returns state of the given axis. Axes are labelled 0-N, where 0-1 will
* represent X/Y on the first joystick, and 2-3 X/Y on the second.
* @param {number} index The axis (0-N) for which to find state.
* @return {number} On the interval [-1,1].
*/
getAxis: function (index) {
return this.getGamepad(index > 1 ? Hand.RIGHT : Hand.LEFT).axes[index];
},
/**
* Returns the state of the specified joystick as a THREE.Vector2.
* @param {Joystick} role
* @param {THREE.Vector2} target
* @return {THREE.Vector2}
*/
getJoystick: function (index, target) {
const gamepad = this.getGamepad(index === Joystick.MOVEMENT ? Hand.LEFT : Hand.RIGHT);
// gamepad can be null here if it becomes disconnected even if isConnected() was called
// in the same tick before calling getJoystick.
if (!gamepad) {
return target.set(0, 0);
}
if (gamepad.mapping === 'xr-standard') {
// See: https://github.com/donmccurdy/aframe-extras/issues/307
switch (index) {
case Joystick.MOVEMENT: return target.set(gamepad.axes[2], gamepad.axes[3]);
case Joystick.ROTATION: return target.set(gamepad.axes[2], 0);
}
} else {
switch (index) {
case Joystick.MOVEMENT: return target.set(gamepad.axes[0], gamepad.axes[1]);
case Joystick.ROTATION: return target.set(gamepad.axes[2], gamepad.axes[3]);
}
}
throw new Error('Unexpected joystick index "%d".', index);
},
/**
* Returns the state of the dpad as a THREE.Vector2.
* @param {THREE.Vector2} target
* @return {THREE.Vector2}
*/
getDpad: function (target) {
const gamepad = this.getGamepad(Hand.LEFT);
if (!gamepad) {
return target.set(0, 0);
}
if (!gamepad.buttons[GamepadButton.DPAD_RIGHT]) {
return target.set(0, 0);
}
return target.set(
(gamepad.buttons[GamepadButton.DPAD_RIGHT].pressed ? 1 : 0)
+ (gamepad.buttons[GamepadButton.DPAD_LEFT].pressed ? -1 : 0),
(gamepad.buttons[GamepadButton.DPAD_UP].pressed ? -1 : 0)
+ (gamepad.buttons[GamepadButton.DPAD_DOWN].pressed ? 1 : 0)
);
},
/**
* Returns true if the gamepad is currently connected to the system.
* @return {boolean}
*/
isConnected: function () {
const gamepad = this.getGamepad(Hand.LEFT);
return !!(gamepad && gamepad.connected);
},
/**
* Returns a string containing some information about the controller. Result
* may vary across browsers, for a given controller.
* @return {string}
*/
getID: function () {
return this.getGamepad(Hand.LEFT).id;
}
});