JavaScript: Web Workers und Conway's Game of Life in 3D
Spielrunden
Der Code der Datei main.js fungiert als Controller des Programms. Neben den Attributen und Regeln des Universums befindet sich dort der Code, der den ParticleRenderer steuert. Der Fokus soll jedoch auf der Funktion naechsteGeneration liegen, die sich mit Web Workers optimieren lässt. Folgende Variablen sind deshalb von Interesse:
- gameOfLife ist eine Instanz der Spiellogik und enthält unter anderem das Multiversum.
- aktivesUniversum ist der Index des aktuell gezeigten Universums.
- positionsArray enthält die Koordinaten für den ParticleRenderer. Der Speicher dafür wird nur einmal durch die Variable positionsArrayBuffer allokiert.
Die Funktion naechsteGeneration selbst beinhaltet nur vier Schritte:
- Berechnen der nächsten Generation
- Wechsel des aktiven Universums
- Generieren des Positionsarrays für den ParticleRenderer
- Einplanen des nächsten Funktionsaufrufs naechsteGeneration
//Berechnet die nächste Generation, wechselt das Universum, erzeugt die
//Partikel und plant die nächste Generation ein
var naechsteGeneration = function() {
var quelle = gameOfLife.multiversum[aktivesUniversum];
var ziel = gameOfLife.multiversum[aktivesUniversum^1];
gameOfLife.berechneNaechsteGeneration(quelle, ziel, function() {
aktivesUniversum ^= 1;
positionsArray =
ziel.generierePositionsArray(positionsArrayBuffer);
setTimeout(naechsteGeneration, 1);
});
};
Worker für Spielrunden
Bei der Verwendung von Web Workers wäre es ideal, wenn alle Schritte der Funktion naechsteGeneration im Hintergrund ausgeführt werden könnten, damit der Main-Event-Loop sich nur um das Rendern kümmern muss. Web Workers erlauben das Kopieren und Transferieren von Daten bei der Kommunikation über einen MessageChannel, ein geteilter Zugriff ist jedoch nicht möglich. Deshalb ist zunächst zu betrachten, welche Daten im Main-Event-Loop und welche im Worker vorliegen.
Der Renderer benötigt nur das positionsArray, der Rest lässt sich deshalb in den Worker verschieben. Aus Gründen der Performance wäre es jedoch auch sinnvoll, den Speicher des positionsArrays zu transferieren, statt ihn zu kopieren. Da ein gleichzeitiger Zugriff nicht möglich ist, ist ein zweiter ArrayBuffer zu allokieren, der bei jedem Funktionsaufruf ausgetauscht wird. Da diese Logik bereits beim Multiversum zum Einsatz kommt, lässt sich auch hier aktivesUnviersum als Index verwenden. Die Variable aktivesUniversum muss deshalb auch im Main-Event-Loop bleiben.
var positionsArrayBuffer =
new ArrayBuffer(optionen.groesse*optionen.groesse*optionen.groesse*16);
wird ersetzt durch:
var positionsArrayBuffers = [
new ArrayBuffer(
optionen.groesse*optionen.groesse*optionen.groesse*16),
new ArrayBuffer(
optionen.groesse*optionen.groesse*optionen.groesse*16)];
Der Code des Workers ist als separate JavaScript-Datei abzulegen. Erst sind dafür die Klassen GameOfLife3d und Universum mit der Funktion importScripts zu importieren. Um Nachrichten empfangen zu können, ist ein Event-Listener am Worker für das Event message zu registrieren. Der Worker lässt sich über die Variable self ansprechen.
Da man beim Erstellen des Workers keine Parameter übergeben kann, muss man dafür auf den MessageChannel zurückgreifen. Neben den Nachrichten für die Spielrunden ist deshalb zudem eine Nachricht zur Initialisierung zu senden. Anhand des Attributs action lässt sich der Typ der Nachricht unterscheiden. Die Nutzdaten der Nachricht sind in der Variable event.data enthalten, der Typ folglich in event.data.action. Beim Eintreffen einer init-Nachricht erstellt das Programm mit den übergebenen event.data.optionen eine neue Instanz der GameOfLife3d-Klasse. Ist der Nachrichtentyp ungleich init geht es von einer Spielrunde aus.
Die ersten drei Schritte, die bisher in der Funktion naechsteGeneration in der Datei main.js enthalten waren, lassen sich dafür fast identisch übernehmen. Lediglich die übergebenen Variablen sind mit dem Zusatz event.data. zu versehen. Im letzten Schritt ist es nötig, das positionsArray wieder an den Haupt-Event-Handler zu übergeben. Zum Übermitteln einer Nachricht lässt sich postMessage nutzen. Der erste Parameter enthält die Nutzdaten eines Objekts. Der zweite, optionale Parameter ArrayBuffer kennzeichnet Objekte, die zu transferieren und nicht zu kopieren sind.
Alle ArrayBuffer-Objekte müssen als Elemente eines Arrays übergeben werden. Da geplant ist, positionsArray zu transferieren, wird dessen Buffer im zweiten Parameter angegeben. Der HTML5-Standard erlaubt es, TypedArrays als Nutzdaten zu übermitteln, im Firefox steht dieses Feature jedoch noch nicht zur Verfügung. Als Workaround lassen sich Typ, Buffer, Länge und Offset als separate Attribute in der Nachricht übertragen und das TypedArray beim Empfänger wieder zusammenbauen. Da der Typ bekannt und der Offset immer gleich Null ist, kann man die Attribute hart codieren.
//Worker-Code für die Initialisierung und die ersten drei
//Schritte der Spielrundenlogik
importScripts('gol3d.js', 'universum.js');
var gameOfLife3d = null;
self.addEventListener('message', function(event) {
if(event.data.action == 'init') {
gameOfLife3d = new GameOfLife3d(event.data.optionen);
} else {
var quelle =
gameOfLife3d.multiversum[event.data.aktivesUniversum];
var ziel =
gameOfLife3d.multiversum[event.data.aktivesUniversum^1];
gameOfLife3d.berechneNaechsteGeneration(quelle, ziel, function() {
var positionsArray =
ziel.generierePositionsArray(event.data.positionsArrayBuffer);
self.postMessage({
positionsArrayBuffer: positionsArray.buffer,
positionsArrayLaenge: positionsArray.length
}, [
positionsArray.buffer
]);
});
}
}, false);
Letzte Anpassungen
Im Code für den Controller ist der Worker noch zu erstellen und die Initialisierungsnachricht zu übertragen. Worker ist im globalen Kontext enthalten und benötigt als einzigen Parameter einen String, der den Dateinamen des Worker-Codes enthält. Zur Initialisierung sendet das Programm eine Nachricht via postMessage. Da alle Teile der Nachricht zu kopieren sind, ist ein zweiter Parameter nicht notwendig.
var worker = new Worker('gol3d-worker.js');
worker.postMessage({action:'init', optionen: optionen});
Erstellt und initialisiert den Worker