DEV Community

Cover image for Une musique de jeu évolutive grâce à JavaScript
Bastien Calou
Bastien Calou

Posted on

Une musique de jeu évolutive grâce à JavaScript

Le week-end dernier, j'ai eu le plaisir de composer la musique de Blobby Zombie, un jeu créé en 48h seulement par mon ami Simon et son camarade Pierre-Yves, lors d'une Game Jam organisée par Hitbox Makers.

Concevoir et coder un jeu en 48h n'étant visiblement pas assez difficile, ils ont créé un jeu multijoueur en ligne fonctionnel. Bravo à eux !

Pour jouer, c'est par ici !

Il faut être plusieurs, chacun sur un ordinateur, et tout le monde partage la même partie. Les règles et commandes sont disponibles ici.

De mon côté, ces deux jours se sont divisés ainsi :

  • 7h le premier jour pour la composition de la bande-son
  • 7h le deuxième jour pour la "programmation musicale"

La musique

Le jeu repose sur un mécanisme de dernier survivant. Il va donc falloir monter en pression progressivement pour que ruissellent les gouttes de sueur des joueuses et joueurs.

Avec un brief aussi complet que les trois mots "arcade", "horreur" et "fun", je me lance.

Anticipant sur la programmation à venir, j'utilise une structure la plus simple possible : une boucle de 4 accords. À chaque nouveau cycle, un instrument se rajoute.

Voici un schéma, c'est important pour la suite, et j'aime les schémas.

Les instruments s'empilent de cycle en cycle pour faire monter la pression.

Mais comment accommoder cette montée en pression de plusieurs minutes à une partie qui pourrait se dérouler beaucoup plus vite ?

La programmation

Commençons par une adaptation simple : j'avais choisi 130 BPM pour la bande son. Passer à 128 BPM est un changement quasi-inaudible qui me permet en revanche d'obtenir des cycles de 15 secondes exactement, ce qui sera bien plus sympathique pour coder et déboguer. C'est toujours ça de pris.

Bon, il nous faut un moyen de sauter de cycle en cycle, par exemple lorsqu'un nouveau joueur se transforme en zombie.

Passer d'une section à l'autre permet d'adapter la situation à l'évolution du jeu.

Puisque mes cycles font 15 secondes, c'est assez facile à calculer.

Si je suis au cycle 2 et que je souhaite avancer au cycle 3, je peux calculer qu'il faut avancer à 30 secondes dans le morceau (le cycle 1 commençant à 0 seconde, et le cycle 2 à 15 secondes).

C'est exactement ce que fait cette première démo. À chaque fois que vous cliquerez sur le bouton "Sauter", vous serez emmené au début de la section suivante :

Fludifier les sauts de cycles

Si vous jouez un peu avec, vous pouvez probablement entendre que le résultat n'est pas très satisfaisant.

Il y a deux raison à cela :

  • sauter au début du cycle interrompt le rythme que vous pouvez entendre dès le début et tout au long du morceau (cette note unique et répétitive)
  • sauter au début du cycle suivant interrompt la progression d'accords et la fait recommencer au début, ce qui n'est pas du tout naturel

La progression d'accords, c'est simplement l'enchaînement des 4 accords qui donne sa structure au morceau. On entend cette progression à partir du deuxième cycle.

Voici un cycle au complet : 4 accords qui font peur s'enchaînent sur 8 mesures.

On va simplifier un peu (beaucoup) le nom des accords et les appeler C, G, C et F (do, sol, do et fa).

On peut ainsi mettre à jour notre schéma, pour représenter cette progression à partir du deuxième cycle :

À l'exception de la note unique à la basse et de la batterie qui est uniquement rythmique, chaque nouveau cycle se base sur la même progression d'accords.

Le problème, donc, c'est que si nous passons du milieu du cycle 3 au début du cycle 4, par exemple, nous risquons de faire ça :

On saute du deuxième accord de la section 3 vers le premier accord de la section 4.

Patatra, la progression d'accords est cassée. Alors que nous nous préparions à entendre C puis F pour finir le cycle 3, nous reprenons la progression au départ : C, puis G à nouveau ! Et si on enchaîne vite les cycles, on entend quasiment que le premier accord en permanence.

Or le cerveau est très doué pour s'habituer à une progression d'accords bien spécifique, et toute déviation est troublante pour l'auditeur (ce que certains compositeurs peuvent utiliser à leur avantage, mais c'est une autre histoire).

Pour remédier à cette violation rythmique et harmonique intolérable à l'oreille, le remède est simple : il ne faut pas aller au début du cycle suivant, mais à l'instant du cycle suivant qui correspond à l'instant actuellement joué.

Image description

Cette fois, on part du deuxième accord du cycle pour arriver sur le deuxième accord du suivant.

Techniquement, c'est presque plus simple que la première version : au lieu de calculer le début de la section suivante, nous allons ajouter 15 secondes — la durée d'un cycle — à la position de lecture.

Par exemple, si la musique a commencé depuis 5 secondes, nous pouvons sauter jusqu'à 5 + 15 = 20 secondes (alors que le début du cycle 2 est à 15 secondes).

Essayez et comparez. Le résultat vous semble-t-il moins abrupt ?

Alors, attention, j'ai dit que les transitions étaient moins abruptes, pas parfaites ! Selon le moment où vous avancez, l'arrivée de tel ou tel instrument peut tout de même sauter aux oreilles. Mais la structure rythmique et harmonique est sauve, et c'est déjà beaucoup.

Masquer la transition

Pour améliorer encore l'effet, voici la botte secrète : un bon gros son bien énergique, qui va venir déguiser notre saut dans le morceau, en plus d'annoncer à tous les joueurs qu'il vient d'y avoir du grabuge.

Ce son, c'est ce que j'appelle le hit. En jouant le hit au bon moment, ce dernier occupe tout l'espace sonore et va permettre aux sauts de s'effectuer "discrètement". Tous les coups sont permis...

Voici le résultat.

Ce n'est pas encore parfait, mais compte tenu du timing serré, c'est déjà pas mal !

Harmoniser le hit

Finissons avec une petite touche cosmétique.

Actuellement, le hit est toujours le même son. Ce ne sont pas n'importe quelles notes qui sont jouées : elles correspondent précisément à celle du premier accord de la progression.

Autrement dit, elles sonnent très bien sur le premier accord.

Sur les autres accords, le rendu n'est pas choquant, car le hit est un élément bien séparé du reste de la musique. Mais qu'est ce que ça donnerait si le hit était harmonisé avec l'accord actuellement joué ?

Pour cela, j'ai exporté 4 hits différents. Ensuite, il suffit de faire correspondre le hit avec l'accord courant. Si on est dans le dernier quart d'un cycle, c'est qu'il faut jouer le hit qui correspond au dernier accord.

C'est parti !

Cette amélioration passe sans doute inaperçue, c'est d'ailleurs son but, mais je pense qu'elle rajoute une petite couche de satisfaction auditive à l'ensemble.

Musique + code = <3

Voici un court extrait du jeu (le dernier saut est assez brusque, mais c'est comme ça !) :

C'est la première fois que je combine ma passion pour la musique et mes connaissances en programmation, et en seulement 48h j'ai pu voir à quel point les possibilités étaient grandes.

C'est l'occasion de recommander une excellente chaîne YouTube : 8-bit Music Theory. Je ne comprends pas la moitié de ce qu'il raconte, mais ses analyses de bande-sons vidéoludiques me fascinent et m'ont fortement influencé pour cette petite expérience.

Top comments (4)

Collapse
 
bcalou profile image
Bastien Calou

J'ai volontairement écarté toute ligne de JavaScript du contenu de l'article, mais un mot tout de même !

Manipuler des audio en JS est très direct :

music = new Audio("music.mp3"); // Charger un fichier
music.volume = 0.5; // Régler le volume
music.play(); // Lancer la lecture
music.stop(); // Arrêter la lecture
Enter fullscreen mode Exit fullscreen mode

Pour avancer de 15 secondes dans une piste audio :

music.currentTime += 15;
Enter fullscreen mode Exit fullscreen mode

J'ai appris au passage qu'on ne pouvait pas jouer deux fois le même fichier en parallèle (ce qui pourtant est nécessaire si deux bruitages identiques sont joués de façon rapprochée). Le problème se résoud avec la fonction cloneNode :

music.cloneNode().play();
music.cloneNode().play(); // Deuxième lecture en parallèle
Enter fullscreen mode Exit fullscreen mode

Par ailleurs, le nombre de joueurs étant très variable, chaque nouveau zombie ne fait pas progresser la musique d'un cycle précisément, contrairement à ce qui est expliqué dans l'article pour simplifier. Nous avons une fonction progressTo(progression) qui prend un nombre entre 0 et 1, reflétant l'évolution du jeu (0 étant de le début, et 1 la fin). Ainsi, appeler progressTo(0.5) signifie que le jeu est à la moitié de sa progression (la moitié des joueurs sont des zombies). La musique peut alors avancer en fonction du nombre de cycles disponibles et, éventuellement, sauter des cycles si nécessaire.

C'est tout ! Le reste du travail n'était que du JS classique.

Cependant, mon implémentation n'est pas ultra précise. Mon code utilise des setTimeout et setInterval, et rien ne garantit qu'ils seront appelé exactement au bon moment.

Je n'avais pas le temps d'expérimenter au-delà, et c'est totalement spéculatif, mais peut-être faudrait-il aller voir du côté des service workers, ou de WebAssembly, ou d'une autre manière de gérer l'audio que j'ignore totalement ?

Collapse
 
sousacaio profile image
Caio frias

Pretty cool Bastien!

Collapse
 
simonjamain profile image
simonjamain • Edited

Je trouve que c'est vraiment super bravo monsieur !

Collapse
 
bcalou profile image
Bastien Calou

<3