Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix CMD Key Issue with useKeyboardControls in Player Component #76

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 5 additions & 16 deletions frontend/src/components/Game.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { cssObj } from '@fuel-ui/css';
import { Box, Button } from '@fuel-ui/react';
import type { KeyboardControlsEntry } from '@react-three/drei';
import { KeyboardControls } from '@react-three/drei';
import { Canvas } from '@react-three/fiber';
import { BN } from 'fuels';
import type { BytesLike } from 'fuels';
import { useState, useEffect, useMemo, Suspense } from 'react';
import { useState, useEffect, Suspense } from 'react';

import type { Modals } from '../constants';
import { Controls, buttonStyle, FoodTypeInput } from '../constants';
import { buttonStyle, FoodTypeInput, ControlsMap } from '../constants';
import { KeyboardControlsProvider } from '../hooks/useKeyboardControls';
import type {
AddressInput,
ContractAbi,
Expand Down Expand Up @@ -111,16 +110,6 @@ export default function Game({
setUpdateNum(updateNum + 1);
}

const controlsMap = useMemo<KeyboardControlsEntry[]>(
() => [
{ name: Controls.forward, keys: ['ArrowUp', 'w', 'W'] },
{ name: Controls.back, keys: ['ArrowDown', 's', 'S'] },
{ name: Controls.left, keys: ['ArrowLeft', 'a', 'A'] },
{ name: Controls.right, keys: ['ArrowRight', 'd', 'D'] },
],
[]
);

return (
<Box css={styles.canvasContainer}>
{status === 'error' && (
Expand Down Expand Up @@ -155,7 +144,7 @@ export default function Game({

{/* PLAYER */}
{player !== null && (
<KeyboardControls map={controlsMap}>
<KeyboardControlsProvider map={ControlsMap}>
<Player
tileStates={tileStates}
modal={modal}
Expand All @@ -166,7 +155,7 @@ export default function Game({
canMove={canMove}
mobileControlState={mobileControlState}
/>
</KeyboardControls>
</KeyboardControlsProvider>
)}
</Suspense>
</Canvas>
Expand Down
13 changes: 6 additions & 7 deletions frontend/src/components/Player.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { useKeyboardControls } from '@react-three/drei';
import { useFrame, useLoader } from '@react-three/fiber';
import type { Dispatch, SetStateAction } from 'react';
import { useState, useEffect, useRef } from 'react';
import type { Texture, Sprite } from 'three';
import { Vector3, TextureLoader, NearestFilter } from 'three';

import type { Modals, Controls } from '../constants';
import type { Modals } from '../constants';
import { convertTime, TILES } from '../constants';
import type { KeyboardControlsState } from '../hooks/useKeyboardControls';
import { useKeyboardControls } from '../hooks/useKeyboardControls';
import type { GardenVectorOutput } from '../sway-api/contracts/ContractAbi';

import type { MobileControls, Position } from './Game';
Expand Down Expand Up @@ -60,7 +61,7 @@ export default function Player({
const [currentTile, setCurrentTile] = useState<number>(0);
const [spriteMap, setSpriteMap] = useState<Texture>();
const ref = useRef<Sprite>(null);
const [, get] = useKeyboardControls<Controls>();
const controls = useKeyboardControls();

const tilesHoriz = 4;
const tilesVert = 5;
Expand All @@ -80,11 +81,10 @@ export default function Player({
const velocity = new Vector3();

useFrame((_s, dl) => {
const state = get();
checkTiles();
updateCameraPosition();

if (canMove) movePlayer(dl, state, mobileControlState);
if (canMove) movePlayer(dl, controls, mobileControlState);
});

function updateCameraPosition() {
Expand Down Expand Up @@ -185,8 +185,7 @@ export default function Player({

function movePlayer(
dl: number,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
state: any,
state: KeyboardControlsState,
mobileControlState: MobileControls
) {
if (!ref.current) return;
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { cssObj } from '@fuel-ui/css';
import type { Asset, BN, NetworkFuel } from 'fuels';
import { Vector3 } from 'three';

import type { KeyboardControlsEntry } from './hooks/useKeyboardControls';

// import contractIds from './sway-api/contract-ids.json';

export const FUEL_PROVIDER_URL = 'https://testnet.fuel.network/v1/graphql';
Expand Down Expand Up @@ -49,6 +51,13 @@ export enum Controls {
back = 'back',
}

export const ControlsMap: KeyboardControlsEntry[] = [
{ name: Controls.forward, keys: ['ArrowUp', 'w', 'W'] },
{ name: Controls.back, keys: ['ArrowDown', 's', 'S'] },
{ name: Controls.left, keys: ['ArrowLeft', 'a', 'A'] },
{ name: Controls.right, keys: ['ArrowRight', 'd', 'D'] },
];

export const TILES = [
new Vector3(-2.47, -0.88, 0),
new Vector3(-1.23, -0.88, 0),
Expand Down
134 changes: 134 additions & 0 deletions frontend/src/hooks/useKeyboardControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import React, {
useState,
createContext,
useContext,
useMemo,
useCallback,
} from 'react';

import { useWindowListener } from './useWindowListener';

export const Controls = {
forward: 'forward',
back: 'back',
left: 'left',
right: 'right',
} as const;

export type ControlsType = (typeof Controls)[keyof typeof Controls];

export type KeyboardControlsEntry = {
name: ControlsType;
keys: string[];
up?: boolean;
};

export type KeyboardControlsState = { [key in ControlsType]: boolean };

const KeyboardControlsContext = createContext<
KeyboardControlsState | undefined
>(undefined);

export const useKeyboardControls = () => {
const context = useContext(KeyboardControlsContext);
if (!context) {
throw new Error(
'useKeyboardControls must be used within a KeyboardControlsProvider'
);
}
return context;
};

type KeyboardControlsProviderProps = {
map: KeyboardControlsEntry[];
children: React.ReactNode;
};

export const KeyboardControlsProvider: React.FC<
KeyboardControlsProviderProps
> = ({ map, children }) => {
const [state, setState] = useState<KeyboardControlsState>(
map.reduce(
(acc, cur) => ({ ...acc, [cur.name]: false }),
{} as KeyboardControlsState
)
);

const keyMap = useMemo(
() =>
map.reduce(
(acc, { name, keys }) => {
keys.forEach((key) => {
acc[key] = name;
});
return acc;
},
{} as { [key: string]: ControlsType }
),
[map]
);

const downHandler = useCallback(
(event: KeyboardEvent) => {
// Reset the state if the meta key is pressed
if (event.metaKey || event.key === 'Meta') {
event.preventDefault();

setState((prevState) =>
Object.keys(prevState).reduce(
(acc, key) => ({
...acc,
[key]: false,
}),
{} as KeyboardControlsState
)
);
return;
}

const controlName = keyMap[event.key];
if (controlName && !state[controlName]) {
event.preventDefault();
setState((prevState) => ({ ...prevState, [controlName]: true }));
}
},
[keyMap, state]
);

const upHandler = useCallback(
(event: KeyboardEvent) => {
// Reset the state if the meta key is pressed
if (event.key === 'Meta') {
event.preventDefault();

setState((prevState) =>
Object.keys(prevState).reduce(
(acc, key) => ({
...acc,
[key]: false,
}),
{} as KeyboardControlsState
)
);

return;
}

const controlName = keyMap[event.key];
if (controlName && state[controlName]) {
event.preventDefault();
setState((prevState) => ({ ...prevState, [controlName]: false }));
}
},
[keyMap, state]
);

useWindowListener('keydown', downHandler);
useWindowListener('keyup', upHandler);

return (
<KeyboardControlsContext.Provider value={state}>
{children}
</KeyboardControlsContext.Provider>
);
};
44 changes: 44 additions & 0 deletions frontend/src/hooks/useWindowListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useRef, useEffect } from 'react';

/**
* A custom React hook that allows you to easily add and remove event listeners to the `window` object.
* This hook ensures that the event listener is properly cleaned up when the component unmounts or the event changes.
* It also handles the potential issue of stale closures by using a ref to keep the callback function up to date.
*
* @template E The type of event that this hook will listen for. It extends the base `Event` type.
* @param {string} event - The name of the event to listen for on the window object. For example: 'resize', 'scroll', etc.
* @param {(e: E) => void} callback - The callback function that will be executed when the event is triggered.
* The callback receives the event object as its parameter.
*
* @example
* // Example of using useWindowListener to add a resize event listener
* useWindowListener('resize', (e) => {
* console.log('Window resized', e);
* });
*
* @example
* // Example of using useWindowListener with a custom event type
* useWindowListener<CustomEvent>('myCustomEvent', (e) => {
* console.log('Custom event triggered', e.detail);
* });
*/
export const useWindowListener = <E extends Event>(
event: string,
callback: (e: E) => void
) => {
// useRef is used to hold a reference to the callback. This approach ensures that
// the callback can be updated without re-adding the event listener, reducing unnecessary operations.
const ref = useRef(callback);

useEffect(() => {
ref.current = callback;
}, [callback]);

useEffect(() => {
const handler = (e: Event) => ref.current(e as E);
window.addEventListener(event, handler);
return () => {
window.removeEventListener(event, handler);
};
}, [event]);
};