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

Added support for gamepad #164

Merged
merged 1 commit into from
Jan 10, 2019
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
Binary file added public/img/keyboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/nes_controller.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
128 changes: 111 additions & 17 deletions src/ControlMapperRow.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,118 @@ class ControlMapperRow extends Component {
}

componentDidUpdate(prevProps, prevState) {
// Prevent setState being called repeatedly
if (prevState.waitingForKey !== 0) {
var keys = this.props.keys;
var button = this.props.button;
var playerButtons = [];
for (var key in keys) {
if (keys[key][0] === 1 && keys[key][1] === button) {
playerButtons[0] = keys[key][2];
console.log(playerButtons[0]);
} else if (keys[key][0] === 2 && keys[key][1] === button) {
playerButtons[1] = keys[key][2];
var keys = this.props.keys;
var button = this.props.button;
var playerButtons = [];
var gamepadButton;
var newButton;

for (var key in keys) {
if (keys[key][0] === 1 && keys[key][1] === button) {
playerButtons[0] = keys[key][2];
console.log(playerButtons[0]);
} else if (keys[key][0] === 2 && keys[key][1] === button) {
playerButtons[1] = keys[key][2];
}
}

var searchButton = (gamepadConfig, buttonId) => {
return gamepadConfig.buttons.filter(b => b.buttonId === buttonId)[0];
};

var searchNewButton = (prevGamepadConfig, gamepadConfig) => {
return gamepadConfig.buttons.filter(b => {
return (
!prevGamepadConfig ||
!prevGamepadConfig.buttons.some(b2 => b2.buttonId === b.buttonId)
);
})[0];
};

var waitingForKey = 0;
var waitingForKeyPlayer = 0;

var gamepadButtonName = gamepadButton => {
if (gamepadButton.type === "button") return "Btn-" + gamepadButton.code;
if (gamepadButton.type === "axis")
return "Axis-" + gamepadButton.code + " " + gamepadButton.value;
};

if (this.props.gamepadConfig && this.props.gamepadConfig.playerGamepadId) {
const playerGamepadId = this.props.gamepadConfig.playerGamepadId;
if (playerGamepadId[0]) {
playerButtons[0] = "";
gamepadButton = searchButton(
this.props.gamepadConfig.configs[playerGamepadId[0]],
button
);
newButton = searchNewButton(
prevProps.gamepadConfig.configs[playerGamepadId[0]],
this.props.gamepadConfig.configs[playerGamepadId[0]]
);
if (gamepadButton) {
playerButtons[0] = gamepadButtonName(gamepadButton);
} else {
if (newButton && newButton.buttonId === this.props.prevButton) {
if (!waitingForKey) {
waitingForKey = 1;
waitingForKeyPlayer = 1;
}
}
}
}
this.setState({
playerOneButton: playerButtons[0],
playerTwoButton: playerButtons[1],
waitingForKey: 0
});

if (playerGamepadId[1]) {
playerButtons[1] = "";
gamepadButton = searchButton(
this.props.gamepadConfig.configs[playerGamepadId[1]],
button
);
newButton = searchNewButton(
prevProps.gamepadConfig.configs[playerGamepadId[1]],
this.props.gamepadConfig.configs[playerGamepadId[1]]
);
if (gamepadButton) {
playerButtons[1] = gamepadButtonName(gamepadButton);
} else {
if (newButton && newButton.buttonId === this.props.prevButton) {
if (!waitingForKey) {
waitingForKey = 2;
waitingForKeyPlayer = 2;
}
}
}
}
}

var newState = {};

if (waitingForKey) {
this.props.handleClick([waitingForKeyPlayer, this.props.button]);
}
// Prevent setState being called repeatedly
if (
prevState.playerOneButton !== playerButtons[0] ||
prevState.playerTwoButton !== playerButtons[1]
) {
newState.playerOneButton = playerButtons[0];
newState.playerTwoButton = playerButtons[1];
}

if (waitingForKey) {
newState.waitingForKey = waitingForKey;
} else if (prevState.waitingForKey === 1) {
if (this.props.currentPromptButton !== this.props.button) {
newState.waitingForKey = 0;
}
} else if (prevState.waitingForKey === 2) {
if (this.props.currentPromptButton !== this.props.button) {
newState.waitingForKey = 0;
}
}

if (Object.keys(newState).length > 0) {
this.setState(newState);
}
}

Expand All @@ -59,7 +153,7 @@ class ControlMapperRow extends Component {
}

render() {
const waitingText = "Press key...";
const waitingText = "Press key or button...";
return (
<tr>
<td>{this.props.buttonName}</td>
Expand Down
143 changes: 137 additions & 6 deletions src/ControlsModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,104 @@ import {
import { Controller } from "jsnes";
import ControlMapperRow from "./ControlMapperRow";

const GAMEPAD_ICON = "../img/nes_controller.png";
const KEYBOARD_ICON = "../img/keyboard.png";

class ControlsModal extends Component {
constructor(props) {
super(props);
this.state = { keys: props.keys, button: undefined, modified: false };
this.state = {
gamepadConfig: props.gamepadConfig,
keys: props.keys,
button: undefined,
modified: false
};
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleGamepadButtonDown = this.handleGamepadButtonDown.bind(this);
this.listenForKey = this.listenForKey.bind(this);

this.state.gamepadConfig = this.state.gamepadConfig || {};
this.state.gamepadConfig.playerGamepadId = this.state.gamepadConfig
.playerGamepadId || [null, null];
this.state.gamepadConfig.configs = this.state.gamepadConfig.configs || {};

this.state.controllerIcon = this.state.gamepadConfig.playerGamepadId.map(
Copy link
Owner

Choose a reason for hiding this comment

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

This stuff seems to be calculated based on gamepadConfig. Perhaps it makes more sense to just be a function or something instead of being on state.

Copy link
Contributor Author

@tario tario Jan 9, 2019

Choose a reason for hiding this comment

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

I am sorry, I don't get it, it is only a default initialization to ensure gamepadConfig starts in a not null state. Show me a code example if you want

Copy link
Owner

Choose a reason for hiding this comment

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

It's only a minor thing, no worries. From a cursory look at the code, it seems like controllerIcon is just computed from gamepadConfig. So, it's not really state, but just a function which would take gamepadConfig as an argument.

I'll get this merged and then make some follow-up issues. :)

gamepadId => (gamepadId ? GAMEPAD_ICON : KEYBOARD_ICON)
);
this.state.controllerIconAlt = this.state.gamepadConfig.playerGamepadId.map(
gamepadId => (gamepadId ? "gamepad" : "keyboard")
);
this.state.currentPromptButton = -1;
}

componentWillUnmount() {
if (this.state.modified) {
this.props.setKeys(this.state.keys);
this.props.setGamepadConfig(this.state.gamepadConfig);
}
this.removeKeyListener("keydown", this.handleKeyDown);
this.removeKeyListener();
}

listenForKey(button) {
this.setState({ button });
var currentPromptButton = button[1];

this.removeKeyListener();
this.setState({ button, currentPromptButton });
this.props.promptButton(this.handleGamepadButtonDown);
document.addEventListener("keydown", this.handleKeyDown);
}

handleGamepadButtonDown(buttonInfo) {
this.removeKeyListener();

var button = this.state.button;

const playerId = button[0];
const buttonId = button[1];

const gamepadId = buttonInfo.gamepadId;
const gamepadConfig = this.state.gamepadConfig;

// link player to gamepad
const playerGamepadId = gamepadConfig.playerGamepadId.slice(0);
const newConfig = {};

playerGamepadId[playerId - 1] = gamepadId;

const rejectButtonId = b => {
return b.buttonId !== buttonId;
};

const newButton = {
code: buttonInfo.code,
type: buttonInfo.type,
buttonId: buttonId,
value: buttonInfo.value
};
newConfig[gamepadId] = {
buttons: (gamepadConfig.configs[gamepadId] || { buttons: [] }).buttons
.filter(rejectButtonId)
.concat([newButton])
};

const configs = Object.assign({}, gamepadConfig.configs, newConfig);

this.setState({
gamepadConfig: {
configs: configs,
playerGamepadId: playerGamepadId
},
currentPromptButton: -1,
controllerIcon: playerGamepadId.map(gamepadId =>
gamepadId ? GAMEPAD_ICON : KEYBOARD_ICON
),
modified: true
});
}

handleKeyDown(event) {
this.removeKeyListener();

var button = this.state.button;
var keys = this.state.keys;
var newKeys = {};
Expand All @@ -39,6 +116,11 @@ class ControlsModal extends Component {
newKeys[key] = keys[key];
}
}

const playerGamepadId = this.state.gamepadConfig.playerGamepadId.slice(0);
const playerId = button[0];
playerGamepadId[playerId - 1] = null;

this.setState({
keys: {
...newKeys,
Expand All @@ -48,12 +130,23 @@ class ControlsModal extends Component {
]
},
button: undefined,
gamepadConfig: {
configs: this.state.gamepadConfig.configs,
playerGamepadId: playerGamepadId
},
currentPromptButton: -1,
controllerIcon: playerGamepadId.map(gamepadId =>
gamepadId ? GAMEPAD_ICON : KEYBOARD_ICON
),
controllerIconAlt: playerGamepadId.map(gamepadId =>
gamepadId ? "gamepad" : "keyboard"
),
modified: true
});
document.removeEventListener("keydown", this.handleKeyDown);
}

removeKeyListener() {
this.props.promptButton(null);
document.removeEventListener("keydown", this.handleKeyDown);
}

Expand All @@ -70,58 +163,96 @@ class ControlsModal extends Component {
<thead>
<tr>
<th>Button</th>
<th>Player 1</th>
<th>Player 2</th>
<th>
Player 1
<img
className="controller-icon"
src={this.state.controllerIcon[0]}
alt={this.state.controllerIconAlt[0]}
/>
</th>
<th>
Player 2
<img
Copy link
Owner

Choose a reason for hiding this comment

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

Have you tested that this works in the built production bundle? I think you might need to import the images to make sure the calculated paths are correct: https://facebook.github.io/create-react-app/docs/adding-images-fonts-and-files

Copy link
Contributor Author

@tario tario Jan 9, 2019

Choose a reason for hiding this comment

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

Great! I alredy tested it using developer run (yarn start) and production build on my local machine. I will test it again on the netlify deploy for this PR (https://deploy-preview-164--youthful-kepler-d6ba79.netlify.com/), only to be sure

Do you need a list of covered cases ?

Copy link
Owner

Choose a reason for hiding this comment

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

Nice okay. Sounds like it works then. :)

className="controller-icon"
src={this.state.controllerIcon[1]}
alt={this.state.controllerIconAlt[1]}
/>
</th>
</tr>
</thead>
<tbody>
<ControlMapperRow
buttonName="Left"
currentPromptButton={this.state.currentPromptButton}
button={Controller.BUTTON_LEFT}
prevButton={Controller.BUTTON_SELECT}
keys={this.state.keys}
handleClick={this.listenForKey}
gamepadConfig={this.state.gamepadConfig}
/>
<ControlMapperRow
buttonName="Right"
currentPromptButton={this.state.currentPromptButton}
button={Controller.BUTTON_RIGHT}
prevButton={Controller.BUTTON_LEFT}
keys={this.state.keys}
handleClick={this.listenForKey}
gamepadConfig={this.state.gamepadConfig}
/>
<ControlMapperRow
buttonName="Up"
currentPromptButton={this.state.currentPromptButton}
button={Controller.BUTTON_UP}
prevButton={Controller.BUTTON_RIGHT}
keys={this.state.keys}
handleClick={this.listenForKey}
gamepadConfig={this.state.gamepadConfig}
/>
<ControlMapperRow
buttonName="Down"
currentPromptButton={this.state.currentPromptButton}
button={Controller.BUTTON_DOWN}
prevButton={Controller.BUTTON_UP}
keys={this.state.keys}
handleClick={this.listenForKey}
gamepadConfig={this.state.gamepadConfig}
/>
<ControlMapperRow
buttonName="A"
currentPromptButton={this.state.currentPromptButton}
button={Controller.BUTTON_A}
prevButton={Controller.BUTTON_DOWN}
keys={this.state.keys}
handleClick={this.listenForKey}
gamepadConfig={this.state.gamepadConfig}
/>
<ControlMapperRow
buttonName="B"
currentPromptButton={this.state.currentPromptButton}
button={Controller.BUTTON_B}
prevButton={Controller.BUTTON_A}
keys={this.state.keys}
handleClick={this.listenForKey}
gamepadConfig={this.state.gamepadConfig}
/>
<ControlMapperRow
buttonName="Start"
currentPromptButton={this.state.currentPromptButton}
button={Controller.BUTTON_START}
prevButton={Controller.BUTTON_B}
keys={this.state.keys}
handleClick={this.listenForKey}
gamepadConfig={this.state.gamepadConfig}
/>
<ControlMapperRow
buttonName="Select"
currentPromptButton={this.state.currentPromptButton}
button={Controller.BUTTON_SELECT}
prevButton={Controller.BUTTON_START}
keys={this.state.keys}
handleClick={this.listenForKey}
gamepadConfig={this.state.gamepadConfig}
/>
</tbody>
</Table>
Expand Down
Loading