🚧 Attention, peinture fraîche !
Cette page a été traduite par une seule personne et n'a pas été relue et vérifiée par quelqu'un d'autre ! Les informations peuvent par exemple être erronées, être formulées maladroitement, ou contenir d'autres types de fautes.
Implémenter le jeu de la vie de Conway
Conception
Avant de nous plonger dans le sujet, nous devons prendre en considération quelques choix de conception.
Un univers infini
Le jeu de la vie se déroule dans un univers infini, mais nous n'avons pas une mémoire et une puissance de calcul infinie. Pour contourner cette limitation plutôt ennuyeuse, il a généralement trois possibilités :
- Identifier dans quel sous-ensemble de l'univers il se passe des choses intéressantes, et agrandir cette zone si nécessaire. Dans le pire des cas, cette expansion se fera sans limites et donc la simulation deviendra de plus en plus lent et arrivera à cours de mémoire.
- Créer un univers à taille fixe, dans lequel les cellules sur ses bords auront moins de voisines que les cellules au centre. Le désavantage de cette approche est que les schémas infinis, comme les planeurs, qui atteignent probablement la fin de l'univers, seront éliminés.
- Créer un univers à taille fixe, mais en boucle, où les cellules sur les bords seront directement voisines de celles qui sont de l'autre côté de l'univers. Comme les voisines de recoupent d'un bout à l'autre de l'univers, les planeurs pourront continuer à vivre à l'infini.
Nous allons implémenter la troisième option.
Interfacer Rust et le JavaScript
⚡ C'est l'un des concepts les plus importants à comprendre et à retenir de ce tutoriel !
Le tas du JavaScript qui est géré par le ramasse-miettes — dans lequel sont
stockés les objets Object
, les tableaux Array
, et les noeuds du DOM — se
distingue de l'espace mémoire linéaire du WebAssembly, dans lequel vivent nos
valeurs Rust. WebAssembly n'a actuellement pas d'accès direct au tas géré par le
ramasse-miettes (du moins en avril 2018, cela peut changer à l'avenir avec la
proposition des "Interface Types"). JavaScript, de l'autre
côté, peut lire et écrire sur l'espace mémoire linéaire de WebAssembly, mais
seulement via un ArrayBuffer
de valeurs scalaires (comme le u8
,
i32
, f64
, etc ...). Les fonctions WebAssembly prennent elles aussi des
valeurs scalaires et en retourne. Ce sont les éléments de base sur lesquels
repose la communication entre WebAssembly et JavaScript.
wasm_bindgen
définit une vision partagée pour travailler avec des structures
composées pour passer outre ces limites. Cette crate passe une structure Rust
dans une std::boxed::Box
et enveloppe ce pointeur dans une classe JavaScript
pour faciliter son utilisation, ou utilise des indices dans une table d'objets
dans Rust qui représentent des objets JavaScript. wasm_bindgen
est très utile,
mais nous devons toujours garder en tête comment les données sont modélisées, et
quelles sont les valeurs et les structures qui passent entre ces deux domaines.
Considérez-la plutôt comme un outil permettant de choisir votre moyen pour
s'interfacer.
Lorsqu'on conçoit une interface entre WebAssembly et JavaScript, nous voulons optimiser les propriétés suivantes :
- Réduire au maximum les copies de données sur et à partir de la mémoire linéaire de WebAssembly. Les copies inutiles provoquent des surcharges inutiles.
- Minimiser les sérialisations et les déserialisations. Pour la même raison
que pour les copies, les sérialisations et la déserialisations provoquent des
surcharges, et impose parfois aussi des copies, en plus. Si nous pouvons
utiliser des manipulateurs opaques pour une structure de données, plutôt que
d'avoir à la sérialiser d'un côté, de la copier dans un endroit connu dans la
mémoire linéaire de WebAssembly, et la déserialiser de l'autre côté, alors
très souvent on économise beaucoup de ressources.
wasm_bindgen
nous aide à définir et travailler avec des manipulateurs opaques d'objets JavaScript ou de structures Rust intégrées dans desBox
.
En règle générale, une bonne conception d'interface JavaScript↔WebAssembly nécessite souvent que les grosses structures de données à durée de vie longue soient implémentées comme étant des types Rust qui vivent dans la mémoire linéaire de WebAssembly, et soient utilisées en JavaScript via des manipulateurs opaques. Le JavaScript appelle les fonctions WebAssembly exportées qui prennent en argument ces manipulateurs opaques, transforment leurs données, procèdent à des calculs lourds, consultent les données, et retournent finalement un petit résultat copiable. En retournant uniquement un petit résultat de l'opération, nous évitons de copier et/ou de tout sérialiser tout ce qui transite entre le tas géré par le ramasse-miettes de JavaScript et la mémoire linéaire de WebAssembly.
Interfacer Rust et JavaScript dans notre jeu de la vie
Commençons par évoquer les pièges à éviter. Nous ne devons pas copier tout l'univers à l'intérieur et à partir de la mémoire linéaire de WebAssembly à chaque tick. Nous ne devons pas allouer des objets pour chaque cellule dans l'univers, ni faire des appels transversaux entre les deux domaines pour lire et écrire chaque cellule.
Qu'est-ce que tout cela implique ? Que nous pouvons représenter l'univers comme
un tableau à une dimension qui vit dans la mémoire linéaire de WebAssembly, et
qui a un octet pour chaque cellule. 0
modélisera une cellule morte, et 1
sera une cellule vivante.
Voici à quoi ressemble un univers de 4 par 4 dans la mémoire :
Pour trouver l'indice d'une cellule dans le tableau à partir d'une ligne et d'une colonne, nous pouvons utiliser cette formule :
indice(ligne, colonne, univers) = ligne * largeur(univers) + colonne
Nous pouvons exposer les cellules de l'univers au JavaScript de différentes
manières. Pour commencer, nous allons implémenter
std::fmt::Display
sur Univers
, qui nous permettra de générer
une String
en Rust des cellules qui représentera les cellules avec des
caractères. Cette chaîne de caractères Rust est ensuite copiée à partir de la
mémoire linéaire de WebAssembly dans une chaîne de caractères en JavaScript,
stockée dans le tas géré par le ramasse-miettes de JavaScript, et est ensuite
affichée dans l'élément HTML contenuTextuel
. Plus tard dans ce chapitre, nous
allons faire évoluer cette implémentation pour éviter de copier les cellules de
l'univers entre les tas et les intégrer dans un <canvas>
.
Une autre conception alternative acceptable serait que Rust retourne une liste de toutes les cellules qui changent d'état après chaque tick, au lieu de donner l'intégralité de l'univers au JavaScript. Ainsi, JavaScript n'aurait pas besoin d'itérer sur tout l'univers lorsqu'il s'occupe du rendu, mais uniquement sur le sous-ensemble concerné. Le désavantage est que cette conception basée sur les différences et un peu plus difficile à implémenter.
Implémentation de Rust
Dans le dernier chapitre, nous avons cloné un modèle initial de projet. Nous allons maintenant modifier ce projet.
Commençons par enlever l'import de alert
et la fonction saluer
dans
wasm-jeu-de-la-vie/src/lib.rs
, et remplacons-les par une définition d'un type
pour les cellules :
#![allow(unused)] fn main() { #[wasm_bindgen] #[repr(u8)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Cellule { Morte = 0, Vivante = 1, } }
Il est important d'avoir #[repr(u8)]
pour que chaque cellule soit représentée
par un seul octet. Il est aussi important que la variante Morte
soit 0
et
que la variante Vivante
vaut 1
, afin que nous puissions facilement compter
les voisines vivantes d'une cellule en les additionnant.
Ensuite, définissons l'univers. L'univers a une largeur et une hauteur, et a un
vecteur de cellules qui a une taille de largeur * hauteur
.
#![allow(unused)] fn main() { #[wasm_bindgen] pub struct Univers { largeur: u32, hauteur: u32, cellules: Vec<Cellule>, } }
Pour accéder à la cellule à une ligne et colonne donnée, nous calculons l'emplacement dans le vecteur de cellules avec la ligne et la colonne comme nous l'avons décrit précédemment :
#![allow(unused)] fn main() { impl Univers { fn calculer_indice(&self, ligne: u32, colonne: u32) -> usize { (ligne * self.largeur + colonne) as usize } // ... } }
Pour calculer le prochain état d'une cellule, nous devons compter combien de
cellules sont vivantes dans son voisinage. Ecrivons donc une méthode
compter_voisines_vivantes
pour cela !
#![allow(unused)] fn main() { impl Universe { // ... fn compter_voisines_vivantes(&self, ligne: u32, colonne: u32) -> u8 { let mut compteur = 0; for delta_ligne in [self.hauteur - 1, 0, 1].iter().cloned() { for delta_colonne in [self.largeur - 1, 0, 1].iter().cloned() { if delta_ligne == 0 && delta_colonne == 0 { continue; } let ligne_voisine = (ligne + delta_ligne) % self.hauteur; let colonne_voisine = (colonne + delta_colonne) % self.largeur; let indice = self.calculer_indice(ligne_voisine, colonne_voisine); compteur += self.cellules[indice] as u8; } } compteur } } }
La méthode compter_voisines_vivantes
utilise les deltas et les modulos pour
éviter de traiter les cas particuliers des bords de l'univers avec le if
.
Lorsqu'on applique un delta de -1
, nous ajoutons self.hauteur - 1
et nous
laissons le modulo faire son travail, plutôt que d'essayer d'enlever 1
.
ligne
ou colonne
peut valoir 0
, et si nous essayons de leur soustraire
1
, nous serons alors en dehors des valeurs acceptées par les entiers
non-signés.
Maintenant, nous avons tout ce dont nous avons besoin pour calculer la prochaine
génération ! Chaque règle du jeu suit des transformations simples suivant des
conditions qui peuvent tenir dans une expression match
. De plus, comme nous
souhaitons que le JavaScript contrôle lorsque les ticks se produisent, nous
allons intégrer cette méthode dans un bloc #[wasm_bindgen]
, pour qu'il soit
exposé au JavaScript.
#![allow(unused)] fn main() { /// Méthodes publiques, exportées en JavaScript. #[wasm_bindgen] impl Univers { pub fn tick(&mut self) { let mut generation_suivante = self.cellules.clone(); for ligne in 0..self.hauteur { for colonne in 0..self.largeur { let indice = self.calculer_indice(ligne, colonne); let cellule = self.cellules[indice]; let voisines_vivantes = self.compter_voisines_vivantes(ligne, colonne); let prochain_etat = match (cellule, voisines_vivantes) { // Règle 1 : toute cellule vivante avec moins de deux // voisines vivantes meurt, comme si cela était un effet de // sous-population. (Cellule::Vivante, x) if x < 2 => Cellule::Morte, // Règle 2 : toute cellule vivante avec deux ou trois // voisines vivantes survit jusqu'à la prochaine génération. (Cellule::Vivante, 2) | (Cellule::Vivante, 3) => Cellule::Vivante, // Règle 3 : toute cellule vivante avec plus de trois // voisines vivantes meurt, comme si cela était un effet de // surpopulation. (Cellule::Vivante, x) if x > 3 => Cellule::Morte, // Règle 4 : toute cellule morte avec exactement trois // voisines vivantes devient une cellule vivante, comme si // cela était un effet de reproduction. (Cellule::Morte, 3) => Cellule::Vivante, // Les cellules qui ne répondent à aucune de ces conditions // restent dans le même état. (statut, _) => statut, }; generation_suivante[idx] = prochain_etat; } } self.cellules = generation_suivante; } // ... } }
Pour l'instant, l'état de l'univers est modélisé par un vecteur de cellules.
Pour rendre cela lisible pour un humain, implémentons un rendu textuel basique.
L'idée est d'écrire l'univers ligne par ligne textuellement, ainsi nous allons
écrire le caractère Unicode ◼
(le "carré moyen noir") pour chaque cellule
vivante. Et pour les cellules mortes, nous allons écrire ◻
(le "carré moyen
blanc").
En implémentant le trait Display
de la bibliothèque standard de Rust, nous
pouvons ajouter un moyen de formater la structure de manière à ce qu'elle soit
adaptée pour l'utilisateur. Cela va aussi nous fournir automatiquement une
méthode to_string
.
#![allow(unused)] fn main() { use std::fmt; impl fmt::Display for Univers { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { for ligne in self.cellules.as_slice().chunks(self.largeur as usize) { for &cellules in ligne { let symbole = if cellule == Cellule::Morte { '◻' } else { '◼' }; write!(f, "{}", symbole)?; } write!(f, "\n")?; } Ok(()) } } }
Enfin, nous définissons un constructeur qui initialise l'univers avec un schéma
intéressant avec des cellules vivantes et mortes, ainsi qu'une méthode rendu
:
#![allow(unused)] fn main() { /// Méthodes publiques, exportées en JavaScript. #[wasm_bindgen] impl Univers { // ... pub fn new() -> Univers { let largeur = 64; let hauteur = 64; let cellules = (0..largeur * hauteur) .map(|i| { if i % 2 == 0 || i % 7 == 0 { Cellule::Vivante } else { Cellule::Morte } }) .collect(); Univers { largeur, hauteur, cellules, } } pub fn rendu(&self) -> String { self.to_string() } } }
Avec tout cela, la partie Rust de notre jeu de la vie est complète !
Recompilez-la en WebAssembly en lançant wasm-pack build
dans le dossier
wasm-jeu-de-la-vie
.
Le rendu avec JavaScript
Pour commencer, ajoutons une balise <pre>
à
wasm-jeu-de-la-vie/www/index.html
, dans lequel afficher l'univers, juste avant
la balise <script>
:
<body>
<pre id="canvas-jeu-de-la-vie"></pre>
<script src="./bootstrap.js"></script>
</body>
De plus, nous voulons que le <pre>
soit centré au milieu de la page Web. Nous
pouvons utiliser les boites flex pour faire cela. Ajoutez la balise <style>
suivante dans le <head>
de wasm-jeu-de-la-vie/www/index.html
:
<style>
body {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
</style>
En haut de wasm-jeu-de-la-vie/www/index.js
, corrigeons notre import pour
importer le Univers
plutôt que la vieille fonction saluer
:
import { Univers } from "wasm-jeu-de-la-vie";
Ensuite, obtenez la balise <pre>
que nous venons juste d'ajouter et instancier
un nouvel univers :
const pre = document.getElementById("canvas-jeu-de-la-vie");
const univers = Univers.new();
Le JavaScript exécute dans une boucle
requestAnimationFrame
. A chaque itération, il écrit
l'univers courant dans le <pre>
, et fait ensuite appel à Univers::tick
.
const boucleDeRendu = () => {
pre.textContent = univers.rendu();
univers.tick();
requestAnimationFrame(boucleDeRendu);
};
Pour initier le processus de rendu, tout ce que nous avons à faire est de faire le premier appel à la première itération de la boucle de rendu :
requestAnimationFrame(boucleDeRendu);
Assurez-vous que votre serveur de développement continue de s'exécuter (lancez
npm run start
dans wasm-jeu-de-la-vie/www
) et voici ce à quoi
http://localhost:8080/ devrait ressembler :
Afficher dans un canvas directement à partir de la mémoire
Générer (et allouer) un String
en Rust et le convertir en String JavaScript
valide par wasm-bindgen
génère des copies inutiles des cellules de l'univers.
Comme le code JavaScript connait déjà la largeur et la hauteur de l'univers,
et peux lire la mémoire linéaire de WebAssembly qui contient les cellules, nous
allons modifier la méthode rendu
pour retourner un pointeur vers le début du
tableau des cellules.
De plus, au lieu d'afficher du texte Unicode, nous allons utiliser l'API de canvas. Nous utiliserons alors cette conception dans la suite du tutoriel.
Dans wasm-jeu-de-la-vie/www/index.html
, remplaçons le <pre>
que nous avons
ajouté précédemment par un <canvas>
dans lequel nous allons faire notre rendu
(il devrait toujours se trouver dans la <body>
, avant le <script>
qui charge
notre JavaScript) :
<body>
<canvas id="canvas-jeu-de-la-vie"></canvas>
<script src='./bootstrap.js'></script>
</body>
Pour obtenir les informations de l'implémentation Rust nécessaires, nous avons
besoin d'ajouter plus d'accesseurs pour obtenir la largeur, la hauteur de
l'univers, et le pointeur à son tableau de cellules. Ils seront eux aussi
exposés au JavaScript. Faites ces ajouts à wasm-jeu-de-la-vie/src/lib.rs
:
#![allow(unused)] fn main() { /// Méthodes publiques, exportées en JavaScript. #[wasm_bindgen] impl Univers { // ... pub fn largeur(&self) -> u32 { self.largeur } pub fn hauteur(&self) -> u32 { self.hauteur } pub fn cellules(&self) -> *const Cellule { self.cellules.as_ptr() } } }
Ensuite, dans wasm-jeu-de-la-vie/www/index.js
, ajoutons aussi l'import de
Cellule
de wasm-jeu-de-la-vie
, et définissons quelques constantes que nous
utiliserons lorsque nous ferons le rendu dans le canvas :
import { Univers, Cellule } from "wasm-jeu-de-la-vie";
const TAILLE_CELLULE = 5; // px
const COULEUR_GRILLE = "#CCCCCC";
const COULEUR_MORTE = "#FFFFFF";
const COULEUR_VIVANTE = "#000000";
Maintenant, ré-écrivons le reste du code JavaScript pour ne plus avoir à écrire
avec textContent
dans le <pre>
mais dessiner dans <canvas>
à la place :
// Construit l'univers, et obtient sa largeur et son hauteur
const univers = Univers.new();
const largeur = univers.largeur();
const hauteur = univers.hauteur();
// Applique une taille au canvas pour accueillir toutes nos cellules et une
// bordure de 1px autour d'elles.
const canvas = document.getElementById("canvas-jeu-de-la-vie");
canvas.height = (TAILLE_CELLULE + 1) * largeur + 1;
canvas.width = (TAILLE_CELLULE + 1) * hauteur + 1;
const ctx = canvas.getContext('2d');
const boucleDeRendu = () => {
univers.tick();
dessinerGrille();
dessinerCellules();
requestAnimationFrame(boucleDeRendu);
};
Pour dessiner la grille entre les cellules, nous dessinons un jeu de lignes espacées régulièrement horizontalement, et un jeu de lignes espacées régulièrement verticalement. Ces lignes s'entrecroisent pour former la grille.
const dessinerGrille = () => {
ctx.beginPath();
ctx.strokeStyle = COULEUR_GRILLE;
// Lignes verticales.
for (let i = 0; i <= largeur; i++) {
ctx.moveTo(i * (TAILLE_CELLULE + 1) + 1, 0);
ctx.lineTo(i * (TAILLE_CELLULE + 1) + 1, (TAILLE_CELLULE + 1) * hauteur + 1);
}
// Lignes horizontales.
for (let j = 0; j <= hauteur; j++) {
ctx.moveTo(0, j * (TAILLE_CELLULE + 1) + 1);
ctx.lineTo((TAILLE_CELLULE + 1) * largeur + 1, j * (TAILLE_CELLULE + 1) + 1);
}
ctx.stroke();
};
Nous pouvons accéder directement à la mémoire linéaire de WebAssembly via
memory
, qui est défini dans le module brut wasm_jeu_de_la_vie_bg
. Pour
dessiner les cellules, nous obtenons le pointeur vers les cellules de l'univers,
construisons un Uint8Array
qui sert de surcouche tampon pour les cellules,
itère sur chaque cellule, et dessine un rectangle blanc ou noir, respectivement
si la cellule est morte ou vivante. En travaillant avec des pointeurs et des
surcouches, nous évitons de copier les cellules entre les deux domaines à chaque
tick.
// Importe la mémoire de WebAssembly au début du fichier.
import { memory } from "wasm-jeu-de-la-vie/wasm_jeu_de_la_vie_bg";
// ...
const calculerIndice = (ligne, colonne) => {
return ligne * largeur + colonne;
};
const dessinerCellules = () => {
const pointeurCellules = univers.cellules();
const cellules = new Uint8Array(memory.buffer, pointeurCellules, largeur * hauteur);
ctx.beginPath();
for (let ligne = 0; ligne < hauteur; ligne++) {
for (let colonne = 0; colonne < largeur; colonne++) {
const indice = calculerIndice(ligne, colonne);
ctx.fillStyle = cellules[indice] === Cellule.Morte
? COULEUR_MORTE
: COULEUR_VIVANTE;
ctx.fillRect(
colonne * (TAILLE_CELLULE + 1) + 1,
ligne * (TAILLE_CELLULE + 1) + 1,
TAILLE_CELLULE,
TAILLE_CELLULE
);
}
}
ctx.stroke();
};
Pour démarrer le processus de rendu, nous allons utiliser le même code que ci-dessus pour démarrer la première itération de la boucle de rendu :
dessinerGrille();
dessinerCellules();
requestAnimationFrame(boucleDeRendu);
Notez que nous faisons appel à dessinerGrille()
et à dessinerCellules()
ici
avant de faire appel à requestAnimationFrame()
. La raison à cela est que
l'état initial de l'univers est dessiné avant que nous procédions à nos
modifications. Si nous avions simplement appelé
requestAnimationFrame(boucleDeRendu)
à la place, nous nous serions retrouvé
dans une situation dans laquelle la première séquence serait dessinée après
le premier appel à univers.tick()
, qui est le second "tick" dans la vie de ces
cellules.
Cela fonctionne !
Recompilez le WebAssembly et la glue de liaison en lançant cette commande dans
le dossier racine wasm-jeu-de-la-vie
:
wasm-pack build
Assurez-vous que votre serveur de développement fonctionne toujours. Si ce n'est
plus le cas, relancez-le dans le dossier wasm-jeu-de-la-vie/www
:
npm run start
Si vous rafraîchissez http://localhost:8080/, vous devriez être accueilli par une simulation de la vie captivante !
Ceci dit, il existe aussi un algorithme très intéressant pour implémenter le jeu de la vie qui s'appelle hashlive. Il utilise une méthode de gestion de la mémoire poussée et peut devenir exponentiellement plus rapide pour calculer les prochaines générations au fur et à mesure qu'il s'exécute ! Sachant cela, vous vous demandez peut-être pourquoi nous n'avons pas implémenté hashlife dans ce tutoriel. Ce n'est pas le but de ce document, car nous nous concentrons sur l'intégration de Rust en WebAssembly, mais nous vous encourageons vivement d'en apprendre plus sur hashlife par vous-même !
Exercices
- Initialiser l'univers avec un simple vaisseau spatial.
-
Au lieu de coder en dur l'univers initial, générez-en un aléatoire, dans lequel chaque cellule a cinquante pour cent de chance d'être vivante ou morte.
Astuce : utilisez la crate
js-sys
pour importer la fonction JavaScriptMath.random
.Réponse
Premièrement, ajoutez
js-sys
comme dépendance danswasm-jeu-de-la-vie/Cargo.toml
:# ... [dependencies] js-sys = "0.3" # ...
Ensuite, utilisez la fonction
js_sys::Math::random
pour générer un nombre aléatoire :#![allow(unused)] fn main() { extern crate js_sys; // (facultatif à partir de Rust 2018) // ... if js_sys::Math::random() < 0.5 { // Vivante ... } else { // Morte ... } }
-
Représenter chaque cellule avec un octet facilite l'itération sur les cellules, mais cela gaspille de la mémoire. Chaque octet a huit bits, mais nous n'avons besoin d'un seul bit pour représenter si chaque cellule est vivante ou morte. Remaniez la représentation des données pour que chaque cellule utilise uniquement un seul bit en mémoire.
Réponse
En Rust, vous pouvez utiliser la crate
fixedbitset
et son typeFixedBitSet
pour représenter les cellules au lieu d'utiliserVec<Cell>
:#![allow(unused)] fn main() { // Assurez-vous d'avoir aussi ajouté la dépendance dans Cargo.toml ! extern crate fixedbitset; // (facultatif en Rust 2018) use fixedbitset::FixedBitSet; // ... #[wasm_bindgen] pub struct Univers { largeur: u32, hauteur: u32, cellules: FixedBitSet, } }
Le constructeur de l'Univers peut être corrigé comme ceci :
#![allow(unused)] fn main() { pub fn new() -> Univers { let largeur = 64; let hauteur = 64; let taille = (largeur * hauteur) as usize; let mut cellules = FixedBitSet::with_capacity(taille); for i in 0..taille { cellules.set(i, i % 2 == 0 || i % 7 == 0); } Univers { largeur, hauteur, cellules, } } }
Pour modifier une cellule à la prochaine tick de l'univers, nous utilisons la méthode
set
deFixedBitSet
:#![allow(unused)] fn main() { generation_suivante.set(indice, match (cellule, voisines_vivantes) { (true, x) if x < 2 => false, (true, 2) | (true, 3) => true, (true, x) if x > 3 => false, (false, 3) => true, (statut, _) => statut }); }
Pour passer un pointeur vers le départ des bits en JavaScript, vous pouvez convertir le
FixedBitSet
en une slice et ensuite convertir la slice en pointeur :#![allow(unused)] fn main() { #[wasm_bindgen] impl Univers { // ... pub fn cellules(&self) -> *const u32 { self.cellules.as_slice().as_ptr() } } }
En JavaScript, la construction d'un
Uint8Array
à partir de la mémoire de WebAssembly est la même que précédemment, excepté que la longueur du tableau n'est pluslargeur * hauteur
, maislargeur * hauteur / 8
puisque nous avons un bit par cellule au lieu d'un octet :const cellules = new Uint8Array(memory.buffer, pointeurCellules, largeur * hauteur / 8);
Pour un indice et un
Uint8Array
donné, vous pouvez obtenir le nième bit avec la fonction suivante :const bitVautTrue = (n, tableau) => { const octet = Math.floor(n / 8); const masque = 1 << (n % 8); return (tableau[octet] & masque) === masque; };
En ayant tout cela, la nouvelle version de
dessinerCellules
ressemble à ceci :const dessinerCellules = () => { const pointeurCellules = univers.cellules(); // On a modifié cela ! const cellules = new Uint8Array(memory.buffer, pointeurCellules, largeur * hauteur / 8); ctx.beginPath(); for (let ligne = 0; ligne < hauteur; ligne++) { for (let colonne = 0; colonne < largeur; colonne++) { const indice = calculerIndice(ligne, colonne); // On a modifié cela ! ctx.fillStyle = bitVautTrue(indice, cellules) ? COULEUR_VIVANTE : COULEUR_MORTE; ctx.fillRect( colonne * (TAILLE_CELLULE + 1) + 1, ligne * (TAILLE_CELLULE + 1) + 1, TAILLE_CELLULE, TAILLE_CELLULE ); } } ctx.stroke(); };