Skip to content

Commit

Permalink
Implement basic touch controls with input visualization
Browse files Browse the repository at this point in the history
  • Loading branch information
TiKevin83 committed Mar 12, 2024
1 parent 969583d commit 828ec73
Show file tree
Hide file tree
Showing 9 changed files with 276 additions and 32 deletions.
25 changes: 25 additions & 0 deletions src/components/Controls/ABStartSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Placeholder } from "./Placeholder";
import { TouchButton } from "./TouchButton";
import { GameBoyButton } from "./useControls";

export const ABStartSelect = () => {
return (
<div className="ml-4 flex flex-col">
<div className="flex flex-row">
<Placeholder />
<TouchButton button={GameBoyButton.SELECT}>SELECT</TouchButton>
<Placeholder />
</div>
<div className="flex flex-row">
<TouchButton button={GameBoyButton.START}>START</TouchButton>
<Placeholder />
<TouchButton button={GameBoyButton.A}>A</TouchButton>
</div>
<div className="flex flex-row">
<Placeholder />
<TouchButton button={GameBoyButton.B}>B</TouchButton>
<Placeholder />
</div>
</div>
);
};
39 changes: 39 additions & 0 deletions src/components/Controls/DPad.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
FaArrowDown,
FaArrowLeft,
FaArrowRight,
FaArrowUp,
} from "react-icons/fa";
import { TouchButton } from "./TouchButton";
import { GameBoyButton } from "./useControls";
import { Placeholder } from "./Placeholder";

export const DPad = () => {
return (
<div className="mr-4 flex flex-col">
<div className="flex flex-row">
<Placeholder />
<TouchButton button={GameBoyButton.UP}>
<FaArrowUp />
</TouchButton>
<Placeholder />
</div>
<div className="flex flex-row">
<TouchButton button={GameBoyButton.LEFT}>
<FaArrowLeft />
</TouchButton>
<Placeholder />
<TouchButton button={GameBoyButton.RIGHT}>
<FaArrowRight />
</TouchButton>
</div>
<div className="flex flex-row">
<Placeholder />
<TouchButton button={GameBoyButton.DOWN}>
<FaArrowDown />
</TouchButton>
<Placeholder />
</div>
</div>
);
};
11 changes: 11 additions & 0 deletions src/components/Controls/Placeholder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const Placeholder = () => {
return (
<div
className="h-16 w-16"
onGotPointerCapture={(event) => {
const targetElement = event.target as HTMLDivElement;
targetElement.releasePointerCapture(event.pointerId);
}}
></div>
);
};
46 changes: 46 additions & 0 deletions src/components/Controls/TouchButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { type GameBoyButton } from "./useControls";
import { useDisplayedButtonsStore } from "./useDisplayedButtonsStore";
import { useTouchButtonsStore } from "./useTouchButtonsStore";

interface Props {
button: GameBoyButton;
children: React.ReactNode;
}

export const TouchButton: React.FC<Props> = ({ button, children }) => {
const { addTouchButton, removeTouchButton } = useTouchButtonsStore(
(state) => ({
addTouchButton: state.addTouchButton,
removeTouchButton: state.removeTouchButton,
}),
);
const { displayedButtons, addDisplayedButton, removeDisplayedButton } =
useDisplayedButtonsStore((state) => ({
displayedButtons: state.displayedButtons,
addDisplayedButton: state.addDisplayedButton,
removeDisplayedButton: state.removeDisplayedButton,
}));

return (
<button
className="pointer-events-auto flex h-16 w-16 touch-none select-none items-center justify-center"
style={{
backgroundColor: (displayedButtons & button) > 0 ? "blue" : "white",
}}
onPointerEnter={() => {
addTouchButton(button);
addDisplayedButton(button);
}}
onGotPointerCapture={(event) => {
const targetElement = event.target as HTMLButtonElement;
targetElement.releasePointerCapture(event.pointerId);
}}
onPointerLeave={() => {
removeTouchButton(button);
removeDisplayedButton(button);
}}
>
{children}
</button>
);
};
119 changes: 98 additions & 21 deletions src/components/Controls/useControls.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useKeyMappingStore } from "./useKeyMappingStore";
import { useControllerMappingStore } from "./useControllerMappingStore";
import { useDisplayedButtonsStore } from "./useDisplayedButtonsStore";
import { useTouchButtonsStore } from "./useTouchButtonsStore";

declare const Module: {
cwrap: (
Expand Down Expand Up @@ -34,6 +36,21 @@ export const useControls = (initialized: boolean, gbPointer?: number) => {
((...args: unknown[]) => unknown) | null
>(null);
const buttons = useRef(0);
const { addDisplayedButton, removeDisplayedButton } =
useDisplayedButtonsStore((state) => ({
addDisplayedButton: state.addDisplayedButton,
removeDisplayedButton: state.removeDisplayedButton,
}));
// Fetch initial state
const touchButtonsRef = useRef(useTouchButtonsStore.getState().touchButtons);
// Connect to the store on mount, disconnect on unmount, catch state-changes in a reference
useEffect(
() =>
useTouchButtonsStore.subscribe(
(state) => (touchButtonsRef.current = state.touchButtons),
),
[],
);
const { keyMapping, keyMappingInProgress } = useKeyMappingStore((state) => ({
keyMapping: state.keyMapping,
keyMappingInProgress: state.keyMappingInProgress,
Expand All @@ -53,33 +70,93 @@ export const useControls = (initialized: boolean, gbPointer?: number) => {
gambatteReset(gbPointer, 101 * (2 << 14));
return;
}
buttons.current |=
(Number(event.code === keyMapping.a) * GameBoyButton.A) |
(Number(event.code === keyMapping.b) * GameBoyButton.B) |
(Number(event.code === keyMapping.select) * GameBoyButton.SELECT) |
(Number(event.code === keyMapping.start) * GameBoyButton.START) |
(Number(event.code === keyMapping.right) * GameBoyButton.RIGHT) |
(Number(event.code === keyMapping.left) * GameBoyButton.LEFT) |
(Number(event.code === keyMapping.up) * GameBoyButton.UP) |
(Number(event.code === keyMapping.down) * GameBoyButton.DOWN);
switch (event.code) {
case keyMapping.a:
addDisplayedButton(GameBoyButton.A);
buttons.current |= GameBoyButton.A;
break;
case keyMapping.b:
addDisplayedButton(GameBoyButton.B);
buttons.current |= GameBoyButton.B;
break;
case keyMapping.select:
addDisplayedButton(GameBoyButton.SELECT);
buttons.current |= GameBoyButton.SELECT;
break;
case keyMapping.start:
addDisplayedButton(GameBoyButton.START);
buttons.current |= GameBoyButton.START;
break;
case keyMapping.right:
addDisplayedButton(GameBoyButton.RIGHT);
buttons.current |= GameBoyButton.RIGHT;
break;
case keyMapping.left:
addDisplayedButton(GameBoyButton.LEFT);
buttons.current |= GameBoyButton.LEFT;
break;
case keyMapping.up:
addDisplayedButton(GameBoyButton.UP);
buttons.current |= GameBoyButton.UP;
break;
case keyMapping.down:
addDisplayedButton(GameBoyButton.DOWN);
buttons.current |= GameBoyButton.DOWN;
break;
default:
break;
}
},
[keyMappingInProgress, keyMapping, gambatteReset, gbPointer],
[
keyMappingInProgress,
keyMapping,
gambatteReset,
gbPointer,
addDisplayedButton,
],
);

const keyUpHandler = useCallback(
(event: KeyboardEvent) => {
event.preventDefault();
buttons.current &=
(Number(event.code !== keyMapping.a) * 0x01) |
(Number(event.code !== keyMapping.b) * 0x02) |
(Number(event.code !== keyMapping.select) * 0x04) |
(Number(event.code !== keyMapping.start) * 0x08) |
(Number(event.code !== keyMapping.right) * 0x10) |
(Number(event.code !== keyMapping.left) * 0x20) |
(Number(event.code !== keyMapping.up) * 0x40) |
(Number(event.code !== keyMapping.down) * 0x80);
switch (event.code) {
case keyMapping.a:
removeDisplayedButton(GameBoyButton.A);
buttons.current &= ~GameBoyButton.A;
break;
case keyMapping.b:
removeDisplayedButton(GameBoyButton.B);
buttons.current &= ~GameBoyButton.B;
break;
case keyMapping.select:
removeDisplayedButton(GameBoyButton.SELECT);
buttons.current &= ~GameBoyButton.SELECT;
break;
case keyMapping.start:
removeDisplayedButton(GameBoyButton.START);
buttons.current &= ~GameBoyButton.START;
break;
case keyMapping.right:
removeDisplayedButton(GameBoyButton.RIGHT);
buttons.current &= ~GameBoyButton.RIGHT;
break;
case keyMapping.left:
removeDisplayedButton(GameBoyButton.LEFT);
buttons.current &= ~GameBoyButton.LEFT;
break;
case keyMapping.up:
removeDisplayedButton(GameBoyButton.UP);
buttons.current &= ~GameBoyButton.UP;
break;
case keyMapping.down:
removeDisplayedButton(GameBoyButton.DOWN);
buttons.current &= ~GameBoyButton.DOWN;
break;
default:
break;
}
},
[keyMapping],
[keyMapping, removeDisplayedButton],
);

useEffect(() => {
Expand Down Expand Up @@ -133,7 +210,7 @@ export const useControls = (initialized: boolean, gbPointer?: number) => {
}
});

return buttons.current | controllerButtons;
return buttons.current | controllerButtons | touchButtonsRef.current;
}, [controllerMapping, gambatteReset]);

useEffect(() => {
Expand Down
20 changes: 20 additions & 0 deletions src/components/Controls/useDisplayedButtonsStore.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { create } from "zustand";
import { type GameBoyButton } from "./useControls";

interface DisplayedButtonsState {
displayedButtons: number;
addDisplayedButton: (button: GameBoyButton) => void;
removeDisplayedButton: (button: GameBoyButton) => void;
}

export const useDisplayedButtonsStore = create<DisplayedButtonsState>(
(set, get) => ({
displayedButtons: 0,
addDisplayedButton: (button) => {
set({ displayedButtons: get().displayedButtons | button });
},
removeDisplayedButton: (button) => {
set({ displayedButtons: get().displayedButtons & ~button });
},
}),
);
18 changes: 18 additions & 0 deletions src/components/Controls/useTouchButtonsStore.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { create } from "zustand";
import { type GameBoyButton } from "./useControls";

interface TouchButtonsState {
touchButtons: number;
addTouchButton: (button: GameBoyButton) => void;
removeTouchButton: (button: GameBoyButton) => void;
}

export const useTouchButtonsStore = create<TouchButtonsState>((set, get) => ({
touchButtons: 0,
addTouchButton: (button) => {
set({ touchButtons: get().touchButtons | button });
},
removeTouchButton: (button) => {
set({ touchButtons: get().touchButtons & ~button });
},
}));
1 change: 1 addition & 0 deletions src/pages/_document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export default function Document() {
return (
<Html lang="en">
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<Script src="libgambatte.js" strategy="beforeInteractive" />
</Head>
<body className="font-mono">
Expand Down
29 changes: 18 additions & 11 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { useSession } from "next-auth/react";
import { useEmuWindowSizeStore } from "~/components/EmuWindowSize/useEmuWindowSizeStore";
import { EmuWindowSize } from "~/components/EmuWindowSize/EmuWindowSize";
import { ColorEmulation } from "~/components/GBCColors/ColorEmulation";
import { DPad } from "~/components/Controls/DPad";
import { ABStartSelect } from "~/components/Controls/ABStartSelect";

declare const Module: {
onRuntimeInitialized: () => void;
Expand Down Expand Up @@ -347,18 +349,22 @@ export default function Home() {
</div>
</>
)}
<div className="flex flex-col items-center gap-2">
<div className="pointer-events-none flex touch-none flex-col items-center gap-2">
<p className="text-white">{gameHash?.toUpperCase()}</p>
<canvas
ref={canvasRef}
id="gameboy"
width={160}
height={144}
style={{
width: `${(windowSize * 160) / actualDevicePixelRatio}px`,
height: `${(windowSize * 144) / actualDevicePixelRatio}px`,
}}
></canvas>
<div className="flex flex-row">
<DPad />
<canvas
ref={canvasRef}
id="gameboy"
width={160}
height={144}
style={{
width: `${(windowSize * 160) / actualDevicePixelRatio}px`,
height: `${(windowSize * 144) / actualDevicePixelRatio}px`,
}}
></canvas>
<ABStartSelect />
</div>
<label htmlFor="Volume" className="text-white">
Volume
</label>
Expand All @@ -372,6 +378,7 @@ export default function Home() {
onChange={(event) => {
setVolume(Number(event.target.value));
}}
className="pointer-events-auto touch-auto"
></input>
</div>
<div className="flex flex-col items-center gap-2">
Expand Down

0 comments on commit 828ec73

Please sign in to comment.