loading...
Cover image for Desarrollando un mini-Rock Band con HTML y JavaScript

Desarrollando un mini-Rock Band con HTML y JavaScript

alvaromontoro profile image Alvaro Montoro ・11 min read

Ésta es una versión reducida y en español del taller "Rocking the Gamepad API" que realicé como parte d ela conferenciaCodeland:Distributed.

Si no tienes una batería de Rock Band, puedes usar este Gamepad Virtual para desarrollar el juego.

This post is also available in English.

El siguiente video cuenta con una explicación de la Gamepad API al mismo tiempo que se desarrolla el juego. Si quieres ir al código directamente, salta el video y ve a la "transcripción" de debajo.

El video dura 40 minutos porque incluye detalles sobre la Gamepad API

En este post vamos a aprender cómo desarrollar una versión simple de un juego al estilo de Rock Band o Guitar Hero, usando solamente HTML estándar y JavaScript sin ninguna biblioteca.

Será un juego más bien pequeño (¡se puede desarrollar en 10 minutos!) pero es divertido y va a funcionar con la batería (tambores) de Rock Band conectada a la computadora. En concreto, voy a usar la batería de Harmonix para PlayStation 3 que venía con el juego original de Rock Band.

Vamos a empezar mostrando una captura del resultado final:

Screenshot of rock band looking game

Captura del juego que desarrollamos durante el taller en CodeLand:Distributed

Va a ser un post cortito, no vamos a entrar en detalle con la API de Gamepad –algo que sí hicimos durante el taller– y nos vamos a limitar a las partes clave necesarias para crear el juego.

¡Vamos al código!

Primero, necesitamos saber cuando se ha conectado un controlador (gamepad en inglés), y para eso leemos los eventos gamepadconnected y gamepaddisconnected respectivamente:

// variable para guardar los IDs de los controladores
const gamepads = {};

// función que se llama cuando se conecte un controlador/gamepad
window.addEventListener("gamepadconnected", function(e) {
  console.info("¡Controlador conectado!");
  gamepads[e.gamepad.index] = true;
});

// función que se llama cuando se desconecte un controlador
window.addEventListener("gamepaddisconnected", function(e) {
  console.info("Controlador desconectado.");
  delete gamepads[e.gamepad.index];
});

Ahora vamos a desarrollar el código que contiene la parte más importante: el método que va a comprobar si hubo cambios en el controlador de juego. Para ello vamos a crear una nueva función que se llamará cuando el gamepad se conecte:

// función que se llama continuamente para leer los valores
function readGamepadValues() {
  // lee los índices de los controladores conectados
  const indexes = Object.keys(gamepads);

  // si sigue habiendo controladores conectados, volver a llamar la función
  if (indexes.length > 0) {
    window.requestAnimationFrame(readGamepadValues);
  }
}

La función está vacía y se está llamando a sí misma continuamente usando window.requestAnimationFrame. Utilizamos ese método porque es más fiable que setTimeout o setInterval y sabemos que se va a llamar justo antes del refresco de pantalla (muy conveniente para lo que queremos hacer).

Como parte del juego sólo vamos a tener una batería conectada al ordenador, pero de todos modos vamos a atravesar la lista de controladores en lugar de acceder directamente al que esté conectado (puede ser útil si luego queremos extender el juego para añadir una opción de multijugador.)

Mientras atravemos la lista de controladores, vamos a leer los botones de cada uno de ellos. Los necesitaremos en un momento:

function readGamepadValues() {
  const indexes = Object.keys(gamepads);
  // lee los controladores conectados al navegador
  const connectedGamepads = navigator.getGamepads();

  // atraviesa la lista de controladores
  for (let x = 0; x < indexes.length; x++) {
    // lee los botones del controlador
    const buttons = connectedGamepads[indexes[x]].buttons;
  }

  if (indexes.length > 0) {
    window.requestAnimationFrame(readGamepadValues);
  }
}

// ...

window.addEventListener("gamepadconnected", function(e) {
  console.info("¡Controlador conectado!");
  // lee los valores al iniciar
  readValues();
});

Ahora que tenemos la lista de botones, el siguiente paso es atravesar esa lista para comprobar qué botones están pulsados.

Podríamos hacerlo directamente en la misma función, pero es conveniente ponerlo en una función aparte para facilitar el desarrollo un poco más adelante. Así que creamos una nueva función que se llama al pulsar el botón:

// función que se llama al pulsar un botón
function buttonPressed(id) {
  console.log(`El botón ${id} fue pulsado`);
}

function readGamepadValues() {

  // ...

  for (let x = 0; x < indexes.length; x++) {
    const buttons = connectedGamepads[indexes[x]].buttons;

    // atraviesa la lista de botones
    for (let y = 0; y < buttons.length; y++) {
      // llama la función cuando se pulse el botón
      if (buttons[y].pressed) {
        buttonPressed(y);
      }
    }
  }

  // ...
}

Con eso ya hemos hecho una parte importante del desarrollo porque sabemos cuándo se ha pulsado un botón. Con eso tenemos casi la mitad del motor del juego creado. Faltaría generar una secuencia aleatoria de botones a pulsar.

...Pero antes de eso hay un problema que resolver...

Si has estado programando siguiendo el post, te habrás dado cuenta que cuando se pulsa un botón, la función buttonPressed se está llamando varias veces y no una sola. Eso ocurre porque aunque pulses el botón muy rápido, casi siempre el botón va a estar pulsado por más tiempo que el ciclo de refresco de la pantalla (16ms aprox.) por lo que las funciones de leer valores y botón pulsado se llaman más de una vez.

Para evitar este comportamiento, vamos a crear una nueva variable para guardar el estado de los botones. Y vamos a llamar la función buttonPressed sólo si el estado anterior del botón estaba como "no pulsado."

// variable para el estado de los botones
const stateButtons = {};

// ...


function readGamepadValues() {

  // ...

    for (let y = 0; y < buttons.length; y++) {
      // si el botón se pulsó
      if (buttons[y].pressed) {
        // ...y su estado anterior era no pulsado
        if (!stateButtons[y]) {
          // se marca el estado de botón como pulsado
          stateButtons[y] = true;
          // y se llama a la función de botón pulsado
          buttonPressed(y);
        }
      // si el botón NO está pulsado
      } else {
        // se quita su estado de botón pulsado
        delete stateButtons[y];
      }
    }

  // ...
}

Con eso hemos terminado el código que controla la batería. Toda la lógica que falta está relacionada con el juego y no con el controlador.

Vamos a continuar entonces seleccionando al azar un botón a pulsar. Nuestra batería funciona con los botones 0-3, lo que va a hacer nuestras vidas muy fáciles.

Nota: tu batería o controlador puede tener los botones en otro orden. La batería Harmonix que tenemos conectada al ordenador tiene la siguiente secuencia: rojo (botón 2), amarillo (3), azul (0) y verde (1). Vamos a desarrollar el juego en consecuencia, pero tienes que asegurarte que los botones están en el mismo orden en tu batería/guitarra/controlador.

Generar un número aleatorio es sencillo con Math.random(). Solo tenemos que asegurarnos que lo generamos en el momento adecuado:

  • Al comenzar el juego
  • Cuando el jugador acierte la nota/botón correcta

El código es el siguiente:

// variable que indica qué botón debe pulsarse ahora
let activeButton = 0;

// función que genera el botón a pulsar
function generateNewRandomActive() {
  // generamos un número entre 0 y 3 (ambos incluidos)
  activeButton = Math.floor(Math.random() * 4);
}

function buttonPressed(id) {
  // si el botón pulsado es el mismo a pulsar
  if (activeButton === id) {
    // se genera un nuevo número aleatorio
    generateNewRandomActive();
  }
}

// ...

window.addEventListener("gamepadconnected", function(e) {
  console.info("¡Controlador conectado!");
  gamepads[e.gamepad.index] = true;
  generateNewRandomActive();
  readValues();
});

Pero, ¿qué es un juego sin puntos? Vamos a continuar añadiendo una opción de puntos y también la racha de notas pulsadas correctamente.

// variables para puntos y racha
let points = 0;
let streak = 0;

// ...

function buttonPressed(id) {
  if (activeButton === id) {
    // si la nota es correcta, añadir los puntos y racha
    streak++;
    points++;
    generateNewRandomActive();
  } else {
    // si la nota no es correcta, la racha vuelve a 0
    streak = 0;
  }
}

Con eso ya tenemos todo el juego hecho:

  • Usamos la API de Gamepad para leer los botones en la batería
  • Generamos un botón a pulsar
  • Detectamos cuando el botón fue pulsado correctamente
  • Cuando se pulsa correctamente, generamos un nuevo botón a pulsar
  • Mantenemos un registro de los puntos y la racha

Pero falta algo muy importante. ¡Los jugadores no saben qué botón tienen que pulsar o cuántos puntos tienen! Hasta ahora sólo hemos hecho JavaScript y no mostramos nada, así que los jugadores no ven nada.

Éste es el momento en el que HTML y CSS vienen al rescate.

Vamos a empezar añadiendo todas las partes necesarias en HTML: puntos, racha, y una batería ordenada como en el controlador físico.

<div id="points"></div>
<div id="streak"></div>

<div id="drumset">
  <!-- recuerda que mi batería tiene la secuencia 2-3-0-1, la tuya puede ser diferente -->
  <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>

Y vamos a darle estilos a la batería y los tambores:

/* ponemos la batería en la parte inferior */
#drumset {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  text-align: center;
}

/* cada tambor va a ser redondeado con un fondo gris */
.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;
}

/* hacer cada tambor de un color diferente (recuerda 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;
}

Ahora la batería se ve así:

Captura de pantalla de la batería en orden: rojo, amarillo, azul y verde

Los dos tambores centrales están algo más altos como en el controlador físico

En cuanto a los puntos y la racha, simplemente vamos a cambiar su tamaño y a posicionarlos dentro de la página:

/* posiciona el texto y le da un resaltado/borde */
#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;
}

/* la racha se posiciona más centrada en la pantalla */
#streak {
  top: 33vmin;
  right: 50vw;
  transform: translate(50%, 0);
  font-size: 12vmin;
  text-align: center;
}

/* si la racha no está vacía se muestra el mensaje "Racha: " */
#streak:not(:empty)::before {
  content: "Racha: ";
}

La última parte para completar el juego es conectar el JavaScript con el HTML/CSS, para que la pantalla muestre los valores de la lógica interna del juego.

Para los puntos y la racha, esto se puede hacer en la función generateNewRandomActive(). Recuerda que se llama la principio del juego y cada vez que se pulsaba un botón correctamente:

function generateNewRandomActive() {
  activeButton = Math.floor(Math.random() * 4);
  // muestra los puntos y racha por pantalla
  document.querySelector("#points").textContent = points;
  document.querySelector("#streak").textContent = streak;
}

En cuanto al botón a pulsar, no basta con mostrar el ID por pantalla porque el jugador no saber qué botón se corresponde con qué tambor. Entonces lo que vamos a hacer es cambiar la clase de la batería con JS y luego estilizar el botón correspondiente via CSS (dándole un tono semitransparente):

function generateNewRandomActive() {
  activeButton = Math.floor(Math.random() * 4);
  document.querySelector("#points").textContent = points;
  document.querySelector("#streak").textContent = streak;
  // añade la clase a la batería indicando que tambor está activo
  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; }

Y con eso hemos completado el juego. Cada vez que pulsamos en el tambor correcto, uno nuevo es seleccionado al azar y se actualizan los puntos uy la racha.

Pero seamos realistas. Aunque el juego funciona, es demasiado simplón... le hace falta algo de magia:

  • La pantalla está casi toda en blanco.
  • La fuente es Times New Roman... no le pega al rock'n'roll.

El problema de la fuente se puede solucionar seleccionando una más apropiada en algún sitio como Google Fonts:

@import url('https://fonts.googleapis.com/css2?family=New+Rocker&display=swap');

* {
  font-family: 'New Rocker', sans-serif;  
}

Y para la traca, vamos a quitar todo el blanco y hacer que se parezca más al juego de verdad, vamos a poner un video de fondo. Y con eso matamos dos pájaros de un tiro: añadimos dinamismo ¡y música!

Para ello, busca un video en Youtube (o cualquier otro proveedor de videos), pulsa en el botón de "Compartir" (Share) y selecciona "Embeber" (Embed). Luego copia el código del <iframe> y pégalo al principio del 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>

Asegúrate de que el iframe del video tiene un tamaño del 100% y añade ?autoplay=1&controls=0 a la URL para que no se muestren los controles y que el video comience automáticamente.

Truco: en lugar de exportar un video, exporta una playlist. Así los videos se reproducirán uno detrás de otro sin necesidad de cambiar nada.

Y haz que el contenedor del video ocupe toda la pantalla:

#video {
  position: absolute;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
}

Ahora sí que hemos terminado y el juego se ve mucho mejor:

Captura de pantalla del juego con el video "Ignorance" de Paramore de fondo

Puede no ser maravilloso, pero es resultón y no está mal para un juego que son sólo 150 líneas de código (16 de HTML + 73 de CSS + 61 de JS) y que no necesita ninguna biblioteca o plugin, sólo usando JavaScript estándar.

Si quieres explorar el código en más detalle el juego está disponible en Codepen (necesitarás un controlador para jugar esta versión):

El juego que desarrollamos durante el taller en CodeLand:Distributed

Obviamente, este juego no es tan complejo como los originales de Rock Band o Guitar Hero, pero es interesante por su sencillez de desarrollo (10 minutos con un solo programador).

Es ideal para niñas y niños que aún no pueden jugar al juego de verdad (a mis hijos les encanta esta versión) y también da mucho juego para extenderlo y mejorarlo. Se puede...

  • añadir multiplicadores/combos
  • añadir mensajes de ánimo después de rachas de 10+, 20+, 30+...
  • integrar con la API de Youtube para detectar el final del video y mostrar los resultados
  • combinar con otra API/plugin para detectar el ritmo/volumen de la música y hacerlo más rápido o más lento
  • añadir un fichero JSON con las notas y hacer que caigan de la parte superior, como en el juego original...

Como te habrás dado cuenta, muchos de estos cambios no requirern mucho tiempo, y pueden hacer que el juego se parezca más al de verdad, mejorando la experiencia de los jugadores. Sólo hay que ponerse a desarrollar.

¡Disfruta programando!


Este post se basa principalmente en el manejo de botones en la API de Gamepad. En otro post veremos como usar los botones direccionales/joystick para crear un juego en plan Dance Dance Revolution.

Posted on by:

alvaromontoro profile

Alvaro Montoro

@alvaromontoro

Full-Stack Software Engineer, Mobile Developer, Web technologies enthusiast. CSS aficionado. Active moderator in Stack Overflow en Español. Husband, Father, Longhorn 🤘

Discussion

pic
Editor guide