Mobile support for website (#607)

Adds mobile control scheme that sort of works.
I feel like it doesn't quite work.

Also, frame rate problems on mobile.

- [ ] Changelog updated / no changelog update needed
This commit is contained in:
Corwin 2024-04-05 20:24:29 +01:00 committed by GitHub
commit c433dbefdc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 247 additions and 8 deletions

View file

@ -1,4 +1,4 @@
import { useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { Mgba, MgbaHandle } from "./mgba"; import { Mgba, MgbaHandle } from "./mgba";
import { BindingsControl, DefaultBindingsSet, Bindings } from "./bindings"; import { BindingsControl, DefaultBindingsSet, Bindings } from "./bindings";
import { styled } from "styled-components"; import { styled } from "styled-components";
@ -48,7 +48,7 @@ const StartButtonWrapper = styled.button`
function App() { function App() {
const [{ volume, bindings }, setState] = useLocalStorage( const [{ volume, bindings }, setState] = useLocalStorage(
{ volume: 1.0, bindings: DefaultBindingsSet() }, { volume: 1.0, bindings: DefaultBindingsSet() },
"agbrswebplayer" "agbrswebplayer",
); );
const setVolume = (newVolume: number) => const setVolume = (newVolume: number) =>
@ -66,6 +66,30 @@ function App() {
setShowBindings(!showBindings); setShowBindings(!showBindings);
}); });
useEffect(() => {
const buttonPress = (event: MessageEvent) => {
const data = event.data;
const { isPressed, button, reset } = data;
if (isPressed === true) {
mgbaRef.current?.buttonPress(button);
} else if (isPressed === false) {
mgbaRef.current?.buttonRelease(button);
}
if (reset) {
mgbaRef.current?.restart();
}
};
window.addEventListener("message", buttonPress);
return () => {
window.removeEventListener("message", buttonPress);
};
}, []);
useAvoidItchIoScrolling(); useAvoidItchIoScrolling();
const gameUrl = window.location.hash.slice(1); const gameUrl = window.location.hash.slice(1);

View file

@ -6,7 +6,7 @@ import {
useState, useState,
} from "react"; } from "react";
import mGBA from "./vendor/mgba"; import mGBA from "./vendor/mgba";
import { KeyBindings } from "./bindings"; import { GbaKey, KeyBindings } from "./bindings";
import { styled } from "styled-components"; import { styled } from "styled-components";
type Module = any; type Module = any;
@ -36,6 +36,8 @@ const MgbaCanvas = styled.canvas`
export interface MgbaHandle { export interface MgbaHandle {
restart: () => void; restart: () => void;
buttonPress: (key: GbaKey) => void;
buttonRelease: (key: GbaKey) => void;
} }
export const Mgba = forwardRef<MgbaHandle, MgbaProps>( export const Mgba = forwardRef<MgbaHandle, MgbaProps>(
@ -70,7 +72,6 @@ export const Mgba = forwardRef<MgbaHandle, MgbaProps>(
if (state !== MgbaState.Uninitialised) return; if (state !== MgbaState.Uninitialised) return;
setState(MgbaState.Initialising); setState(MgbaState.Initialising);
mgbaModule.current = { mgbaModule.current = {
canvas: canvas.current, canvas: canvas.current,
}; };
@ -124,9 +125,11 @@ export const Mgba = forwardRef<MgbaHandle, MgbaProps>(
useImperativeHandle(ref, () => { useImperativeHandle(ref, () => {
return { return {
restart: () => mgbaModule.current.quickReload(), restart: () => mgbaModule.current.quickReload(),
buttonPress: (key: GbaKey) => mgbaModule.current.buttonPress(key),
buttonRelease: (key: GbaKey) => mgbaModule.current.buttonUnpress(key),
}; };
}); });
return <MgbaCanvas ref={canvas} />; return <MgbaCanvas ref={canvas} />;
} },
); );

BIN
website/site/assets/L.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B

BIN
website/site/assets/R.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 B

BIN
website/site/assets/ab.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

View file

@ -1,6 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>agbrs crash backtrace</title> <title>agbrs crash backtrace</title>
<style> <style>
body { body {

View file

@ -1,6 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>agb - a rust framework for making Game Boy Advance games</title> <title>agb - a rust framework for making Game Boy Advance games</title>
<style> <style>
*, *,
@ -19,11 +20,13 @@
flex-direction: column; flex-direction: column;
margin: 0; margin: 0;
} }
h1, h1,
h2, h2,
h3 { h3 {
line-height: 1.2; line-height: 1.2;
} }
.gameDisplay > div { .gameDisplay > div {
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -31,9 +34,10 @@
} }
.gameDisplay { .gameDisplay {
height: calc(min(480px, 40vh, calc(100vw / 3))); height: clamp(480px, 40vh, calc(100vw / 3));
max-width: 100vw; max-width: 100vw;
margin-top: 20px; margin-top: 20px;
overflow: hidden;
} }
.gameDisplay .imageWrapper { .gameDisplay .imageWrapper {
@ -54,7 +58,7 @@
} }
header, header,
.help { .desktopHelp {
max-width: 60%; max-width: 60%;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
@ -88,8 +92,50 @@
.links > a:hover { .links > a:hover {
border: solid black 2px; border: solid black 2px;
} }
.mobileControls {
display: flex;
gap: 10px;
justify-content: center;
align-items: center;
flex-direction: column;
margin-bottom: 40px;
}
.mobileControls img {
image-rendering: pixelated;
height: 100%;
}
.mobileControlsRow {
display: flex;
align-items: center;
justify-content: center;
gap: 40px;
}
@media (max-width: 800px) {
.desktopHelp {
display: none;
}
}
@media (min-width: 800px) {
.mobileControls {
display: none;
}
}
.mobileControlsBig {
height: calc(32px * 6);
}
.mobileControlsSmall {
height: calc(32px * 3);
}
</style> </style>
</head> </head>
<body> <body>
<header> <header>
<h1>agb - a rust framework for making Game Boy Advance games</h1> <h1>agb - a rust framework for making Game Boy Advance games</h1>
@ -99,13 +145,31 @@
<div> <div>
<div class="imageWrapper"><img src="assets/left.png" /></div> <div class="imageWrapper"><img src="assets/left.png" /></div>
<iframe <iframe
id="gameFrame"
onload="this.contentWindow.focus()" onload="this.contentWindow.focus()"
src="mgba/index.html#/assets/combo.gba" src="mgba/index.html#/assets/combo.gba"
></iframe> ></iframe>
<div class="imageWrapper"><img src="assets/right.png" /></div> <div class="imageWrapper"><img src="assets/right.png" /></div>
</div> </div>
</div> </div>
<div class="help"> <div id="mobileControls" class="mobileControls">
<div class="mobileControlsRow mobileControlsSmall">
<img id="mobileL" src="assets/L.png" />
<img id="mobileR" src="assets/R.png" />
</div>
<div class="mobileControlsRow mobileControlsBig">
<img id="mobileDpad" src="assets/dpad.png" />
<img id="mobileAb" src="assets/ab.png" />
</div>
<div class="mobileControlsRow mobileControlsSmall">
<img id="mobileStart" src="assets/START.png" />
<img id="mobileSelect" src="assets/SELECT.png" />
</div>
<div class="mobileControlsRow">
<button id="mobileRestart">Restart</button>
</div>
</div>
<div class="desktopHelp">
<p> <p>
Press escape to open the menu where you can view or change controls Press escape to open the menu where you can view or change controls
and restart the game. The game provided is a combination of multiple and restart the game. The game provided is a combination of multiple
@ -122,5 +186,152 @@
<a href="https://docs.rs/agb/latest/agb/">Docs</a> <a href="https://docs.rs/agb/latest/agb/">Docs</a>
</div> </div>
</section> </section>
<script>
const addSimpleButton = (ele, key) => {
ele.addEventListener("touchstart", (evt) => pressButton(key));
ele.addEventListener("touchend", (evt) => releaseButton(key));
};
mobileRestart.addEventListener("click", () => {
gameFrame.contentWindow.postMessage({ reset: true });
});
const pressButton = (key) => {
gameFrame.contentWindow.postMessage({ isPressed: true, button: key });
};
const releaseButton = (key) => {
gameFrame.contentWindow.postMessage({ isPressed: false, button: key });
};
mobileControls.addEventListener("touchmove", (evt) =>
evt.preventDefault()
);
mobileControls.addEventListener("contextmenu", (evt) =>
evt.preventDefault()
);
addSimpleButton(mobileL, "L");
addSimpleButton(mobileR, "L");
addSimpleButton(mobileStart, "Start");
addSimpleButton(mobileSelect, "Select");
let previouslyPressedButtons = [];
const dpadMovement = (touch) => {
const target = touch.target.getBoundingClientRect();
const touchPoint = { x: touch.clientX, y: touch.clientY };
const targetArea = {
x: target.left,
y: target.top,
width: target.width,
height: target.height,
};
const relativePosition = {
x: touchPoint.x - targetArea.x,
y: touchPoint.y - targetArea.y,
};
const touchedBox = {
x: Math.floor(relativePosition.x / (targetArea.width / 3)),
y: Math.floor(relativePosition.y / (targetArea.height / 3)),
};
const buttonBoxMapping = [
[["Up", "Left"], ["Up"], ["Up", "Right"]],
[["Left"], [], ["Right"]],
[["Down", "Left"], ["Down"], ["Down", "Right"]],
];
const buttonsToPress =
(buttonBoxMapping[touchedBox.y] ?? [])[touchedBox.x] ?? [];
for (let oldButton of previouslyPressedButtons) {
if (!buttonsToPress.includes(oldButton)) {
releaseButton(oldButton);
}
}
for (let newButton of buttonsToPress) {
if (!previouslyPressedButtons.includes(newButton)) {
pressButton(newButton);
}
}
previouslyPressedButtons = buttonsToPress;
};
mobileDpad.addEventListener("touchstart", (evt) =>
dpadMovement(evt.touches[0])
);
mobileDpad.addEventListener("touchmove", (evt) =>
dpadMovement(evt.touches[0])
);
mobileDpad.addEventListener("touchend", (evt) => {
for (let oldButton of previouslyPressedButtons) {
releaseButton(oldButton);
}
previouslyPressedButtons = [];
});
let mobileAbAPress = undefined;
const mobileAbMovement = (touch) => {
const target = touch.target.getBoundingClientRect();
const touchPoint = { x: touch.clientX, y: touch.clientY };
const targetArea = {
x: target.left,
y: target.top,
width: target.width,
height: target.height,
};
const relativePosition = {
x: touchPoint.x - targetArea.x,
y: touchPoint.y - targetArea.y,
};
const aPress = relativePosition.x > relativePosition.y;
if (aPress !== mobileAbAPress) {
if (mobileAbAPress === true) {
releaseButton("A");
} else if (mobileAbAPress === false) {
releaseButton("B");
}
}
if (aPress) {
pressButton("A");
} else {
pressButton("B");
}
mobileAbAPress = aPress;
};
mobileAb.addEventListener("touchstart", (evt) =>
mobileAbMovement(evt.touches[0])
);
mobileAb.addEventListener("touchmove", (evt) =>
mobileAbMovement(evt.touches[0])
);
mobileAb.addEventListener("touchend", (evt) => {
if (mobileAbAPress === true) {
releaseButton("A");
} else if (mobileAbAPress === false) {
releaseButton("B");
}
mobileAbAPress = undefined;
});
</script>
</body> </body>
</html> </html>