Develop a Simple Rock Band Game With HTML and JavaScript | by Alvaro Montoro | Jun, 2022

Use the Gamepad API to build a music game for all levels

In this article, we will learn how to develop a simple version of a Rock Band- or Guitar Hero-styled game using standard HTML and vanilla JavaScript. It is a short version of the workshop “Rocking the Gamepad API” that we did as part of the Codeland: Distributed conference.

It will be a small game (it takes just 10 minutes!), but it has a cool factor: it will work with the Rock Band drumset connected to the computer. In particular, we will use the Harmonix Drumset for PlayStation 3, but you can use a different controller.

Let’s begin by showing the result:

Screenshot of rock band looking game
Screenshot of the game we developed during the workshop

The article will be short, though. Therefore, we will not dive deep into the Gamepad API -something we did during the workshop–and limit its use to the essential parts 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 crucial part of the game: the method that checks if something changed in the gamepad. To do so, we will create a new function that the browser will call 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 thansetTimeout or setInterval and we know that it is going to be called right before the screen refreshes (which is convenient).

We will have a single gamepad/drumset connected to the computer, but we will traverse the list instead of directly accessing the unique identifier. We do that for consistency, and in case more than one gamepad is connected (which could be helpful if you 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 traverse that list to check if any of them is 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 to handle the button-pressed event:

// 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 detect 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 push, but we need to handle one issue before that.

If you have been coding along until here, you will have noticed that the buttonPressed function is called multiple times when you press a button. 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 will add a new variable that will save the state of the buttons and only call buttonPressed if the previous state of the button is “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 all of the drumset-related code. So most of the remaining logic will not 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 will 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 need to make sure that we call it at the right moments:

  • At the beginning of the game.
  • After handling a correct button-pressed event.

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 user pressed the target button.
  • If it was, we generate a new target button.
  • We keep track of points and streaks.

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 is time for HTML and CSS to come to the rescue.

Let’s start by adding all the critical 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>

Then we style 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:

Screenshot of the drums on the screen: red, yellow, blue, green
Lifted the drums in the middle to match the real PS3 drumset

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 beforeScriptS the game is connecting the Java with the HTML/CS, so the screen shows the values ​​from the game.

For the points and streak, we can do it 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, etc.

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

Correcting the font issue is straightforward. We need to pick 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 will put an actual video as the game background.

To do that, browse for a video on Youtube or another 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;
}

With this last touch, we finished, and the game looks nicer:

Screenshot of the game using the video “Ignorance” by Paramore

Not bad for a game with 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):

The actual game that we developed during the workshop

Leave a Comment