UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

527 lines (526 loc) 13.4 kB
import { EventHandler } from "../../core/event-handler.js"; import { PAD_FACE_1, PAD_FACE_2, PAD_FACE_3, PAD_FACE_4, PAD_L_SHOULDER_1, PAD_R_SHOULDER_1, PAD_L_SHOULDER_2, PAD_R_SHOULDER_2, PAD_SELECT, PAD_START, PAD_L_STICK_BUTTON, PAD_R_STICK_BUTTON, PAD_UP, PAD_DOWN, PAD_LEFT, PAD_RIGHT, PAD_VENDOR, XRPAD_TRIGGER, XRPAD_SQUEEZE, XRPAD_TOUCHPAD_BUTTON, XRPAD_STICK_BUTTON, XRPAD_A, XRPAD_B, PAD_L_STICK_X, PAD_L_STICK_Y, PAD_R_STICK_X, PAD_R_STICK_Y, XRPAD_TOUCHPAD_X, XRPAD_TOUCHPAD_Y, XRPAD_STICK_X, XRPAD_STICK_Y } from "./constants.js"; import { math } from "../../core/math/math.js"; import { platform } from "../../core/platform.js"; const dummyArray = Object.freeze([]); let getGamepads = function() { return dummyArray; }; if (typeof navigator !== "undefined") { getGamepads = (navigator.getGamepads || navigator.webkitGetGamepads || getGamepads).bind(navigator); } const MAPS_INDEXES = { buttons: { PAD_FACE_1, PAD_FACE_2, PAD_FACE_3, PAD_FACE_4, PAD_L_SHOULDER_1, PAD_R_SHOULDER_1, PAD_L_SHOULDER_2, PAD_R_SHOULDER_2, PAD_SELECT, PAD_START, PAD_L_STICK_BUTTON, PAD_R_STICK_BUTTON, PAD_UP, PAD_DOWN, PAD_LEFT, PAD_RIGHT, PAD_VENDOR, XRPAD_TRIGGER, XRPAD_SQUEEZE, XRPAD_TOUCHPAD_BUTTON, XRPAD_STICK_BUTTON, XRPAD_A, XRPAD_B }, axes: { PAD_L_STICK_X, PAD_L_STICK_Y, PAD_R_STICK_X, PAD_R_STICK_Y, XRPAD_TOUCHPAD_X, XRPAD_TOUCHPAD_Y, XRPAD_STICK_X, XRPAD_STICK_Y } }; const MAPS = { DEFAULT: { buttons: [ // Face buttons "PAD_FACE_1", "PAD_FACE_2", "PAD_FACE_3", "PAD_FACE_4", // Shoulder buttons "PAD_L_SHOULDER_1", "PAD_R_SHOULDER_1", "PAD_L_SHOULDER_2", "PAD_R_SHOULDER_2", // Other buttons "PAD_SELECT", "PAD_START", "PAD_L_STICK_BUTTON", "PAD_R_STICK_BUTTON", // D Pad "PAD_UP", "PAD_DOWN", "PAD_LEFT", "PAD_RIGHT", // Vendor specific button "PAD_VENDOR" ], axes: [ // Analog Sticks "PAD_L_STICK_X", "PAD_L_STICK_Y", "PAD_R_STICK_X", "PAD_R_STICK_Y" ] }, DEFAULT_DUAL: { buttons: [ // Face buttons "PAD_FACE_1", "PAD_FACE_2", "PAD_FACE_3", "PAD_FACE_4", // Shoulder buttons "PAD_L_SHOULDER_1", "PAD_R_SHOULDER_1", "PAD_L_SHOULDER_2", "PAD_R_SHOULDER_2", // Other buttons "PAD_SELECT", "PAD_START", "PAD_L_STICK_BUTTON", "PAD_R_STICK_BUTTON", // Vendor specific button "PAD_VENDOR" ], axes: [ // Analog Sticks "PAD_L_STICK_X", "PAD_L_STICK_Y", "PAD_R_STICK_X", "PAD_R_STICK_Y" ], synthesizedButtons: { PAD_UP: { axis: 0, min: 0, max: 1 }, PAD_DOWN: { axis: 0, min: -1, max: 0 }, PAD_LEFT: { axis: 0, min: -1, max: 0 }, PAD_RIGHT: { axis: 0, min: 0, max: 1 } } }, PS3: { buttons: [ // X, O, TRI, SQ "PAD_FACE_1", "PAD_FACE_2", "PAD_FACE_4", "PAD_FACE_3", // Shoulder buttons "PAD_L_SHOULDER_1", "PAD_R_SHOULDER_1", "PAD_L_SHOULDER_2", "PAD_R_SHOULDER_2", // Other buttons "PAD_SELECT", "PAD_START", "PAD_L_STICK_BUTTON", "PAD_R_STICK_BUTTON", // D Pad "PAD_UP", "PAD_DOWN", "PAD_LEFT", "PAD_RIGHT", "PAD_VENDOR" ], axes: [ // Analog Sticks "PAD_L_STICK_X", "PAD_L_STICK_Y", "PAD_R_STICK_X", "PAD_R_STICK_Y" ], mapping: "standard" }, DEFAULT_XR: { buttons: [ // Back buttons "XRPAD_TRIGGER", "XRPAD_SQUEEZE", // Axes buttons "XRPAD_TOUCHPAD_BUTTON", "XRPAD_STICK_BUTTON", // Face buttons "XRPAD_A", "XRPAD_B" ], axes: [ // Analog Sticks "XRPAD_TOUCHPAD_X", "XRPAD_TOUCHPAD_Y", "XRPAD_STICK_X", "XRPAD_STICK_Y" ], mapping: "xr-standard" } }; const PRODUCT_CODES = { "Product: 0268": "PS3" }; const custom_maps = {}; function getMap(pad) { const custom = custom_maps[pad.id]; if (custom) { return custom; } for (const code in PRODUCT_CODES) { if (pad.id.indexOf(code) !== -1) { const product = PRODUCT_CODES[code]; if (!pad.mapping) { const raw = MAPS[`RAW_${product}`]; if (raw) { return raw; } } return MAPS[product]; } } if (pad.mapping === "xr-standard") { return MAPS.DEFAULT_XR; } const defaultmap = MAPS.DEFAULT; const map = pad.buttons.length < defaultmap.buttons.length ? MAPS.DEFAULT_DUAL : defaultmap; map.mapping = pad.mapping; return map; } let deadZone = 0.25; function sleep(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } class GamePadButton { value = 0; pressed = false; touched = false; wasPressed = false; wasReleased = false; wasTouched = false; constructor(current, previous) { if (typeof current === "number") { this.value = current; this.pressed = current === 1; this.touched = current > 0; } else { this.value = current.value; this.pressed = current.pressed; this.touched = current.touched ?? current.value > 0; } if (previous) { if (typeof previous === "number") { this.wasPressed = previous !== 1 && this.pressed; this.wasReleased = previous === 1 && !this.pressed; this.wasTouched = previous === 0 && this.touched; } else { this.wasPressed = !previous.pressed && this.pressed; this.wasReleased = previous.pressed && !this.pressed; this.wasTouched = !(previous.touched ?? previous.value > 0) && this.touched; } } } update(button) { const { value, pressed } = button; const touched = button.touched ?? value > 0; this.wasPressed = !this.pressed && pressed; this.wasReleased = this.pressed && !pressed; this.wasTouched = !this.touched && touched; this.value = value; this.pressed = pressed; this.touched = touched; } } const dummyButton = Object.freeze(new GamePadButton(0)); class GamePad { _compiledMapping = { buttons: [], axes: [] }; id; index; _buttons; _axes; _previousAxes; mapping; map; hand; pad; constructor(gamepad, map) { this.id = gamepad.id; this.index = gamepad.index; this._buttons = gamepad.buttons.map((b) => new GamePadButton(b)); this._axes = [...gamepad.axes]; this._previousAxes = [...gamepad.axes]; this.mapping = map.mapping; this.map = map; this.hand = gamepad.hand || "none"; this.pad = gamepad; this._compileMapping(); } get connected() { return this.pad.connected; } _compileMapping() { const { axes, buttons } = this._compiledMapping; const axesIndexes = MAPS_INDEXES.axes; const buttonsIndexes = MAPS_INDEXES.buttons; axes.length = 0; buttons.length = 0; const axesMap = this.map.axes; if (axesMap) { this.map.axes.forEach((axis, i) => { axes[axesIndexes[axis]] = () => this.pad.axes[i] || 0; }); } for (let i = 0, l = axes.length; i < l; i++) { if (!axes[i]) { axes[i] = () => 0; } } const buttonsMap = this.map.buttons; if (buttonsMap) { buttonsMap.forEach((button, i) => { buttons[buttonsIndexes[button]] = () => this._buttons[i] || dummyButton; }); } const synthesizedButtonsMap = this.map.synthesizedButtons; if (synthesizedButtonsMap) { Object.entries(synthesizedButtonsMap).forEach((button) => { const { axis, max, min } = button[1]; buttons[buttonsIndexes[button[0]]] = () => new GamePadButton( Math.abs(math.clamp(this._axes[axis] ?? 0, min, max)), Math.abs(math.clamp(this._previousAxes[axis] ?? 0, min, max)) ); }); } for (let i = 0, l = buttons.length; i < l; i++) { if (!buttons[i]) { buttons[i] = () => dummyButton; } } } update(gamepad) { this.pad = gamepad; const previousAxes = this._previousAxes; const axes = this._axes; previousAxes.length = 0; previousAxes.push(...axes); axes.length = 0; axes.push(...gamepad.axes); const buttons = this._buttons; for (let i = 0, l = buttons.length; i < l; i++) { buttons[i].update(gamepad.buttons[i]); } return this; } updateMap(map) { map.mapping = "custom"; custom_maps[this.id] = map; this.map = map; this.mapping = "custom"; this._compileMapping(); } resetMap() { if (custom_maps[this.id]) { delete custom_maps[this.id]; const map = getMap(this.pad); this.map = map; this.mapping = map.mapping; this._compileMapping(); } } get axes() { return this._compiledMapping.axes.map((a) => a()); } get buttons() { return this._compiledMapping.buttons.map((b) => b()); } async pulse(intensity, duration, options) { const actuators = this.pad.vibrationActuator ? [this.pad.vibrationActuator] : this.pad.hapticActuators || dummyArray; if (actuators.length) { const startDelay = options?.startDelay ?? 0; const strongMagnitude = options?.strongMagnitude ?? intensity; const weakMagnitude = options?.weakMagnitude ?? intensity; const results = await Promise.all( actuators.map(async (actuator) => { if (!actuator) { return true; } if (actuator.playEffect) { return actuator.playEffect(actuator.type, { duration, startDelay, strongMagnitude, weakMagnitude }); } else if (actuator.pulse) { await sleep(startDelay); return actuator.pulse(intensity, duration); } return false; }) ); return results.some((r) => r === true || r === "complete"); } return false; } getButton(index) { const button = this._compiledMapping.buttons[index]; return button ? button() : dummyButton; } isPressed(button) { return this.getButton(button).pressed; } wasPressed(button) { return this.getButton(button).wasPressed; } wasReleased(button) { return this.getButton(button).wasReleased; } isTouched(button) { return this.getButton(button).touched; } wasTouched(button) { return this.getButton(button).wasTouched; } getValue(button) { return this.getButton(button).value; } getAxis(axis) { const a = this.axes[axis]; return a && Math.abs(a) > deadZone ? a : 0; } } class GamePads extends EventHandler { static EVENT_GAMEPADCONNECTED = "gamepadconnected"; static EVENT_GAMEPADDISCONNECTED = "gamepaddisconnected"; gamepadsSupported; current = []; _previous = []; _ongamepadconnectedHandler; _ongamepaddisconnectedHandler; constructor() { super(); this.gamepadsSupported = platform.gamepads; this._ongamepadconnectedHandler = this._ongamepadconnected.bind(this); this._ongamepaddisconnectedHandler = this._ongamepaddisconnected.bind(this); window.addEventListener("gamepadconnected", this._ongamepadconnectedHandler, false); window.addEventListener("gamepaddisconnected", this._ongamepaddisconnectedHandler, false); this.poll(); } set deadZone(value) { deadZone = value; } get deadZone() { return deadZone; } get previous() { const current = this.current; for (let i = 0, l = current.length; i < l; i++) { const buttons = current[i]._buttons; if (!this._previous[i]) { this._previous[i] = []; } for (let j = 0, m = buttons.length; j < m; j++) { const button = buttons[i]; this.previous[i][j] = button ? !button.wasPressed && button.pressed || button.wasReleased : false; } } this._previous.length = this.current.length; return this._previous; } _ongamepadconnected(event) { const pad = new GamePad(event.gamepad, this.getMap(event.gamepad)); const current = this.current; let padIndex = current.findIndex((gp) => gp.index === pad.index); while (padIndex !== -1) { current.splice(padIndex, 1); padIndex = current.findIndex((gp) => gp.index === pad.index); } current.push(pad); this.fire("gamepadconnected", pad); } _ongamepaddisconnected(event) { const current = this.current; const padIndex = current.findIndex((gp) => gp.index === event.gamepad.index); if (padIndex !== -1) { this.fire("gamepaddisconnected", current[padIndex]); current.splice(padIndex, 1); } } update() { this.poll(); } poll(pads = []) { if (pads.length > 0) { pads.length = 0; } const padDevices = getGamepads(); for (let i = 0, len = padDevices.length; i < len; i++) { if (padDevices[i]) { const pad = this.findByIndex(padDevices[i].index); if (pad) { pads.push(pad.update(padDevices[i])); } else { const nPad = new GamePad(padDevices[i], this.getMap(padDevices[i])); this.current.push(nPad); pads.push(nPad); } } } return pads; } destroy() { window.removeEventListener("gamepadconnected", this._ongamepadconnectedHandler, false); window.removeEventListener("gamepaddisconnected", this._ongamepaddisconnectedHandler, false); } getMap(pad) { return getMap(pad); } isPressed(orderIndex, button) { return this.current[orderIndex]?.isPressed(button) || false; } wasPressed(orderIndex, button) { return this.current[orderIndex]?.wasPressed(button) || false; } wasReleased(orderIndex, button) { return this.current[orderIndex]?.wasReleased(button) || false; } getAxis(orderIndex, axis) { return this.current[orderIndex]?.getAxis(axis) || 0; } pulse(orderIndex, intensity, duration, options) { const pad = this.current[orderIndex]; return pad ? pad.pulse(intensity, duration, options) : Promise.resolve(false); } pulseAll(intensity, duration, options) { return Promise.all( this.current.map((pad) => pad.pulse(intensity, duration, options)) ); } findById(id) { return this.current.find((gp) => gp && gp.id === id) || null; } findByIndex(index) { return this.current.find((gp) => gp && gp.index === index) || null; } } export { GamePad, GamePadButton, GamePads };