import { VRSPACEUI } from '../ui/vrspace-ui.js';
import { VRSPACE } from '../client/vrspace.js';
/**
* Gamepad helper class used by HUD and VRHelper, NOT used by 3rd person camera.
* Implementes UI control with gamepad: selecting and activating HUD buttons in and out of XR,
* and XR teleportation and interaction with scene. As gamepad is not standard XR controller,
* and mobiles don't come with XR controller, this is the only option to interact with scene in mobile VR.
* Traverse through hud menus with gamepad buttons, left/right/up/down, either left or right hand.
* Activate current option with either trigger or up.
* To teleport and rotate in (mobile) XR, use thumbstick, and to interact, use trigger.
*/
export class GamepadHelper {
/**
* @type {GamepadHelper}
*/
static instance = null;
static getInstance(scene) {
if (!GamepadHelper.instance) {
GamepadHelper.instance = new GamepadHelper(scene);
}
return GamepadHelper.instance;
}
constructor(scene) {
this.scene = scene;
this.gamepadState = {};
this.connectListeners = [];
this.axisListeners = [];
this.triggerListeners = [];
this.hudLeft = [2,14];
this.hudRight = [1,15];
this.hudUp = [3,12];
this.hudDown = [0,13];
this.trackGamepad();
}
/**
* Main point of gamepad support, called from the constructor.
* Once the browser emits gamepadconnected event,
* installs tracker function into main rendering loop, to track states that
* rotate the camera, teleport, and fire gamepad button events.
*/
trackGamepad() {
// https://forum.babylonjs.com/t/no-gamepad-support-in-webxrcontroller/15147/2
let gamepadTracker = () => {
const gamepad = navigator.getGamepads()[this.gamepadState.index];
for (let i = 0; i < gamepad.buttons.length; i++) {
let buttonState = gamepad.buttons[i].value > 0 || gamepad.buttons[i].pressed || gamepad.buttons[i].touched;
if (this.gamepadState.buttons[i] != buttonState) {
this.gamepadState.buttons[i] = buttonState;
this.gamepadButton(i, buttonState);
}
}
let treshold = 0.5;
for (let i = 0; i < gamepad.axes.length; i++) {
if (this.gamepadState.axes[i] != gamepad.axes[i]) {
let val = gamepad.axes[i];
this.gamepadState.axes[i] = val;
//console.log(i+" "+this.gamepadState.axes[i]);
if (i == 0 || i == 2) {
// left-right
if (val < -treshold) {
if (!this.gamepadState.left) {
this.gamepadState.left = true;
this.notifyListeners(this.axisListeners, 'left');
}
} else if (val > treshold) {
if (!this.gamepadState.right) {
this.gamepadState.right = true;
this.notifyListeners(this.axisListeners, 'right');
}
} else {
this.notifyListeners(this.axisListeners, 'none');
this.gamepadState.left = false;
this.gamepadState.right = false;
}
}
if (i == 1 || i == 3) {
// forward-back
if (val < -treshold) {
if (!this.gamepadState.forward) {
this.gamepadState.forward = true;
this.notifyListeners(this.axisListeners, 'forward');
}
} else if (val > treshold) {
if (!this.gamepadState.back) {
this.gamepadState.back = true;
this.notifyListeners(this.axisListeners, 'back');
}
} else {
this.gamepadState.forward = false;
this.gamepadState.back = false;
this.notifyListeners(this.axisListeners, 'none');
}
}
}
}
}
window.addEventListener("gamepaddisconnected", (e) => {
console.log("Gamepad disconnected ", e.gamepad.id);
this.scene.unregisterBeforeRender(gamepadTracker);
this.notifyListeners(this.connectListeners, false);
this.gamepad = null;
});
window.addEventListener("gamepadconnected", (e) => {
console.log("Gamepad " + e.gamepad.index + " connected " + e.gamepad.id);
this.gamepad = e.gamepad;
this.gamepadState = {
index: e.gamepad.index,
id: e.gamepad.id,
buttons: [],
axes: [],
forward: false,
back: false,
left: false,
right: false
}
e.gamepad.buttons.forEach(b => {
let state = b.value > 0 || b.pressed || b.touched;
//console.log('button state: '+state);
this.gamepadState.buttons.push(state);
});
e.gamepad.axes.forEach(a => {
//console.log('axis state: '+a);
this.gamepadState.axes.push(a);
});
this.scene.registerBeforeRender(gamepadTracker);
console.log("gamepad state initialized");
this.notifyListeners(this.connectListeners, true);
});
}
/**
* Gamepad button event handler. Buttons left/right/up/down are forwarded to the HUD.
* Trigger button and select button events are forwarded either to HUD, or to the scene, as appropriate.
* @param index button index, see https://github.com/alvaromontoro/gamecontroller.js/blob/master/public/gamepad.svg
* @param state true/false for pressed/released
*/
gamepadButton(index, state) {
// triggers: left 4, 6, right 5, 7
// select 8, start 9
// left right down up: right 2 1 0 3 (X B A Y) left 14 15 13 12
// stick: left 10 right 11
//console.log(index+" "+state);
try {
if (index == 8 || index == 6 || index == 7 || index == 4 || index == 5) {
console.log('activate ' + index + ' scene: ' + (this.pickInfo != null));
// select, triggers
if (this.forwardToHud(index)) {
// hud event takes precedence
if (state) {
// only process button down
VRSPACEUI.hud.activate();
}
} else {
this.notifyListeners(this.triggerListeners,state);
}
} else if (state && VRSPACEUI.hud) {
if (this.hudLeft.includes(index)) {
VRSPACEUI.hud.left();
} else if (this.hudRight.includes(index)) {
VRSPACEUI.hud.right();
} else if (this.hudDown.includes(index)) {
VRSPACEUI.hud.down();
} else if (this.hudUp.includes(index)) {
VRSPACEUI.hud.up();
}
}
} catch (error) {
console.error("Error:", error.stack);
}
}
/** Returns true if HUD can process gamepad event, i.e. a button or form is currently active.*/
forwardToHud(index) {
return VRSPACEUI.hud &&
VRSPACEUI.hud.activeControl &&
(VRSPACEUI.hud.activeControl.getClassName() == "HolographicButton"||VRSPACEUI.hud.activeControl.getClassName() == "Form") &&
(index == 8 || index == 9);
}
notifyListeners(listeners, event) {
listeners.forEach(listener => listener(event));
}
addAxisListener(callback) {
VRSPACE.addListener(this.axisListeners, callback);
}
removeAxisListener(callback) {
VRSPACE.removeListener(this.axisListeners, callback);
}
addConnectListener(callback) {
VRSPACE.addListener(this.connectListeners, callback);
}
removeConnectListener(callback) {
VRSPACE.removeListener(this.connectListeners, callback);
}
addTriggerListener(callback) {
VRSPACE.addListener(this.triggerListeners, callback);
}
removeTriggerListener(callback) {
VRSPACE.removeListener(this.triggerListeners, callback);
}
}