mirror of
https://github.com/italicsjenga/agb.git
synced 2024-12-23 08:11:33 +11:00
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:
commit
c433dbefdc
|
@ -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);
|
||||||
|
|
|
@ -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
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
BIN
website/site/assets/R.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 173 B |
BIN
website/site/assets/SELECT.png
Normal file
BIN
website/site/assets/SELECT.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 190 B |
BIN
website/site/assets/START.png
Normal file
BIN
website/site/assets/START.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 189 B |
BIN
website/site/assets/ab.png
Normal file
BIN
website/site/assets/ab.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 241 B |
BIN
website/site/assets/dpad.png
Normal file
BIN
website/site/assets/dpad.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 156 B |
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue