Space of Cubes

Beschreibung

Space of Cubes ist ein in JavaScript (Three.js) entwickeltes Open-World-Sandbox-Spiel ähnlich zu Minecraft. Die erste Version bietet 9 verschiedene Blöcke, eine zufallsbasierte Weltgenerierung, einen Mehrspielermodus und einige Grafikeinstellungen. Mobilgeräte werden ebenfalls unterstützt. Eine neuere, experimentelle Version rendert die Welt mithilfe einer eigens programmierten Voxel-Engine weitaus effizienter, aber verfügt noch nicht über dieselben Features.

Das Ziel dieses Spiels besteht darin, eigenständig eine Welt aufzubauen und die unendlich grosse Landschaft zu erkunden. Aufgrund von technischen Limitierungen kann ich die vollständige, erste Version des Spiels nicht auf diesem Webserver hosten, weswegen nur entsprechende Screenshots und Videos vorhanden sind. Die Neuauflage hingegen ist online spielbar.

Screenshots

Erste Version (Mai 2022):

Entwicklung

Die Programmierung von Space of Cubes (erste Version) kostete mich insgesamt einige Wochen. Zuerst absolvierte ich ein Three.js-Tutorial, um mit der Entwicklung beginnen zu können. Danach setzte ich den Renderer und die Steuerung um. Die grösste Herausforderung bestand darin, die Funktionalität der Steuerung auf jedem Gerät zu gewährleisten.

Nachdem ich meine Kompatibilitätsprobleme behoben hatte, musste ich die Baumechanik implementieren. Mit der rechten Maustaste lässt sich ein Block platzieren, indem man das Fadenkreuz auf eine Seite eines beliebigen anderen Blocks richtet. Anschliessend wird der neue Block an die anvisierte Seite platziert.
Durch Three.js Raycasting wird die korrekte Seite bestimmt.
Das Abbauen funktioniert identisch zum Platzieren von Blöcken, jedoch wird diese Aktion durch einen Linksklick hervorgerufen. Der anvisierte Block wird entfernt.

Da ich zu diesem Zeitpunkt noch keinen Block entworfen hatte, musste ich temporär Platzhalter einsetzen. Sobald alles funktionierte, erstellte ich eine Bildschirmaufnahme.

Anschliessend richtete ich eine Datenbank ein, in der die Positionen und Typen aller Blöcke gespeichert werden.
Beim Platzieren eines Blocks werden die Daten direkt zum Server übermittelt. Um die Änderungen anderer Spieler anzuzeigen, werden alle Blöcke, die seit der letzten Aktualisierung platziert wurden, in einem sekündlichen Intervall geladen.
Diese Funktion ermöglichte mir, die Darstellungsdistanz von Blöcken zu limitieren. In folgendem Video ist das Laden und Entladen von Blöcken ersichtlich:

Mit einer funktionstüchtigen Datenbank wagte ich mich an insgesamt neun Blocktypen. Jeder Typ verfügt über eine andere Textur.
Um die Texturen dreidimensional darzustellen, verwendete ich Normal Maps. Ebenfalls definierte ich mithilfe von Specular Maps die unterschiedlichen Materialeigenschaften.

Nachdem ich alle Blöcke vervollständigt hatte, konnte ich mit der Weltgenerierung beginnen. Hierfür verwendete ich eine Simplex-Noise-Implementierung in JavaScript. Anhand von Koordinaten werden zufällige Werte ausgegeben, die sich je nach "Lage" mehr oder weniger voneinander unterscheiden. Richtig konfiguriert ergibt sich daraus ein mehrdimensionales Konstrukt, welches einer natürlichen Berglandschaft mit Höhlen erstaunlich nah kommt.

Allerdings stellte die neue Weltgenerierung folgende Hürde dar: Es müssen Millionen von Blöcken in die Datenbank geschrieben werden, um die gesamte Welt vor dem Betreten abspeichern zu können. Dies führte zu stundenlangen Wartezeiten, da ich jeden Block einzeln mit einem jeweiligen SQL-Befehl hochgeladen habe.
Deswegen integrierte ich alle Blöcke in eine einzelne Abfrage. Diese war allerdings so gross, dass der SQL-Server abstürzte.

Schlussendlich verpackte ich jeweils ungefähr 7500 Blöcke in eine Abfrage, was einen optimalen Kompromiss zwischen Performance und Stabilität darstellte.
Mit dieser Methode traten bedauerlicherweise erneut Probleme auf: Je mehr Blöcke sich in der Datenbank befanden, desto länger dauerten die Abfragen.
Ich fand heraus, dass der Server nach jeder erfolgreichen insert-Abfrage den Index aktualisierte, was bei einer zunehmenden Anzahl Blöcke einiges an Zeit beansprucht. Das Problem liess sich lösen, indem ich den Index vor der Weltgenerierung entfernte und danach wieder hinzufügte. Auf diese Weise lassen sich bis zu 160'000 Datensätze pro Sekunde erstellen, wodurch die serverseitige Performance akzeptabel war.

Das Laden und Entladen der Welt verlief nun ohne Probleme. Da jedoch tausende Blöcke zeitgleich dargestellt wurden, war das Rendering sehr rechenintensiv. Dies führte wiederum zu clientseitigen Performanceproblemen, die ich erst mehrere Jahre später durch eine eigene Voxel-Engine lösen konnte.
Eine Voxel-Engine sammelt Blöcke innerhalb eines bestimmten Bereichs und rendert sie alle als ein einzelnes Objekt, einem sogenannten Chunk. Sollte ein Block entfernt oder hinzugefügt werden, muss entsprechend der gesamte Chunk gelöscht, neu aufgebaut und gerendet werden, was allerdings weitaus weniger Leistung beansprucht als sämtliche Blöcke einzeln darzustellen.

In meiner von Grund auf neu entwickelten Version dieses Spiels implementierte ich solch eine Voxel-Engine und verzichtete stattdessen auf die Datenbank. Sämtliche Blöcke werden innerhalb des Arbeitsspeichers hinterlegt, bis die Welt als JSON-Datei exportiert wird. Der Exporter wandelt das entsprechende JavaScript-Objekt in einen String (Text) um, welcher anschliessend in eine JSON-Datei geschrieben wird. Um einen Speicherstand zu laden, muss lediglich die JSON-Datei importiert werden, wodurch der String zurück in ein JavaScript-Objekt umgewandelt wird.
Um die Performance zusätzlich zu steigern, verzichte ich während der Chunk-Generierung auf nicht sichtbare Flächen. Falls zwei Blöcke aneinander liegen, werden die beiden Flächen dazwischen nie dargestellt oder gar in Betracht gezogen.

Mithilfe dieser Optimierungen ist es nun möglich, zehntausende Blöcke zeitgleich zu betrachten, ohne dass Space of Cubes ungewollt zu einer Diashow mutiert. Ebenso lassen sich Welten nun in wenigen Sekunden generieren. Die neue, in 2024 entwickelte Version ist hier auf meiner Webseite spielbar:

Jetzt spielen

Um den Charakter zu bewegen, kann W, A, S, D, Shift und die Leertaste verwendet werden. Blöcke lassen sich mit der rechten Maustaste platzieren und mit der linken abbauen. Mithilfe des Mausrads kann zwischen den verfügbaren Blocktypen gewechselt werden.