src/gamepad.mjs
import { Controller } from './controller.mjs';
import { Logger } from './vendor/logger.min.mjs';
/**
* A gamepadmanager to help with games / handling input from a controller
* @class GamepadManagerSingleton
* @license GamepadManager does not have a license at this time. For licensing contact the author
* @author https://github.com/doubleactii
* @todo Currently bluetooth gamepads when disconnecting (PS4 only) do no fire a disconnected event. Manually calling `this.gamepad.vibrationActuator.reset()` can force it to call a disconnect event, but
* this is a messy way of checking each tick to see if the gamepad is still connected. It also will cancel ongoing vibrations. Find a fix. (This is a GamepadAPI issue/OS issue/not code wise issue)
*/
class GamepadManagerSingleton {
/**
* Object containing all connected controllers
*
* @type {Object}
*/
controllers = {};
/**
* Object containing the callback for when a controller is connected
*
* @type {Object}
*/
connectHandler = {};
/**
* Controllers connected before an event listener caught them.
* @type {Set<Controller> | null}
*/
connectedControllers = null;
/**
* Controllers connected before an event listener caught them.
* @type {Set<Controller> | null}
*/
unassignedControllers = null;
/**
* Object containing the callback for when a controller is disconnected
*
* @type {Object}
*/
disconnectHandler = {};
/**
* The version of the module.
*/
version = "VERSION_REPLACE_ME";
/**
* Creates the instance and assigns event handlers to gamepad events
*/
constructor() {
/** The logger module this module uses to log errors / logs.
* @private
* @type {Object}
*/
this.logger = new Logger();
this.logger.registerType('Gamepad-Module', '#ff6600');
this.connectedControllers = new Set();
this.unassignedControllers = new Set();
// Bind this class instance to the event handlers
this.handleGamepadConnected = this.handleGamepadConnected.bind(this);
this.handleGamepadDisconnected = this.handleGamepadDisconnected.bind(this);
this.pollGamepadState = this.pollGamepadState.bind(this);
// Check for gamepad support
if ('getGamepads' in navigator) {
window.addEventListener('gamepadconnected', this.handleGamepadConnected);
window.addEventListener('gamepaddisconnected', this.handleGamepadDisconnected);
requestAnimationFrame(this.pollGamepadState);
} else {
this.logger.prefix('Gamepad-Module').warn('Gamepad API not supported in this browser.');
}
}
/**
* Gets the angle between two points
*
* @param {Object} pStartPoint - The starting point
* @param {Object} pEndPoint - The ending point
* @returns {number} The angle between the starting point and the ending point
*/
static getAngle(pStartPoint, pEndPoint) {
const y = pStartPoint.y - pEndPoint.y;
const x = pStartPoint.x - pEndPoint.x;
return -Math.atan2(y, x) - Math.PI;
}
/**
* This gets the first controller connected. This controller is dominant
*
* @returns {Gamepad} The first controller connected
*/
getMainController() {
return this.controllers['0'];
}
/**
* @returns {Array} An array of all connected controllers
*/
getControllers() {
return { ...this.controllers };
}
/**
* Attaches a callback to the specified event.
*
* @param {Object} pEvent - The event to attach the callback to
* @param {Function} pCallback - The function to be called when the event is triggered
* @return {GamepadManagerSingleton} The GamepadManagerSingleton instance
*/
on(pEvent, pCallback) {
if (typeof(pEvent) === 'string') {
if (typeof(pCallback) === 'function') {
switch (pEvent) {
case 'connect':
this.connectHandler[pEvent] = pCallback;
this.unassignedControllers.forEach(pController => this.connectHandler[pEvent](pController));
this.unassignedControllers.clear();
break;
case 'disconnect':
this.disconnectHandler[pEvent] = pCallback;
break;
default:
this.logger.prefix('Gamepad-Module').error(`The event "${pEvent}" is not supported.`);
}
} else {
this.logger.prefix('Gamepad-Module').error(`The callback for event "${pEvent}" is not a function.`);
}
}
return this;
}
/**
* Listener function for when a gamepad is connected
*
* @param {pGamepadEvent} pGamepadEvent - A gamepad event
*/
handleGamepadConnected(pGamepadEvent) {
// Create a controller from the gamepad that was connected
// This controller only saves a snapshot of the data of when it was first created, but we update it based on new polled data
const controller = new Controller(pGamepadEvent.gamepad);
this.controllers[controller.index] = controller;
this.connectedControllers.add(controller);
if (typeof(this.connectHandler.connect) === 'function') {
this.connectHandler.connect(controller);
} else {
this.unassignedControllers.add(controller);
}
}
/**
* Listener function for when the gamepad is disconnected
*
* @param {pGamepadEvent} pGamepadEvent - A gamepad event
*/
handleGamepadDisconnected(pGamepadEvent) {
// Delete the controller when it's disconnected
// Maybe add a option to save gamepad info for a short while, incase it disconnected due to battery?
// When reconnected it can prompt an alert that says "restore configuration for gamepad". This will restore that configuration to the controller.
const index = pGamepadEvent.gamepad.index;
const controller = this.controllers[index];
if (typeof(this.disconnectHandler.disconnect) === 'function') this.disconnectHandler.disconnect(controller);
this.connectedControllers.delete(controller);
delete this.controllers[index];
}
/**
* Get the latest game state of the connected gamepads (Chrome only saves snapshots of the state, we have to keep polling to get updated states)
*/
pollGamepadState() {
const gamepads = navigator.getGamepads();
if (!gamepads) return;
// Loop through all connected controllers and update their state
for (const gamepad of gamepads) {
// Can be null if disconnected during the session
if (gamepad) {
for (const controller in this.controllers) {
// Make sure we are updating the correct controller with the right data from the gamepad at the same index
if (gamepad.index === this.controllers[controller].gamepad.index) {
this.controllers[controller].updateState(gamepad);
}
}
}
}
requestAnimationFrame(this.pollGamepadState);
}
}
export const GamepadManager = new GamepadManagerSingleton();