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

kiosk: support gamepad dpad for navigation #9712

Merged
merged 1 commit into from
Oct 10, 2023
Merged
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
52 changes: 26 additions & 26 deletions kiosk/src/Components/GameList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useContext, useCallback } from "react";
import React, { useEffect, useState, useContext, useCallback } from "react";
import { Swiper, SwiperSlide } from "swiper/react";
import { Swiper as SwiperClass, Pagination } from "swiper";
import "swiper/css";
Expand All @@ -8,11 +8,7 @@ import { playSoundEffect } from "../Services/SoundEffectService";
import { AppStateContext } from "../State/AppStateContext";
import { selectGameByIndex } from "../Transforms/selectGameByIndex";
import { launchGame } from "../Transforms/launchGame";
import {
getHighScores,
getSelectedGameIndex,
getSelectedGameId,
} from "../State";
import { getSelectedGameIndex, getSelectedGameId } from "../State";
import * as GamepadManager from "../Services/GamepadManager";
import * as NavGrid from "../Services/NavGrid";
import { useOnControlPress, useMakeNavigable } from "../Hooks";
Expand All @@ -21,12 +17,18 @@ interface IProps {}

const GameList: React.FC<IProps> = ({}) => {
const { state: kiosk } = useContext(AppStateContext);
const localSwiper = useRef<SwiperClass>();
const [localSwiper, setLocalSwiper] = useState<SwiperClass | undefined>(
undefined
);
const [userInitiatedTransition, setUserInitiatedTransition] =
React.useState(false);
const [pageInited, setPageInited] = React.useState(false);

useMakeNavigable(localSwiper?.current?.el, {
const handleLocalSwiper = (swiper: SwiperClass) => {
setLocalSwiper(swiper);
};

useMakeNavigable(localSwiper?.el, {
exitDirections: [NavGrid.NavDirection.Down, NavGrid.NavDirection.Up],
autofocus: true,
});
Expand All @@ -41,7 +43,8 @@ const GameList: React.FC<IProps> = ({}) => {
};

const syncSelectedGame = useCallback(() => {
const gameIndex = localSwiper?.current?.realIndex || 0;
if (!localSwiper || localSwiper.destroyed) return;
const gameIndex = localSwiper.realIndex || 0;
const selectedGameIndex = getSelectedGameIndex();
if (
selectedGameIndex !== undefined &&
Expand All @@ -53,14 +56,15 @@ const GameList: React.FC<IProps> = ({}) => {

// on page load use effect
useEffect(() => {
if (!localSwiper || localSwiper.destroyed) return;
if (!pageInited) {
if (kiosk.allGames.length) {
if (!kiosk.selectedGameId) {
selectGameByIndex(0);
localSwiper.current?.slideTo(2);
localSwiper.slideTo(2);
} else {
const index = getSelectedGameIndex() || 0;
localSwiper.current?.slideTo(index + 2);
localSwiper.slideTo(index + 2);
}
setPageInited(true);
}
Expand All @@ -71,9 +75,10 @@ const GameList: React.FC<IProps> = ({}) => {
useOnControlPress(
[localSwiper, userInitiatedTransition],
() => {
if (NavGrid.isActiveElement(localSwiper?.current?.el)) {
if (!localSwiper || localSwiper.destroyed) return;
if (NavGrid.isActiveElement(localSwiper.el)) {
setUserInitiatedTransition(true);
setTimeout(() => localSwiper.current?.slideNext(), 1);
setTimeout(() => localSwiper.slideNext(), 1);
}
},
GamepadManager.GamepadControl.DPadRight
Expand All @@ -83,9 +88,10 @@ const GameList: React.FC<IProps> = ({}) => {
useOnControlPress(
[localSwiper, userInitiatedTransition],
() => {
if (NavGrid.isActiveElement(localSwiper?.current?.el)) {
if (!localSwiper || localSwiper.destroyed) return;
if (NavGrid.isActiveElement(localSwiper.el)) {
setUserInitiatedTransition(true);
setTimeout(() => localSwiper.current?.slidePrev(), 1);
setTimeout(() => localSwiper?.slidePrev(), 1);
}
},
GamepadManager.GamepadControl.DPadLeft
Expand All @@ -95,12 +101,12 @@ const GameList: React.FC<IProps> = ({}) => {
useOnControlPress(
[localSwiper, userInitiatedTransition],
() => {
if (NavGrid.isActiveElement(localSwiper?.current?.el)) {
if (!localSwiper || localSwiper.destroyed) return;
if (NavGrid.isActiveElement(localSwiper.el)) {
launchSelectedGame();
}
},
GamepadManager.GamepadControl.AButton,
GamepadManager.GamepadControl.BButton
GamepadManager.GamepadControl.AButton
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this line doing? And why remove the B button?

Copy link
Contributor

@kimprice kimprice Oct 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, is this just setting the button to launch the games?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct, this is the code that maps which button (or buttons) launch the selected game. This is a spot where the expected behavior of keyboard and gamepad differ. With a keyboard, you would typically expect that pressing Enter would launch the game. But, in kiosk's setup, both the keyboard's Enter key and the gamepad's B button are mapped to the same functionality: they both represent the simulator's B button. In other areas of Kiosk, B is used to navigate back within the UI, like closing a dialog. It felt like a bug to be able to launch a game with the gamepad's B button. For that reason, and for consistency with the "navigate back" behavior elsewhere, I decided to remove it here.


const transitionStart = () => {
Expand Down Expand Up @@ -133,22 +139,16 @@ const GameList: React.FC<IProps> = ({}) => {
slidesPerView={1.8}
centeredSlides={true}
pagination={{ type: "fraction" }}
onSwiper={swiper => {
localSwiper.current = swiper;
}}
onSwiper={handleLocalSwiper}
allowTouchMove={true}
modules={[Pagination]}
onSlideChangeTransitionStart={() => transitionStart()}
onTouchEnd={() => onTouchEnd()}
>
{kiosk.allGames.map((game, index) => {
const gameHighScores = getHighScores(game.id);
return (
<SwiperSlide key={index}>
<GameSlide
highScores={gameHighScores}
game={game}
/>
<GameSlide game={game} />
</SwiperSlide>
);
})}
Expand Down
10 changes: 6 additions & 4 deletions kiosk/src/Components/GameSlide.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { GameData, HighScore } from "../Types";
import { AppStateContext } from "../State/AppStateContext";
import HighScoresList from "./HighScoresList";
import { GameMenu } from "./GameMenu";
import { getHighScores } from "../State";

interface IProps {
highScores: HighScore[];
game: GameData;
}
const GameSlide: React.FC<IProps> = ({ highScores, game }) => {
const GameSlide: React.FC<IProps> = ({ game }) => {
const { state: kiosk } = useContext(AppStateContext);

const handleSlideClick = (ev?: React.MouseEvent) => {
Expand All @@ -35,10 +35,12 @@ const GameSlide: React.FC<IProps> = ({ highScores, game }) => {
<div className="gameTitle">{game.name}</div>
<div className="gameDescription">{game.description}</div>
<HighScoresList
highScores={highScores}
highScores={getHighScores(game.id)}
highScoreMode={game.highScoreMode}
/>
{game.date && <div className="gameDate">{lf("Added {0}", game.date)}</div>}
{game.date && (
<div className="gameDate">{lf("Added {0}", game.date)}</div>
)}
</div>

{kiosk.selectedGameId && game.id === kiosk.selectedGameId && (
Expand Down
96 changes: 54 additions & 42 deletions kiosk/src/Services/GamepadManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,36 @@ export enum GamepadControl {
ResetButton = "ResetButton",
}

enum GamepadAxis {
None = "None",
Up = "Up",
Down = "Down",
Left = "Left",
Right = "Right",
}

const gamepadControlToPinIndex: { [key in GamepadControl]: number } = {
[GamepadControl.AButton]: configData.GamepadAButtonPin,
[GamepadControl.BButton]: configData.GamepadBButtonPin,
[GamepadControl.DPadUp]: configData.GamepadUpDownAxis,
[GamepadControl.DPadDown]: configData.GamepadUpDownAxis,
[GamepadControl.DPadLeft]: configData.GamepadLeftRightAxis,
[GamepadControl.DPadRight]: configData.GamepadLeftRightAxis,
[GamepadControl.DPadUp]: configData.GamepadDpadUpButtonPin,
[GamepadControl.DPadDown]: configData.GamepadDpadDownButtonPin,
[GamepadControl.DPadLeft]: configData.GamepadDpadLeftButtonPin,
[GamepadControl.DPadRight]: configData.GamepadDpadRightButtonPin,
[GamepadControl.MenuButton]: configData.GamepadMenuButtonPin,
[GamepadControl.EscapeButton]: configData.GamepadEscapeButtonPin,
[GamepadControl.ResetButton]: configData.GamepadResetButtonPin,
};

const gamepadControlToAxisDirection: { [key in GamepadControl]: number } = {
[GamepadControl.AButton]: 0,
[GamepadControl.BButton]: 0,
[GamepadControl.DPadUp]: -1,
[GamepadControl.DPadDown]: 1,
[GamepadControl.DPadLeft]: -1,
[GamepadControl.DPadRight]: 1,
[GamepadControl.MenuButton]: 0,
[GamepadControl.EscapeButton]: 0,
[GamepadControl.ResetButton]: 0,
const gamepadControlToAxis: { [key in GamepadControl]: GamepadAxis } = {
[GamepadControl.AButton]: GamepadAxis.None,
[GamepadControl.BButton]: GamepadAxis.None,
[GamepadControl.DPadUp]: GamepadAxis.Up,
[GamepadControl.DPadDown]: GamepadAxis.Down,
[GamepadControl.DPadLeft]: GamepadAxis.Left,
[GamepadControl.DPadRight]: GamepadAxis.Right,
[GamepadControl.MenuButton]: GamepadAxis.None,
[GamepadControl.EscapeButton]: GamepadAxis.None,
[GamepadControl.ResetButton]: GamepadAxis.None,
};

export enum ControlValue {
Expand Down Expand Up @@ -129,8 +137,7 @@ class GamepadManager {
);

this.minAxisRequired = Math.max(
configData.GamepadUpDownAxis,
configData.GamepadLeftRightAxis
...Object.values(configData.GamepadAxes).map(v => v.Pin)
);
}

Expand Down Expand Up @@ -258,6 +265,15 @@ class GamepadManager {
}
}

mergeControlValues(...values: ControlValue[]): ControlValue {
for (const value of values) {
if (value === ControlValue.Down) {
return ControlValue.Down;
}
}
return ControlValue.Up;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little confused. Do the left and right controls become an up control?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code just returns Down if any of the passed in values are Down. Otherwise it returns Up. It's a helper function that is useful when multiple gamepad buttons are mapped to the same control. For example, both left-stick-left and dpad-left represent the DPadLeft control. It either one are active, we want to represent the DPadLeft control as being Down.

}

readGamepad(gamepad: Gamepad): ControlStates {
return {
[GamepadControl.AButton]: this.readGamepadButtonValue(
Expand All @@ -268,21 +284,24 @@ class GamepadManager {
gamepad,
GamepadControl.BButton
),
[GamepadControl.DPadUp]: this.readGamepadDirectionValue(
gamepad,
GamepadControl.DPadUp
[GamepadControl.DPadUp]: this.mergeControlValues(
this.readGamepadButtonValue(gamepad, GamepadControl.DPadUp),
this.readGamepadDirectionValue(gamepad, GamepadControl.DPadUp)
),
[GamepadControl.DPadDown]: this.readGamepadDirectionValue(
gamepad,
GamepadControl.DPadDown
[GamepadControl.DPadDown]: this.mergeControlValues(
this.readGamepadButtonValue(gamepad, GamepadControl.DPadDown),
this.readGamepadDirectionValue(gamepad, GamepadControl.DPadDown)
),
[GamepadControl.DPadLeft]: this.readGamepadDirectionValue(
gamepad,
GamepadControl.DPadLeft
[GamepadControl.DPadLeft]: this.mergeControlValues(
this.readGamepadButtonValue(gamepad, GamepadControl.DPadLeft),
this.readGamepadDirectionValue(gamepad, GamepadControl.DPadLeft)
),
[GamepadControl.DPadRight]: this.readGamepadDirectionValue(
gamepad,
GamepadControl.DPadRight
[GamepadControl.DPadRight]: this.mergeControlValues(
this.readGamepadButtonValue(gamepad, GamepadControl.DPadRight),
this.readGamepadDirectionValue(
gamepad,
GamepadControl.DPadRight
)
),
[GamepadControl.MenuButton]: this.readGamepadButtonValue(
gamepad,
Expand Down Expand Up @@ -483,11 +502,8 @@ class GamepadManager {
control: GamepadControl
): ControlValue {
const pinIndex = gamepadControlToPinIndex[control];
if (pinIndex < 0 || pinIndex >= gamepad.buttons.length) {
throw new Error(
`Gamepad at index ${gamepad.index} does not have a button at pin ${pinIndex}`
);
}
if (pinIndex < 0 || pinIndex >= gamepad.buttons.length)
return ControlValue.Up;
return gamepad.buttons[pinIndex].pressed
? ControlValue.Down
: ControlValue.Up;
Expand All @@ -497,15 +513,11 @@ class GamepadManager {
gamepad: Gamepad,
control: GamepadControl
): ControlValue {
const axisIndex = gamepadControlToPinIndex[control];
const threshold =
gamepadControlToAxisDirection[control] *
configData.GamepadLeftRightThreshold;
if (axisIndex < 0 || axisIndex >= gamepad.axes.length) {
throw new Error(
`Gamepad at index ${gamepad.index} does not have an axis at index ${axisIndex}`
);
}
const axisConf = configData.GamepadAxes[gamepadControlToAxis[control]];
const axisIndex = axisConf.Pin;
if (axisIndex < 0 || axisIndex >= gamepad.axes.length)
return ControlValue.Up;
const threshold = axisConf.Sign * axisConf.Threshold;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does threshold mean?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This value represents the threshold at which a gamepad axis is considered active. Gamepad sticks, even when not touched, rarely report (0,0) as their position due to inherent noise. They're always changing and reporting small, off-center values, like (0.01, -0.003). Furthermore, small stick movements report erratic positions and aren't reliable. The threshold allows us to filter out this noise and only respond to meaningful movements.

if (threshold < 0) {
return gamepad.axes[axisIndex] <= threshold
? ControlValue.Down
Expand Down
Loading
Loading