@epicgames-ps/lib-pixelstreamingfrontend-ue5.4
Version:
Frontend library for Unreal Engine 5.4 Pixel Streaming
315 lines (288 loc) • 12.2 kB
text/typescript
// Copyright Epic Games, Inc. All Rights Reserved.
import { Logger } from '../Logger/Logger';
import { StreamMessageController } from '../UeInstanceMessage/StreamMessageController';
import { EventListenerTracker } from '../Util/EventListenerTracker';
import { Controller } from './GamepadTypes';
/**
* The class that handles the functionality of gamepads and controllers
*/
export class GamePadController {
controllers: Array<Controller>;
requestAnimationFrame: (callback: FrameRequestCallback) => number;
toStreamerMessagesProvider: StreamMessageController;
// Utility for keeping track of event handlers and unregistering them
private gamePadEventListenerTracker = new EventListenerTracker();
/**
* @param toStreamerMessagesProvider - Stream message instance
*/
constructor(toStreamerMessagesProvider: StreamMessageController) {
this.toStreamerMessagesProvider = toStreamerMessagesProvider;
this.requestAnimationFrame = (
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.requestAnimationFrame
).bind(window);
const browserWindow = window as Window;
const onBeforeUnload = (ev: Event) =>
this.onBeforeUnload(ev);
window.addEventListener('beforeunload', onBeforeUnload);
if ('GamepadEvent' in browserWindow) {
const onGamePadConnected = (ev: GamepadEvent) =>
this.gamePadConnectHandler(ev);
const onGamePadDisconnected = (ev: GamepadEvent) =>
this.gamePadDisconnectHandler(ev);
window.addEventListener('gamepadconnected', onGamePadConnected);
window.addEventListener('gamepaddisconnected', onGamePadDisconnected);
this.gamePadEventListenerTracker.addUnregisterCallback(
() => window.removeEventListener('gamepadconnected', onGamePadConnected)
);
this.gamePadEventListenerTracker.addUnregisterCallback(
() => window.removeEventListener('gamepaddisconnected', onGamePadDisconnected)
);
} else if ('WebKitGamepadEvent' in browserWindow) {
const onWebkitGamePadConnected = (ev: GamepadEvent) => this.gamePadConnectHandler(ev);
const onWebkitGamePadDisconnected = (ev: GamepadEvent) => this.gamePadDisconnectHandler(ev);
window.addEventListener('webkitgamepadconnected', onWebkitGamePadConnected);
window.addEventListener('webkitgamepaddisconnected', onWebkitGamePadDisconnected);
this.gamePadEventListenerTracker.addUnregisterCallback(
() => window.removeEventListener('webkitgamepadconnected', onWebkitGamePadConnected)
);
this.gamePadEventListenerTracker.addUnregisterCallback(
() => window.removeEventListener('webkitgamepaddisconnected', onWebkitGamePadDisconnected)
);
}
this.controllers = [];
if (navigator.getGamepads) {
for (const gamepad of navigator.getGamepads()) {
if (gamepad) {
this.gamePadConnectHandler(new GamepadEvent('gamepadconnected', { gamepad }));
}
}
}
}
/**
* Unregisters all event handlers
*/
unregisterGamePadEvents() {
this.gamePadEventListenerTracker.unregisterAll();
for(const controller of this.controllers) {
if(controller.id !== undefined) {
this.onGamepadDisconnected(controller.id);
}
}
this.controllers = [];
this.onGamepadConnected = () => { /* */ };
this.onGamepadDisconnected = () => { /* */ };
}
/**
* Connects the gamepad handler
* @param gamePadEvent - the activating gamepad event
*/
gamePadConnectHandler(gamePadEvent: GamepadEvent) {
Logger.Log(Logger.GetStackTrace(), 'Gamepad connect handler', 6);
const gamepad = gamePadEvent.gamepad;
const temp: Controller = {
currentState: gamepad,
prevState: gamepad,
id: undefined
};
this.controllers.push(temp);
this.controllers[gamepad.index].currentState = gamepad;
this.controllers[gamepad.index].prevState = gamepad;
Logger.Log(
Logger.GetStackTrace(),
'gamepad: ' + gamepad.id + ' connected',
6
);
window.requestAnimationFrame(() => this.updateStatus());
this.onGamepadConnected();
}
/**
* Disconnects the gamepad handler
* @param gamePadEvent - the activating gamepad event
*/
gamePadDisconnectHandler(gamePadEvent: GamepadEvent) {
Logger.Log(Logger.GetStackTrace(), 'Gamepad disconnect handler', 6);
Logger.Log(
Logger.GetStackTrace(),
'gamepad: ' + gamePadEvent.gamepad.id + ' disconnected',
6
);
const deletedController = this.controllers[gamePadEvent.gamepad.index];
delete this.controllers[gamePadEvent.gamepad.index];
this.controllers = this.controllers.filter(
(controller) => controller !== undefined
);
this.onGamepadDisconnected(deletedController.id);
}
/**
* Scan for connected gamepads
*/
scanGamePads() {
const gamepads = navigator.getGamepads
? navigator.getGamepads()
: navigator.webkitGetGamepads
? navigator.webkitGetGamepads()
: [];
for (let i = 0; i < gamepads.length; i++) {
if (gamepads[i] && gamepads[i].index in this.controllers) {
this.controllers[gamepads[i].index].currentState = gamepads[i];
}
}
}
/**
* Updates the status of the gamepad and sends the inputs
*/
updateStatus() {
this.scanGamePads();
const toStreamerHandlers =
this.toStreamerMessagesProvider.toStreamerHandlers;
// Iterate over multiple controllers in the case the multiple gamepads are connected
for (const controller of this.controllers) {
// If we haven't received an id (possible if using an older version of UE), return to original functionality
const controllerIndex = (controller.id === undefined) ? this.controllers.indexOf(controller) : controller.id;
const currentState = controller.currentState;
for (let i = 0; i < controller.currentState.buttons.length; i++) {
const currentButton = controller.currentState.buttons[i];
const previousButton = controller.prevState.buttons[i];
if (currentButton.pressed) {
// press
if (i == gamepadLayout.LeftTrigger) {
// UEs left analog has a button index of 5
toStreamerHandlers.get('GamepadAnalog')([
controllerIndex,
5,
currentButton.value
]);
} else if (i == gamepadLayout.RightTrigger) {
// UEs right analog has a button index of 6
toStreamerHandlers.get('GamepadAnalog')([
controllerIndex,
6,
currentButton.value
]);
} else {
toStreamerHandlers.get('GamepadButtonPressed')([
controllerIndex,
i,
previousButton.pressed ? 1 : 0
]);
}
} else if (!currentButton.pressed && previousButton.pressed) {
// release
if (i == gamepadLayout.LeftTrigger) {
// UEs left analog has a button index of 5
toStreamerHandlers.get('GamepadAnalog')([
controllerIndex,
5,
0
]);
} else if (i == gamepadLayout.RightTrigger) {
// UEs right analog has a button index of 6
toStreamerHandlers.get('GamepadAnalog')([
controllerIndex,
6,
0
]);
} else {
toStreamerHandlers.get('GamepadButtonReleased')([
controllerIndex,
i,
0
]);
}
}
}
// Iterate over gamepad axes (we will increment in lots of 2 as there is 2 axes per stick)
for (let i = 0; i < currentState.axes.length; i += 2) {
// Horizontal axes are even numbered
const x = parseFloat(currentState.axes[i].toFixed(4));
// Vertical axes are odd numbered
// https://w3c.github.io/gamepad/#remapping Gamepad browser side standard mapping has positive down, negative up. This is downright disgusting. So we fix it.
const y = -parseFloat(currentState.axes[i + 1].toFixed(4));
// UE's analog axes follow the same order as the browsers, but start at index 1 so we will offset as such
toStreamerHandlers.get('GamepadAnalog')([
controllerIndex,
i + 1,
x
]); // Horizontal axes, only offset by 1
toStreamerHandlers.get('GamepadAnalog')([
controllerIndex,
i + 2,
y
]); // Vertical axes, offset by two (1 to match UEs axes convention and then another 1 for the vertical axes)
}
this.controllers[controllerIndex].prevState = currentState;
}
if (this.controllers.length > 0) {
this.requestAnimationFrame(() => this.updateStatus());
}
}
onGamepadResponseReceived(gamepadId: number) {
for(const controller of this.controllers) {
if(controller.id === undefined) {
controller.id = gamepadId;
break;
}
}
}
/**
* Event to send the gamepadconnected message to the application
*/
onGamepadConnected() {
// Default Functionality: Do Nothing
}
/**
* Event to send the gamepaddisconnected message to the application
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onGamepadDisconnected(controllerIdx: number) {
// Default Functionality: Do Nothing
}
onBeforeUnload(ev: Event) {
// When a user navigates away from the page, we need to inform UE of all the disconnecting
// controllers
for(const controller of this.controllers) {
this.onGamepadDisconnected(controller.id);
}
}
}
/**
* Additional types for Window and Navigator
*/
declare global {
interface Window {
mozRequestAnimationFrame(callback: FrameRequestCallback): number;
webkitRequestAnimationFrame(callback: FrameRequestCallback): number;
}
interface Navigator {
webkitGetGamepads(): Gamepad[];
}
}
/**
* Gamepad layout codes enum
*/
export enum gamepadLayout {
RightClusterBottomButton = 0,
RightClusterRightButton = 1,
RightClusterLeftButton = 2,
RightClusterTopButton = 3,
LeftShoulder = 4,
RightShoulder = 5,
LeftTrigger = 6,
RightTrigger = 7,
SelectOrBack = 8,
StartOrForward = 9,
LeftAnalogPress = 10,
RightAnalogPress = 11,
LeftClusterTopButton = 12,
LeftClusterBottomButton = 13,
LeftClusterLeftButton = 14,
LeftClusterRightButton = 15,
CentreButton = 16,
// Axes
LeftStickHorizontal = 0,
LeftStickVertical = 1,
RightStickHorizontal = 2,
RightStickVertical = 3
}