Add controller support (#630)

Simple use of gamepad apis to add controller support.
This commit is contained in:
Corwin 2024-04-10 20:38:54 +01:00 committed by GitHub
commit 27ad5d1184
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 141 additions and 48 deletions

View file

@ -9,6 +9,7 @@ import mGBA, { mGBAEmulator } from "./vendor/mgba";
import { GbaKey, KeyBindings } from "./bindings"; import { GbaKey, KeyBindings } from "./bindings";
import { styled } from "styled-components"; import { styled } from "styled-components";
import { useFrameSkip } from "./useFrameSkip.hook"; import { useFrameSkip } from "./useFrameSkip.hook";
import { useController } from "./useController.hook";
type Module = any; type Module = any;
@ -105,6 +106,7 @@ export const Mgba = forwardRef<MgbaHandle, MgbaProps>(
}, [state]); }, [state]);
useFrameSkip(mgbaModule); useFrameSkip(mgbaModule);
useController(mgbaModule);
useEffect(() => { useEffect(() => {
if (!gameLoaded) return; if (!gameLoaded) return;

View file

@ -0,0 +1,94 @@
import { MutableRefObject, useEffect } from "react";
import { mGBAEmulator } from "./vendor/mgba";
import { GbaKey } from "./bindings";
export function useController(mgbaModule: MutableRefObject<mGBAEmulator>) {
useEffect(() => {
let stopped = false;
let previouslyPressedButtons = new Set<GbaKey>();
function raf(time: DOMHighResTimeStamp) {
const controllers = navigator.getGamepads();
const currentlyPressed = new Set<GbaKey>();
for (let controller of controllers) {
if (!controller) continue;
if (controller.buttons[1].pressed) {
currentlyPressed.add(GbaKey.A);
}
if (controller.buttons[0].pressed) {
currentlyPressed.add(GbaKey.B);
}
if (controller.buttons[5].pressed) {
currentlyPressed.add(GbaKey.R);
}
if (controller.buttons[4].pressed) {
currentlyPressed.add(GbaKey.L);
}
if (controller.buttons[8].pressed) {
currentlyPressed.add(GbaKey.Select);
}
if (controller.buttons[9].pressed) {
currentlyPressed.add(GbaKey.Start);
}
if (controller.buttons[12].pressed) {
currentlyPressed.add(GbaKey.Up);
}
if (controller.buttons[13].pressed) {
currentlyPressed.add(GbaKey.Down);
}
if (controller.buttons[14].pressed) {
currentlyPressed.add(GbaKey.Left);
}
if (controller.buttons[15].pressed) {
currentlyPressed.add(GbaKey.Right);
}
if (controller.axes[0] < -0.5) {
currentlyPressed.add(GbaKey.Left);
}
if (controller.axes[0] > 0.5) {
currentlyPressed.add(GbaKey.Right);
}
if (controller.axes[1] < -0.5) {
currentlyPressed.add(GbaKey.Up);
}
if (controller.axes[1] > 0.5) {
currentlyPressed.add(GbaKey.Down);
}
}
for (let oldButton of previouslyPressedButtons) {
if (!currentlyPressed.has(oldButton)) {
mgbaModule.current.buttonUnpress(oldButton);
}
}
for (let newButton of currentlyPressed) {
if (!previouslyPressedButtons.has(newButton)) {
mgbaModule.current.buttonPress(newButton);
}
}
previouslyPressedButtons = currentlyPressed;
if (!stopped) {
window.requestAnimationFrame(raf);
}
}
function gamepadConnectedEvent() {}
window.addEventListener("gamepadconnected", gamepadConnectedEvent);
window.requestAnimationFrame(raf);
return () => {
stopped = true;
window.removeEventListener("gamepadconnected", gamepadConnectedEvent);
};
}, [mgbaModule]);
}

View file

@ -1,53 +1,48 @@
import { MutableRefObject, useEffect } from "react"; import { MutableRefObject, useEffect } from "react";
import { mGBAEmulator } from "./vendor/mgba"; import { mGBAEmulator } from "./vendor/mgba";
export function useFrameSkip(mgbaModule: MutableRefObject<mGBAEmulator>) { export function useFrameSkip(mgbaModule: MutableRefObject<mGBAEmulator>) {
useEffect(() => { useEffect(() => {
let previous: number | undefined = undefined; let previous: number | undefined = undefined;
let stopped = false; let stopped = false;
let smoothedFrameTime = 60; let smoothedFrameTime = 60;
let totalTime = 0; let totalTime = 0;
let paused = false; let paused = false;
function raf(time: DOMHighResTimeStamp) { function raf(time: DOMHighResTimeStamp) {
if (previous) { if (previous) {
const delta = time - previous; const delta = time - previous;
smoothedFrameTime = (smoothedFrameTime * 3 + delta) / 4; smoothedFrameTime = (smoothedFrameTime * 3 + delta) / 4;
const smoothedFrameRate = Math.round(1 / (smoothedFrameTime / 1000)); const smoothedFrameRate = Math.round(1 / (smoothedFrameTime / 1000));
totalTime += 1 / smoothedFrameRate;
totalTime += 1 / smoothedFrameRate; if (totalTime >= 1 / 60) {
totalTime -= 1 / 60;
if (totalTime >= 1 / 60) { if (paused) {
totalTime -= 1 / 60; mgbaModule.current.resumeGame();
if (paused) { paused = false;
mgbaModule.current.resumeGame(); }
paused = false; } else {
} if (!paused) {
} else { mgbaModule.current.pauseGame();
if (!paused) { paused = true;
mgbaModule.current.pauseGame(); }
paused = true;
}
}
}
previous = time;
if (!stopped) {
window.requestAnimationFrame(raf);
}
} }
}
previous = time;
if (!stopped) {
window.requestAnimationFrame(raf); window.requestAnimationFrame(raf);
return () => { stopped = true; }; }
}, [mgbaModule]); }
window.requestAnimationFrame(raf);
return () => {
} stopped = true;
};
}, [mgbaModule]);
}

View file

@ -1,7 +1,9 @@
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
export function useLocalStorage<T>(defaultValue: T, export function useLocalStorage<T>(
appName: string): [T, (newValue: T) => void] { defaultValue: T,
appName: string
): [T, (newValue: T) => void] {
const [value, setValue] = useState(() => { const [value, setValue] = useState(() => {
try { try {
const storageValue = localStorage.getItem(appName); const storageValue = localStorage.getItem(appName);
@ -19,7 +21,7 @@ export function useLocalStorage<T>(defaultValue: T,
setValue(newValue); setValue(newValue);
try { try {
localStorage.setItem(appName, JSON.stringify(newValue)); localStorage.setItem(appName, JSON.stringify(newValue));
} catch { } } catch {}
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);

View file

@ -1,10 +1,10 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export function useClientValue<T>(fn: () => T) { export function useClientValue<T>(fn: () => T) {
const [value, setValue] = useState<T>(); const [value, setValue] = useState<T>();
useEffect(() => { useEffect(() => {
setValue(fn()); setValue(fn());
}, [fn]); }, [fn]);
return value; return value;
} }