This is an abridged version of the workshop "Rocking the Gamepad API" that we did as part of Codeland:Distributed conference.
If you don't have a drumkit, you can use this virtual gamepad to develop the game.
In this post, we are going to learn how to develop a simple version of a Rock Band/Guitar Hero styled game, using standard HTML and vanilla JavaScript.
It will be a small game (it's just 10 minutes!), but it has a cool factor: it will work with the Rock Band drumset connected to the computer. In particular, we are going to use the Harmonix Drumset for PlayStation 3, but you can use a different controller.
Let's begin by showing the end result:
This is going to be a short post though. We are not going to dive deep into the Gamepad API –something that we did during the workshop–, and we will limit its use to the key parts that we need.
Let's start coding!
First, we need to read the connection/disconnection events and save the unique identifier of the connected gamepad:
// variable to hold the gamepads unique identifiers
const gamepads = {};
// function to be called when a gamepad is connected
window.addEventListener("gamepadconnected", function(e) {
console.info("Gamepad connected!");
gamepads[e.gamepad.index] = true;
});
// listener to be called when a gamepad is disconnected
window.addEventListener("gamepaddisconnected", function(e) {
console.info("Gamepad disconnected");
delete gamepads[e.gamepad.index];
});
Now we will develop the code that will contain the most important part of the game: the method that checks if something changed in the gamepad. To do so, we will create a new function that will be called once the gamepad is connected:
// function to be called continuously to read the gamepad values
function readGamepadValues() {
// read the indexes of the connected gamepads
const indexes = Object.keys(gamepads);
// if there are gamepads connected, keep reading their values
if (indexes.length > 0) {
window.requestAnimationFrame(readGamepadValues);
}
}
Right now that function is empty, and it's calling itself continuously using window.requestAnimationFrame
. We use that method because it is more reliable than say setTimeout
or setInterval
, and we know that it is going to be called right before the screen refreshes (which is convenient).
We are going to have a single gamepad/drumset connected to the computer, but we are going to traverse the list instead of directly accessing the unique identifier. We do that for consistency and just in case that more than one gamepad is connected (which could be useful if you are going to develop a multiplayer version.)
While we traverse the list of gamepads, we will read their buttons, which we will need to access later:
function readGamepadValues() {
const indexes = Object.keys(gamepads);
// read the gamepads connected to the browser
const connectedGamepads = navigator.getGamepads();
// traverse the list of gamepads reading the ones connected to this browser
for (let x = 0; x < indexes.length; x++) {
// read the gamepad buttons
const buttons = connectedGamepads[indexes[x]].buttons;
}
if (indexes.length > 0) {
window.requestAnimationFrame(readGamepadValues);
}
}
// ...
window.addEventListener("gamepadconnected", function(e) {
console.info("Gamepad connected!");
// read the values while the gamepad is connected
readValues();
});
Now that we have the list of buttons, the next step is to also traverse that list to check if any of them are pressed.
We could do it in the same readValues
function, but it could be convenient to have it separate for later expansion, so we will create a new function that will be called when a button is pressed:
// function to be called when a button is pressed
function buttonPressed(id) {
console.log(`Button ${id} was pressed`);
}
function readGamepadValues() {
// ...
for (let x = 0; x < indexes.length; x++) {
const buttons = connectedGamepads[indexes[x]].buttons;
// traverse the list of buttons
for (let y = 0; y < buttons.length; y++) {
// call the new function when a button is pressed
if (buttons[y].pressed) {
buttonPressed(y);
}
}
}
// ...
}
We are already in a nice place because we are detecting when each button is pressed. With that, we have half the (simple) game engine built. We still need to generate random sequences of notes/buttons to press; but before that, we need to handle one issue.
If you have been coding along until here, you will have noticed that when you press a button, the buttonPressed
function is called multiple times. This happens because no matter how fast we try to do it, the button is down for longer than 16ms, which makes the button down more than one cycle of the screen refresh, which ends up with readValues
and buttonPressed
being called more than once.
To avoid that behavior, we are going to add a new variable that will save the state of the buttons. And only call buttonPressed
if the previous state of the button was not pressed.
// variable that will hold the state of the pressed buttons
const stateButtons = {};
// ...
function readGamepadValues() {
// ...
for (let y = 0; y < buttons.length; y++) {
// if the button is pressed
if (buttons[y].pressed) {
// ...and its previous state was not pressed
if (!stateButtons[y]) {
// we mark it as pressed
stateButtons[y] = true;
// and call the buttonPressed function
buttonPressed(y);
}
// if the button is NOT pressed
} else {
// delete the pressed state
delete stateButtons[y];
}
}
// ...
}
We are handling the drumset completely already. Most of the remaining logic is not going to be related to the gamepad management, but to the game itself.
First, let's generate a random button to press. We are using the drumset and the buttons are 0-3, which is going to make our lives easier.
Note: your drumset or gamepad may have the buttons in a different order. The Harmonix that we have connected to the browser has the following sequence: Red (button 2), Yellow (3), Blue (0), and Green (1). We will develop accordingly, make sure that this works with your drumset/guitar/controller.
Generating a random number is simple with Math.random()
. We just need to make sure that we call it at the right moments:
- At the beginning of the game
- When a button was pressed correctly
The code for that is as follows:
// variable to hold which button is active (to be pressed next)
let activeButton = 0;
// function that generates a new random button
function generateNewRandomActive() {
// generate a new number between 0 and 3 (both included)
activeButton = Math.floor(Math.random() * 4);
}
function buttonPressed(id) {
// if the pressed button is the same as the active one
if (activeButton === id) {
// generate a new random button to press
generateNewRandomActive();
}
}
// ...
window.addEventListener("gamepadconnected", function(e) {
console.info("Gamepad connected!");
gamepads[e.gamepad.index] = true;
generateNewRandomActive();
readValues();
});
Now, what is a game without points? Let's continue by adding points and keeping track of the streak of notes played correctly.
// variable for the points and streak
let points = 0;
let streak = 0;
// ...
function buttonPressed(id) {
if (activeButton === id) {
// add points
streak++;
points++;
generateNewRandomActive();
} else {
streak = 0;
}
}
With that, we have the whole game done:
- Using the Gamepad API we read the hits in the drum
- We generate a target button
- We detect if the target button was pressed
- When it is pressed correctly, we generate a new target button
- We keep track of points and streak
But there's something big missing! Players cannot see the points or which is the button to press... So far we have only done JavaScript, so players cannot see anything at all!
It's is time for HTML and CSS to come to the rescue.
Let's start by adding all the key parts to the HTML: points, streak, and a set of drums.
<div id="points"></div>
<div id="streak"></div>
<div id="drumset">
<!-- remember our drumset is sorted 2-3-0-1, it may be different for you -->
<div class="drum" id="drum-2"></div>
<div class="drum" id="drum-3"></div>
<div class="drum" id="drum-0"></div>
<div class="drum" id="drum-1"></div>
</div>
Let's start by styling the drums:
/* set the drumset at the bottom of the page */
#drumset {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
text-align: center;
}
/* make gray drums rounded with a darker border */
.drum {
width: 20vmin;
height: 20vmin;
background: #ccc;
box-sizing: border-box;
border: 1vmin solid #333;
border-radius: 50%;
position: relative;
display: inline-block;
margin-bottom: 5vmin;
}
/* make each drum of its respective color (remember 2-3-0-1) */
#drum-0 {
box-shadow: inset 0 0 0 2vmin blue;
top: -5vmin;
}
#drum-1 {
box-shadow: inset 0 0 0 2vmin green;
}
#drum-2 {
box-shadow: inset 0 0 0 2vmin red;
}
#drum-3 {
box-shadow: inset 0 0 0 2vmin yellow;
top: -5vmin;
}
The drums now look like this:
As for the points and streak values, we are simply going to position them within the page:
/* position the text and add a border to highlight it */
#points, #streak {
position: absolute;
top: 5vmin;
right: 5vmin;
font-size: 18vmin;
color: #fff;
text-shadow: 0 -1px #000, 1px -1px #000, 1px 0 #000,
1px 1px #000, 0 1px #000, -1px 1px #000,
-1px 0 #000, -1px -1px #000;
}
/* the streak will go in the middle of the screen */
#streak {
top: 33vmin;
right: 50vw;
transform: translate(50%, 0);
font-size: 12vmin;
text-align: center;
}
/* if the streak is not empty, add the word "Streak" before */
#streak:not(:empty)::before {
content: "Streak: ";
}
The last part to complete the game is connecting the JavaScript with the HTML/CSS, so the screen shows the values from the game.
For the points and streak, this can be done in the generateNewRandomActive()
function. Remember it was called at the beginning of the game and every time that a correct button is pressed:
function generateNewRandomActive() {
activeButton = Math.floor(Math.random() * 4);
// show the points and streak on the screen
document.querySelector("#points").textContent = points;
document.querySelector("#streak").textContent = streak;
}
As for which button is the next one to hit, we are going to do it by adding a class to the drumset via JS, and styling the corresponding button using CSS (setting a semitransparent version of the background to the drum):
function generateNewRandomActive() {
activeButton = Math.floor(Math.random() * 4);
document.querySelector("#points").textContent = points;
document.querySelector("#streak").textContent = streak;
// add the activeButton class to the drumset
document.querySelector("#drumset").className = `drum-${activeButton}`;
}
#drumset.drum-0 #drum-0 { background: #00f8; }
#drumset.drum-1 #drum-1 { background: #0f08; }
#drumset.drum-2 #drum-2 { background: #f008; }
#drumset.drum-3 #drum-3 { background: #ff08; }
And with that, we have completed the game. We hit the right drum, a new random drum is selected, we get to see the points and the streak...:
But let's be realistic. The game works, but it is too simple. It is missing some pizzazz:
- The screen looks mostly white
- The font is Times New Roman... not much rock'n'roll there
The font issue can be easily corrected by picking a more appropriate font somewhere like Google Fonts:
@import url('https://fonts.googleapis.com/css2?family=New+Rocker&display=swap');
* {
font-family: 'New Rocker', sans-serif;
}
And finally, the cherry top. To remove all the white color and make it look more like the game, we are going to put an actual video as the game background.
To do that, browse for a video on Youtube or other video service, click on the "Share" button and select "Embed." Copy the <iframe>
code and paste it at the beginning of the HTML:
<div id="video">
<iframe width="100%" height="100%" src="https://www.youtube.com/embed/OH9A6tn_P6g?controls=0&autoplay=1" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>
Make sure to adjust the video iframe size to 100%, and add ?autoplay=1&controls=0
to the video, so the controls won't be displayed, and the video will automatically start playing.
Trick: instead of using a video, you could add a playlist. Then the videos will follow one another.
And make the video container occupy the whole screen:
#video {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
}
Now we are done, and the game looks nicer:
Not bad for a game that is only 150 lines of code (16 HTML + 73 CSS + 61 JS) and that doesn't use any library, just standard and vanilla JavaScript.
If you want to explore the code, the game is on Codepen (you will need a gamepad to play this version):
This game is not as complex as the original Rock Band and Guitar Hero games, but it is definitely interesting for something developed by one person in 10 minutes.
It is ideal for children that cannot play the real game yet (my kids love this version), and it has a lot of room for extension and improvements:
- Add a booster/combo multiplier
- Add encouragement messages after 10+, 20+, 30+ streaks
- Integrate it with the Youtube API to detect the end of the song/video and show stats
- Combine it with other APIs/plugins to detect when the music is louder to make it faster-paced
- Add a JSON file with notes and times so notes fall from the top at the right time...
Many of those improvements wouldn't take much time, and they would make the game a lot more like the real game.
Enjoy coding!
This post mainly focused on managing the buttons of the Gamepad API; for the next post, we'll see how to create a Dance Dance Revolution styled game using the joystick/navigational buttons. Stay tuned.