J’ai codé l’univers entier dans le navigateur

J’ai codé l’univers tout entier (et au-delà) en JavaScript

Ce n’est pas un titre putaclic. Je l’ai vraiment fait. Et c’est pas juste une expérience web cette fois. Non. C’est un vrai jeu, entier, librement explorable, en 3D, directement dans ton navigateur ! Tu me crois pas ?



TLDR


Ho, je sais que ton temps est précieux. Voici la version condensée.

J’ai codé un jeu gratuit en 3D dans le navigateur, librement explorable, à travers des univers infinis générés de manière procédurale en JavaScript. L’objectif ? Aller d’univers en univers et découvrir l’origine de tout.

C’est carrément une histoire en quatre chapitres avec une révélation épique à la fin.

Avant d’aller plus loin dans la lecture de cet article, arrête tout, ouvre Chrome, mets-toi en plein écran, prends des pop-corn et joue à ACROSS THE MULTIVERSE !



J’ai codé l’univers entier dans le navigateur


Pas le temps de jouer ?

Tu préfères en profiter sur un PC et pas sur un portable ?

Ou tu veux tout simplement en voir plus avant d’y jouer ?

Pas de problème.

J’ai fait une bande-annonce de lancement sur YouTube ! Ça dure seulement 3 min. Ça montre beaucoup de choses. Par contre attention, c’est extrêmement spectaculaire !





J’ai mis beaucoup de mon temps, de mon âme et de mes compétences dans la création de ce jeu gratuit pour les internets.

Si tu as cinq secondes devant toi pour le partager, ça serait merveilleux.



En attendant, il est temps de parler de l’éléphant qui clignote au milieu de la pièce.

Mais comment j’ai fait un truc pareil ?



Talk is cheap. Show me the code

Je sais que beaucoup d’entre vous préfèrent tout de suite plonger dans le code. Avant même de lire mes si belles explications. Et je comprends tout à fait cette démarche.

Pour les plus impatients, voici le code source entier du jeu.

C’est bien sur open source sous licence MIT (pour le code).

Je conseille quand même de suivre l’histoire de la création progressive du projet via cet article. Ça va donner plus de contexte. Et surtout faire beaucoup plus de sens.



Comment construire l’univers ?

Avant même de commencer, il faut savoir que j’utilise la librairie JavaScript Three.js. Cette librairie permet d’utiliser l’api WebGL via JavaScript pour faire du rendu 3D dans le navigateur.

Il est possible que tu ne captes rien à la phrase d’avant et/ou que tu ne connaisses pas Three.js.
Heureusement j’ai pensé à tout le monde.

J’ai fait un article dédié à Three.js et au rendu 3D en JavaScript.

Cet article va te permettre de tout de suite comprendre la base du projet en seulement 5 minutes.

Bref, revenons à nos moutons.

Comment construire l’univers ?

Clairement le problème est trop gros.

Je ne pouvais pas attaquer frontalement ce problème. Et ce n’est pas comme ça qu’on fait quand on est développeur. Il y a un seul réflexe à avoir quand est face à un problème énorme et complexe.



Réduire la complexité

Construire l’univers ? Trop compliqué. Je ne sais même pas par où commencer. OK, alors réduisons ça.

Construire la Voie lactée ? Toujours trop compliqué, réduisons ça.

Construire notre système solaire ? Non, mais non. C’est trop compliqué. Réduisons ça.

Construire un espace vide rempli d’étoile ?

Ha ! Ça me semble faisable ça ! Un simple champ d’étoiles dans les ténèbres de l’espace.

En y réfléchissant un petit peu, je me dis que ce problème est vraiment simple. Ça veut dire que j’ai assez réduit la complexité.

Il est temps de s’y mettre.



Comment construire un simple champ d’étoiles ?

À partir de ce moment-là, j’ai toujours utilisé une image de référence. Une photo ou une représentation réelle de ce que je voulais recréer. Une image à regarder pour me rapprocher le plus possible d’un rendu réaliste.

Pour le champ d’étoiles, j’avais décidé de prendre une photo du satellite Hubble.





Je me dis que j’ai juste à afficher des points blancs de façon random dans un espace noir.

C’est très facile.

Commençons par créer un espace vide, noir, et mettons une caméra dedans.

const scene = new THREE.Scene()
const renderer = new THREE.WebGLRenderer({ powerPreference: "high-performance", antialias: false, stencil: false, depth: false })
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setClearColor(0xffffff, 0)
renderer.domElement.id = 'starfield'

document.body.appendChild(renderer.domElement)

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
camera.position.set(0, 10, 0)
camera.lookAt(0, 0, 0)

function animate(time) {
    renderer.render(scene, camera)
    requestAnimationFrame(animate)
}

animate()

OK. Super ton truc. Tout ça pour afficher un écran noir ?!

Oui. Soyons patients. Il faut bien commencer quelque part.

Maintenant, plus dur.

Affichons des points blancs de façon random dans cet espace noir. Tu sais faire ? Moi je savais pas faire. Donc je suis allé voir la doc.

Et bingo !

Il existe une classe dédiée à ça : Points.

Parfait, suivons la doc et écrivons ça.

function getStarsGeometry() {
    const geometry = new THREE.BufferGeometry()

    geometry.setAttribute('position', new THREE.Float32BufferAttribute(getStarsRandomVertices(), 3))

    return geometry
}

function getStarsRandomVertices(verticesNumber = 10000) {
    const vertices = []

    for (let i = 0; i < verticesNumber; i++) {
        const x = 2000 * Math.random() - 1000;
        const y = 2000 * Math.random() - 1000;
        const z = 2000 * Math.random() - 1000;

        vertices.push(x, y, z);
    }

    return vertices
}

function getStarsMaterial() {
    const starSprite = new THREE.TextureLoader().load('../images/star.png');
    const starMaterial = new THREE.PointsMaterial({ size: 5, sizeAttenuation: true, map: starSprite, alphaTest: 0.5, transparent: true });

    return starMaterial
}

function getStars() {
    const stars = new THREE.Points(getStarsGeometry(), getStarsMaterial())

    return stars
}

scene.add(getStars())

Dans ce bout de code, ce qui va vraiment faire la magie c’est la fonction getStarsRandomVertices.

Notre champ d’étoiles (ici représenté par new THREE.Points) a besoin de deux choses.

1 : Les coordonnées de chacun des points à afficher
2 : Le material de chacun des points. C’est à dire ce qu’on va afficher (pour simplifier) pour chacun des points.



Les coordonnées sont gérées par getStarsRandomVertices.

Notre caméra est placée aux coordonnées 0,0,0. On veut des étoiles tout autour de nous. Donc il faudrait que nos points soient placés entre les coordonnées -1000 et 1000. Tout autour de nous.

Pour faire ça, on va faire un calcul tout simple.

2000 * Math.random() – 1000.

Ce bout de math hyper simple nous donne une valeur random (math.random est pas vraiment random mais admettons) entre -1000 et 1000. On met ce calcul dans chacun des axes (x, y, z) et le tour est joué !



Le material est géré par getStarsMaterial

Pas grand-chose à dire ici. On va simplement utiliser une image d’un rond blanc comme texture et l’appliquer. Pour le moment on n’a pas besoin de grand-chose.



On assemble les deux dans la fonction getStars et on a résolu notre problème.

Bon, pour le moment ça donne juste une image avec des points blanc statique en 2D.

C’est un peu tout nul. On peut mieux faire.

C’est le moment d’itérer sur ce résultat.



Amélioration via itération

Commençons simplement par animer les choses.

L’idée, pour le moment, c’est de simplement bouger la caméra par rapport au mouvement de la souris. Quand j’ai fait ça, je voulais juste bien me rendre compte que je faisais désormais de la 3D.

Écrivons ça.

document.body.addEventListener('pointermove', onPointerMove);

function onPointerMove(event) {
    if (event.isPrimary === false) return

    mouseX = event.clientX - windowHalfX
    mouseY = event.clientY - windowHalfY
}

function animate(time) {
    renderer.render(scene, camera)

    camera.position.x += (mouseX - camera.position.x) * 0.05
    camera.position.y += (-mouseY - camera.position.y) * 0.05

    requestAnimationFrame(animate)
}

OK c’est cool, ça bouge, c’est de la 3D, on est content.

Maintenant, rendons ça encore plus interactif. Ce que j’aimerais moi c’est me balader là-dedans. Librement. Comme dans un jeu FPS, avec la souris et le clavier.

À ce moment-là, j’avais deux choix :

  • Soit je réécrivais moi-même tout un système de navigation FPS.
  • Soit je reprenais une classe de contrôle FPS déjà toute faite par Three.js.




J’ai évidemment choisi de reprendre le code déjà fait.

Il est testé et utilisé par beaucoup de monde. Je te conseille de faire la même chose quand cette situation t’arrive. À part si tu es dans une démarche d’apprentissage, ça ne sert à rien de réinventer la roue.

Par contre, les features proposées par le module ne me suffisaient pas.

J’en voulais plus.

Je voulais un système de vélocité. Donner aux joueurs cette impression d’accélération et de décélération. Du coup, il fallait que j’utilise le module existant et que j’étende ces possibilités dans ma propre classe.

OK, écrivons ça.

import { PointerLockControls } from './PointerLockControls.js'
import * as THREE from 'three'

export default class Controls {
  constructor (camera, parameters) {
    this.parameters = parameters
    this.camera = camera
    this.pointerLockControls = new PointerLockControls(this.camera, document.body)

    this.velocity = new THREE.Vector3()
    this.direction = new THREE.Vector3()

    this.moveForward = false
    this.moveBackward = false
    this.moveLeft = false
    this.moveRight = false
  }

  onKeyDown (event) {
    if (this.pointerLockControls.isLocked) {
      switch (event.code) {
        case 'ArrowUp':
        case 'KeyW':
        case 'KeyZ':
          this.moveForward = true
          break
        case 'ArrowLeft':
        case 'KeyA':
        case 'KeyQ':
          this.moveLeft = true
          break
        case 'ArrowDown':
        case 'KeyS':
          this.moveBackward = true
          break
        case 'ArrowRight':
        case 'KeyD':
          this.moveRight = true
          break
      }
    }
  }

  onKeyUp (event) {
    if (this.pointerLockControls.isLocked) {
      switch (event.code) {
        case 'ArrowUp':
        case 'KeyW':
        case 'KeyZ':
          this.moveForward = false
          break
        case 'ArrowLeft':
        case 'KeyA':
        case 'KeyQ':
          this.moveLeft = false
          break
        case 'ArrowDown':
        case 'KeyS':
          this.moveBackward = false
          break
        case 'ArrowRight':
        case 'KeyD':
          this.moveRight = false
          break
      }
    }
  }


  handleMovements (timePerf, prevTimePerf) {
    const delta = timePerf - prevTimePerf

    this.direction.z = Number(this.moveForward) - Number(this.moveBackward)
    this.direction.x = Number(this.moveRight) - Number(this.moveLeft)

    if (this.moveForward || this.moveBackward) {
      this.velocity.z -= this.direction.z * this.parameters.controls.velocity * delta
    }

    if (this.moveLeft || this.moveRight) {
      this.velocity.x -= this.direction.x * this.parameters.controls.velocity * delta
    }

    this.pointerLockControls.moveRight(-this.velocity.x * delta)
    this.pointerLockControls.moveForward(-this.velocity.z * delta)
  }
}


Et juste comme ça, on a construit un champ d’étoiles explorable comme dans un FPS. Tiens aller, je te mets une codesandbox, te peux jouer en live dedans.

Tu as juste à cliquer dans les étoiles pour passer en mode FPS.

Pas mal non ?

C’est pas mal.

Mais il est temps de s’intéresser aux choses sérieuses.



Comment simuler l’infini ?

Jusqu’à maintenant on a juste placé des points autour du joueur. Mais il suffit qu’il bouge un peu et il voit la supercherie. C’est un peu pourri.

Alors comment on fait pour scaler ça ?

Comment on le fait d’une façon qui fait du sens ?

Et surtout comment on le fait sans exploser la mémoire et/ou sans descendre sous les 60FPS.

C’est maintenant que commence le vrai projet.



La grille

À ce moment-là, j’ai arrêté de toucher à mon clavier.

Pour coder une solution pour ce genre de problème, on ne peut pas « tâtonner jusqu’à y arriver ». Non. Il faut résoudre le problème de façon conceptuelle -sur papier- avant de faire quoique soit.

Sinon tu perds ton temps.

Et je déteste perdre mon temps.

C’est le moment de faire des dessins.

Une des premières idées qui m’était venue à l’esprit était le concept de grille pour représenter l’espace.

Concrètement, l’espace serait une grille infinie. Le joueur irait de case en case pour voir ce qu’elles contiennent. Et dans chaque case on met ce qu’on veut.

Des étoiles, des nébuleuses, des galaxies, des soleils, des trous noirs, ce qu’on veut !

Pour mieux comprendre ce que je raconte, je vais te faire un petit dessin.



Résoudre le problème

Commençons par représenter notre code existant. Tout de suite, on a notre joueur dans la case d’origine 0,0,0 qui est entouré d’étoiles. S’il s’éloigne un peu il se retrouve dans le noir complet.

Conceptuellement, ça ressemble à ça.



  • Le petit bonhomme représente notre joueur.
  • Les étoiles bleues représentent les points déjà placés aléatoirement autour de lui.
  • Les chiffres en rouge sont les coordonnées de chaque case.


Normalement, jusqu’ici, rien de complexe.

Et c’est bien l’objectif ! Je veux à tout prix garder cette simplicité. C’est complexe de faire simple. Essayons de garder cette simplicité au niveau de l’algorithme de mise à jour de la grille.

Il nous faut deux grandes fonctions principales.



La fonction d’initialisation.

Cette fonction va créer la matière dans la case d’origine et dans toutes les cases autour du joueur.

L’avantage de la phase d’initialisation c’est qu’on peut se permettre des actions coûteuses. Tant que le joueur n’est pas dans une phase de gameplay, on est assez libre.

Conceptuellement, ça ressemble à ça.

  • Les étoiles vertes représentent les points qu’on crées dynamiquement lors de la fonction d’initialisation

La fonction de mise à jour.

Cette fonction va mettre à jour la grille seulement quand le joueur franchit la frontière entre deux cases.

Avec cette fonction on veut deux choses :

  • Supprimer le contenu des cases trop éloignées du joueur
  • Créer le contenu des cases où le joueur est le plus susceptible d’aller

Conceptuellement, ça ressemblerait à ça.

  • Les étoiles bleues représentent les points déjà placés
  • Les étoiles vertes représentent les points qu’on crées
  • Les étoiles rouges représentent les points qu’on supprime

Et juste comme ça, on a géré la simulation de l’infini.

Le joueur peut aller partout où il veut. Dans le sens et de la façon dont il veut. Il ne verra pas la supercherie. Partout où il ira, on lui aura déjà préparé à l’avance des choses merveilleuses à regarder.

J’aime cette solution, car il y a plusieurs avantages.

  • C’est relativement performant

Le fait qu’on crée les contenus des cases à la volée et surtout qu’on en supprime en même temps soulage beaucoup la mémoire. De plus, on ne crée que le minimum de cases nécessaires à chaque fois.

  • On n’a pas à gérer la direction du joueur

Peu importe la direction du joueur, l’algorithme sera le même. En effet, on n’a pas besoin de savoir quelles sont les cases en face du joueur. On veut juste savoir quelles sont les cases autour du joueur qui sont vides ! Et donc il peut aller dans le sens qu’il veut, notre algorithme fera exactement la même chose.

  • C’est simple à gérer

Pas besoin de structure de données tout droit venue des enfers type graphe ou arbre comme un octree. Non, non. Olala laissez-moi tranquille. Un tableau, deux hashmap et ça suffit. Pas de prise de tête. On reste simple.

Bon bah, y’a plus qu’a.



Coder la solution

On va créer cette fameuse classe qui va gérer la grille. Par souci de longueur et de simplification, je n’explique pas tout. Et surtout, je ne montre pas tout.

Tu as le source code entier si tu veux tout voir.

On regarde juste les parties importantes aujourd’hui.

import MultiverseFactory from '../procedural/MultiverseFactory'

export default class Grid {
  constructor (camera, parameters, scene, library) {
    this.camera = camera
    this.parameters = parameters
    this.scene = scene
    this.library = library
    this.activeClusters = new Map()
    this.queueClusters = new Map()

    this.multiverseFactory = new MultiverseFactory(this.scene, this.library, this.parameters)
  }

  getCurrentClusterPosition () {
    const currentCameraPosition = this.getCurrentCameraPosition()
    const xCoordinate = Math.trunc(currentCameraPosition.x / this.parameters.grid.clusterSize)
    const yCoordinate = Math.trunc(currentCameraPosition.y / this.parameters.grid.clusterSize)
    const zCoordinate = Math.trunc(currentCameraPosition.z / this.parameters.grid.clusterSize)
    const currentClusterPosition = `${xCoordinate},${yCoordinate},${zCoordinate}`

    return currentClusterPosition
  }

  getCurrentCameraPosition () {
    this.camera.updateMatrixWorld()

    return this.camera.position
  }

  getClustersStatus (currentCluster) {
    const clustersNeighbour = this.getNeighbourClusters(currentCluster)
    const clustersToPopulate = this._getEmptyClustersToPopulate(clustersNeighbour)
    const clustersToDispose = this._getPopulatedClustersToDispose(clustersNeighbour, currentCluster)

    return {
      clustersNeighbour,
      clustersToPopulate,
      clustersToDispose
    }
  }

  getNeighbourClusters (currentCluster) {
    const neighbourClusters = [currentCluster]
    const currentClusterArray = currentCluster.split(',')
    const x = currentClusterArray[0]
    const y = currentClusterArray[1]
    const z = currentClusterArray[2]

    // forward
    neighbourClusters.push(`${x},${y},${Number(z) - 1}`)

    // backward
    neighbourClusters.push(`${x},${y},${Number(z) + 1}`)

    // right
    neighbourClusters.push(`${Number(x) + 1},${y},${z}`)

    // left
    neighbourClusters.push(`${Number(x) - 1},${y},${z}`)

    // forward right
    neighbourClusters.push(`${Number(x) + 1},${y},${Number(z) - 1}`)

    // forward left
    neighbourClusters.push(`${Number(x) - 1},${y},${Number(z) - 1}`)

    // backward right
    neighbourClusters.push(`${Number(x) + 1},${y},${Number(z) + 1}`)

    // backward left
    neighbourClusters.push(`${Number(x) - 1},${y},${Number(z) + 1}`)

    return neighbourClusters
  }

  disposeClusters (clustersToDispose) {
    for (const clusterToDispose of clustersToDispose) {
      let matter = this.activeClusters.get(clusterToDispose)

      matter.dispose()
      matter = null

      this.activeClusters.delete(clusterToDispose)
    }
  }

  addMattersToClustersQueue (matters, type = 'starfield', subtype = null) {
    for (const clusterToPopulate of Object.keys(matters)) {
      this.queueClusters.set(clusterToPopulate, {
        type: type,
        subtype: subtype,
        data: matters[clusterToPopulate]
      })
    }
  }

  populateNewUniverse () {
    const clusterStatus = this.getClustersStatus('0,0,0')

    this.buildMatters(clusterStatus.clustersToPopulate)
  }


  renderMatters (position, cluster) {
    const matter = this.multiverseFactory.createMatter(cluster.type)
  
    matter.generate(cluster.data, position, cluster.subtype)
    matter.show()
  
    this.queueClusters.delete(position)
    this.activeClusters.set(position, matter)
  }

  _getEmptyClustersToPopulate (neighbourClusters) {
    const emptyClustersToPopulate = []

    for (const neighbourCluster of neighbourClusters) {
      if (!this.activeClusters.has(neighbourCluster)) {
        emptyClustersToPopulate.push(neighbourCluster)
      }
    }

    return emptyClustersToPopulate
  }

  _getPopulatedClustersToDispose (neighbourClusters, currentCluster) {
    const populatedClustersToDispose = []

    for (const activeClusterKey of this.activeClusters.keys()) {
      if (currentCluster !== activeClusterKey && !neighbourClusters.includes(activeClusterKey)) {
        populatedClustersToDispose.push(activeClusterKey)
      }
    }

    return populatedClustersToDispose
  }
}


Et ça marche !

Le contenu des cases est ajouté à la volée quand le joueur s’approche. L’illusion est presque parfaite. Je dis presque parce que malheureusement, on a un gros problème.

Je sais que ça ne se voit pas énormément dans la vidéo.

Les performances à la mise à jour de la grille sont désastreuses.

Ça freeze l’image, c’est juste dégueulasse et injouable en l’état.

Il est donc temps de diagnostiquer puis d’optimiser.



Diagnostiquer & Optimiser

Quand un problème de performance arrive dans une application, le premier réflexe c’est de diagnostiquer avant de faire quoique ce soit.



Diagnostiquer

Dans le cas d’une application web comme la nôtre, on va faire ça avec la chrome dev tools. F12, onglet « Performance » puis CTRL+E pour enregistrer ce qui se passe. Puis on utilise l’application normalement avant d’arrêter l’enregistrement et d’analyser les résultats.

En faisant ça, très vite, j’ai compris ce qu’il se passait.





On a des gros drop de FPS car on essaye de faire trop de choses en meme temps.

On fait trop de choses pour JavaScript. JavaScript étant single-threaded, ca pardonne pas. On en demande trop, dans un trop petit laps de temps, pour un seul thread.

Tu te rappelles le calcul simple dont je te parlais au début ?

2000 * Math.random() – 1000

On fait ça 300 000 fois. En une frame.

Multiplier par 3 pour chaque axe (x, y x) des coordonnées.

Encore multiplié par 3 pour les trois nouvelles cases qu’on crée à chaque mouvement de case en case du joueur.

Et pour le moment, on fait juste un calcul simple pour les champs d’étoiles. Quand on va créer des nébuleuses ou des galaxies plus tard, les maths vont être beaucoup plus intensives.

C’est coûteux. Très couteux. Tellement couteux qu’on dépasse la limite des 16ms autorisés par frame pour une image fluide. On monte à 33ms. Ça bloque l’event loop, ça freeze l’image et ça devient injouable.

Si on laisse comme ça, notre joueur va lui aussi quitter le jeu en 33ms.



Optimiser

Pour éviter ça, j’ai deux solutions.

  • Premièrement, on va se libérer de la limite single thread de JavaScript.

On va le faire en utilisant les Web Workers du navigateur. Je ne vais pas faire un cours là-dessus, c’est très connu, et la page MDN est extrêmement bien foutue pour les comprendre.

Concrètement, on va envoyer aux Web Workers tous les calculs lourds du jeu.

Ces calculs seront alors faits en tâche de fond, par le navigateur. L’objectif c’est de ne pas déranger notre thread principal. Il doit être chargé d’une seule chose : afficher les choses de façon fluide aux joueurs.

Une fois les calculs lourds faits, les Web Workers nous renvoient les résultats dans des événements. Notre thread principal n’a plus qu’à les afficher !



// in worker.js

self.onmessage = messageEvent => {
  const heavyMath = _getHeavyMath()

  self.postMessage(heavyMath)
}

function _getHeavyMath () {
  const positions = []
  const colors = []
  
  // heavy math to process positions and colors of particles

  return {
    positions: new Float32Array(positions),
    colors: new Float32Array(colors)
  }
}


// in main.js

worker.onmessage = messageEvent => this.showData(messageEvent.data)

Et juste comme ça, on divise par dix la charge !

Mais c’est pas suffisant. Pour avoir des performances excellentes, on va soulager un peu plus l’event loop.



  • Deuxièmement, on va étaler les phases d’affichage des cases dans le temps.

En l’état, les calculs lourds sont bien faits dans les web workers. Mais il est très possible que l’affichage des trois cases soit demandé exactement au même moment. On veut éviter ça pour avoir une image parfaitement fluide.

Pour ce faire, on va utiliser un petit trick.

On va autoriser un seul affichage de champs d’étoiles au même moment via un boolean. Puis on va étaler les phases d’affichage de chaque case via un setTimeout. Ce qui veut dire que l’affichage de chaque case ne sera pas instantané. Il sera espacé de 50ms. Une par une.

50ms c’est énorme en termes de soulagement pour l’event loop.

Et c’est imperceptible par le joueur.

Exactement ce qu’il nous faut.

isRenderingClusterInProgress = true

const clusterTorender = grid.queueClusters.keys().next().value

setTimeout(() => {
  grid.renderMatters(clusterTorender, 
  grid.queueClusters.get(clusterTorender))
  isRenderingClusterInProgress = false
}, parameters.global.clusterRenderTimeOut)

Et voilà !

Des champs d’étoiles infinis dans ton navigateur.

Elle est pas belle la vie ?



Et le reste ?

Si tu as jouer au jeu et/ou vu le trailer, tu vois bien qu’il manque l’explication pour 90% du contenu.

Les nébuleuses, les soleils, les trous noirs, les géantes rouges, les putains de trous de ver entre les univers, les quatre univers différents et le final incroyable !

En effet. Mais ce projet est énorme. Trop énorme pour en faire un seul article.

Une flopée d’articles (au moins quatre) sur le sujet arrive. On va s’attarder sur chacun des sujets pour en parler.

Je ne sais pas encore si je vais les poster sur ce blog. Je pense que ca ira plus sur mon profil dev.to. Ça va pas mal dépendre de votre réaction ici.



Épilogue

C’était le plus gros projet personnel que j’ai jamais fait. C’était incroyable à faire. Y’a eu des hauts, des bas, de la détresse et de l’émerveillement. Je vous laisse profiter du jeu. Pas sûr que je refasse un projet aussi gros tout de suite. Hésitez pas à le partager autour de vous, ça me ferait beaucoup plaisir.

Je vais aller dormir un peu maintenant.

Qui me parle ?

jesuisundev
Je suis un dev. En ce moment, je suis développeur backend senior / DevOps à Montréal pour un géant du jeux vidéo. Le dev est l'une de mes passions et j'écris comme je parle. Je continue à te parler quotidiennement sur mon Twitter. Tu peux m'insulter à cet e-mail ou le faire directement dans les commentaires juste en dessous. Y'a même une newsletter !

Pour me soutenir, la boutique officielle est disponible ! Sinon désactiver le bloqueur de pub et/ou utiliser les liens affiliés dans les articles, ça m'aide aussi.

45 commentaires sur “J’ai codé l’univers tout entier (et au-delà) en JavaScript”

  1. Hello,

    A mon avis poste ici. C’est très technique mais c’est passionnant, vraiment.
    Et franchement bravo, j’aurai pas le temps d’y jouer aujourd’hui, mais j’y jouerai !

  2. Subjugué par les nébuleuse, que de beautées !! Beau travail. Les trous noirs sont moins réussi, on dirait qu’ils pop d’un coup de nul part…

  3. Moi j’ai pas envie de parler du rendu, de trou noir réussi ou pas. Même si je suis en sidération totale. Je suis une baby dev. Le genre qui essaie de faire marcher Vanta.js sur React parce qu’animer custom un background m’est impossible techniquement parlant. Le genre qui vient de passer 6 mois à apprendre le code et qui a lu tes articles plusieurs fois, parfois pour apprendre des trucs, parfois pour me marrer (bon souvent les deux en vrai). Et quand j’ai vu ton nouveau projet, j’ai surtout vu la magie infinie de ce job. Quand mon code fonctionne, y a de la magie, à ma petite échelle. Mais quand je vois ce que tu as fait, et grâce à tes quelques étapes expliquées, je réalise l’incroyable infinité de magie de fou de ce job ! Et merci pour ça !! Tu montres pas seulement l’immensité de l’univers physique dans ce projet, tu me montres à moi (et certainement à plein d’autres), l’immensité de l’univers du dev ! J’espère franchement un jour te croiser pour te remercier pour le contenu de haute qualité que tu nous offres. Moi je viens de finir le premier projet en équipe de ma mini carrière et on a fait un background étoilé (et on en a chié). Ton projet à toi a donné de la perspective à notre modeste réalisation ^^

    1. Salut Elena !

      Super honoré de t’être utile dans ton apprentissage. Ca m’a pris des années pour être confiant dans ce que je faisait et encore plus longtemps pour me sentir a ma place. Donc te prend pas la tête et continue a bosser, ca va le faire !

  4. Ça aurait été bien de pouvoir se poser sur les planètes, ou que les échelles soient un peu plus réaliste, ou qu’on puisse aller en haut ou bas, ou savoir sa vélocité, et que les trous noir n’apparaissent pas de façon aléatoire sur un timer.

    Sympa sinon

  5. Après avoir joué au jeu, je viens de lire toutes les explications de cette page à mes garçons de 7 et 10 ans. Ils ont compris une bonne partie et surtout vu la démarche avec émerveillement. Ils te font dire qu’ils ont adoré et que le jeu est super joli !

  6. Un énorme gros félicitation pour ton projet de voyage spatial. Beaucoup d’efforts pour arriver à quelque chose de très beau, de relaxant et en quelque sorte de spirituel j’ose dire. Bref, ça en valait la peine.
    Je ne suis pas programmeur (mais ayant déjà étudier la prog en C++ il y a déjà plus de 20 ans, j’en ais 50) mais j’ai quand même lu tes explications et techniques en prog, et j’ai trouvé cela intéressant et drôle aussi.
    Continue comme ça. Tu fais partie des programmeurs exceptionnels qui nous fait rêver…comme celui de voyager dans l’univers…et pourquoi pas ?

  7. Félicitations pour avoir mené ce projet à terme, j’avais suivi un peu les quelques articles que tu avais déjà écrit sur le sujet.
    Au niveau du rendu, la qualité est assez inégale à mon goût mais certains sont vraiment superbes.
    Au niveau technique, c’est intéressant, je ne connais pas du tout Three.js et n’utilise pas WebGL mais ça a l’air franchement puissant.
    Allez, je retourne me balader dans l’univers.

    1. J’avoue que c’est parfois inégale. Il faut dire que je suis toujours en apprentissage sur WebGL et Three.js et parfois ca se voit un peu.

  8. Wooow bravo l’artiste !! L’immersion est vraiment top, ça fait rêver. Ca me donne envie de me lancer dans ThreeJS.
    J’ai mille questions, hâte de voir comment tu as résolu les autres problèmes que tu as rencontré

  9. C’est drôle, ce week-end je me suis perdu sur Youtube à regarder les vidéos de science sur l’espace et les trous noirs, et lundi, je tombe sur ton magnifique projet… J’ai bien passé une bonne heure à voguer dans les étoiles et à me dire « mais quoi, tout ça en JS ??? ». Mon niveau technique de développement est bien trop faible pour apprécier ton code et potasser la source de ce jeu, mais en tout cas, c’est un très beau travail artistique !
    Je trouve ça génial que tu nous partages les origines de cette expérience web et tous tes balbutiements pour y parvenir, c’est très instructif. Un grand bravo et merci !

  10. Chapeau ! J’ai été émerveillé par la vision de ce film interactif, par ces univers imaginaires et leurs passages transversaux et aussi par la technique sous-jacente. Le jeu fonctionne aussi sous Firefox (v92).

  11. Le résultat est vraiment spectaculaire. Bravo !

    Si on parle d’une simulation, ça veut dire qu’on se rapproche du réel. Et dans le réel, le retour à un écran qui était 2 cases après t’affichera la même liste d’étoiles à la même position. Or là, on repart sur une liste d’étoiles aléatoire.
    Je sais que c’est tatillon mais si tu devais générer un univers d’étoiles avec un état permanent tu n’aurais pas d’autre choix que de stocker (localStorage ?) les parties visitées.

    1. Oui, et pour le coup ca serait plutôt facile à faire. J’ai juste mit ca en todo pour le futur.
      Je pourrais meme utiliser en multiplayer avec un serveur et une db.
      Le premier qui parcoure les lieux créer/découvre l’univers et les autres suivent ces pas.

      1. En effet, c’est une excellente dans l’objectif de créer un monde persistant. Encore une fois, félicitations pour ce beau projet et bonne continuation et encore merci pour le partage d’explications. Bien à toi.

    2. Peut-être qu’il suffirait de stocker la seed qui a servi à générer la case? Comme ça quand in revient dessus on retrouve les mêmes étoiles.

  12. Hello,
    C’est top ce que tu à fait !
    Je ne peux pas accéder à ton projet sur Github, mais pour la partie webworker si tu as besoin d’optimiser un peu plus, tu peux transférer un bout de mémoire (Via les TypedArray) d’un worker vers ton thread principal !
    Tu là probablement déjà fait, mais dans le doute 😉

    Bonne continuation à toi !

  13. Bien joué, c’est du bon boulot. Sur mon pc le jeu tourne pas très bien, mais c’est un vieux coucou, peut-être une option pour réduire la taille des chunks. Mais c’est cool j’avais pas conscience que on pouvais faire ce genre de chose en web. Poste le ici, je pense pas que des néophyte pur lise ce blog, et un tel don de vulgarisation que même en y comprenant pas grand chose c’est un plaisir a lire 😀

  14. je découvre ton site et en même temps ce super projet 🙂 merci pour ce partage ! très intéressant de voir ce qu’il est possible de faire en JS sur un navigateur .. par contre plus de nouvelles depuis 2021 ?!?

    bonne continuation 🙂
    signé #jesuisundevaussi

T'en penses quoi ?

Your email address will not be published. Required fields are marked *