#3 Ein Gefängnis mit der Maus bauen, und Räume, die sich selbst finden
· Nick
Der zweite Meilenstein ist fertig, und zum ersten Mal gibt es etwas zu sehen. Der Godot-Client sitzt jetzt auf dem Simulationskern, und man kann mit der Maus ein einstöckiges Gefängnis bauen: Böden legen, Wände ziehen, Türen setzen, Räume markieren, Dinge abreißen. Alles besteht aus einfachen Würfeln und flachen Farben, und Menschen gibt es noch keine. Aber es ist ein Ort, und es ist ein Ort, um den man mit der Kamera herumgehen kann.
Was man jetzt tun kann
Du startest ein neues Spiel, wählst eine Kartengröße (64, 128 oder 256 Kacheln im Quadrat) und optional einen Seed und bekommst ein leeres Raster. Die Kamera ist orthografisch mit fester Neigung. Sie dreht sich in strengen 90°-Schritten, zoomt zum Cursor hin, verschiebt sich mit den Tasten oder der mittleren Maustaste und hat einen Draufsicht-Modus zum Bauen. Bei einer frei drehbaren Kamera würden Wände die Räume verdecken, also dreht sie sich nicht frei.
Böden und Abriss nehmen ein Rechteck. Wände folgen einem Umriss (oder einer geraden Linie, wenn du Shift hältst). Eine Tür ist ein Klick, und sie wählt ihre Richtung anhand der benachbarten Wände, mit einer Taste zum Drehen. Während du ziehst, wird der Bereich unter dem Cursor als Geisterbild gezeigt: grüne Kacheln werden gebaut, rote übersprungen, und eine Beschriftung sagt, wie viele und warum. Nichts geht an die Welt, bis du loslässt, und wenn der Kern sagt, dass sich keine einzige Kachel ändern würde, wird gar nichts gesendet. Rechtsklick oder Escape bricht ab.
Das Weltmodell hat mehrere Etagen, also gibt es eine Etagenauswahl, und Etagen über der aktuellen werden ausgeblendet. Räume haben ein eigenes Werkzeug, dazu unten mehr. Ein Debug-Overlay zeigt Chunk-Grenzen, begehbare Kacheln, die Kachel unter dem Cursor und eine Zeitanzeige. Jedes Wort der Oberfläche ist Englisch.
Die Vorschau ist das Echte
Ein Geisterbild, das vom Ergebnis abweicht, ist schlimmer als gar keins. Deshalb rät der Client nicht, was ein Baubefehl tun wird. Der Kern kann für denselben Bereich beantworten, „was würde passieren, wenn du das ausführst“, Kachel für Kachel, mit einem Grund für jede Kachel, die er ablehnen würde. Die Prüfung, die die Vorschau füllt, ist dieselbe Regel, die der Befehl beim echten Ausführen nutzt, also können die beiden nicht auseinanderlaufen. Der Client zeichnet nur die Antwort. Er fragt den Kern erst wieder, wenn sich etwas geändert hat, das die Antwort beeinflusst: das Werkzeug, der gezogene Bereich, die Etage oder die Karte selbst.
Dasselbe Prinzip prägt den Rest des Clients: Er liest den Zustand der Welt und sendet Befehle und hat keine eigene Simulationslogik. Vorerst tickt der Kern im Hauptthread, und ein Befehl aus einem Klick wird zu Beginn des nächsten Frames angewendet, bevor die Welt tickt. Falls die Tick-Zeiten einmal ausschlagen, wenn die Welt voller Menschen ist, ist der Plan, den Kern in einen eigenen Thread zu verlegen, mit einer gemessenen Schwelle dafür, wann.
Räume, die sich selbst finden
Ich habe mir zwei Wege angesehen, Räume zu machen. Prison Architect lässt dich eine Zone zeichnen, die dann ein eigenes Objekt mit eigenen Kacheln ist. Das ist leicht zu verstehen, geht in einem Gefängnis aber schief: Ein ganzer Zellenblock wäre eine einzige Zone, und nachdem du eine Wand umgebaut hast, musst du sie neu malen. Der andere Weg ist, umschlossene Bereiche automatisch zu finden, aber dann kannst du keinen Hof markieren, um den keine Wände stehen.
Gewählt habe ich eine Mischung. Gespeichert wird nur ein Raumtyp auf jeder Kachel: Zelle, Kantine, Waschraum, Krankenstation, Besuchsraum, Freistundenhof und so weiter. Alles andere wird aus den Wänden abgeleitet. Ein Bereich ist eine Gruppe offener Kacheln, die über Seiten verbunden sind, auf einer Ebene, und er ist umschlossen, wenn er weder den Kartenrand noch einen leeren Abgrund berührt. Ein Raum sind alle Kacheln eines Typs innerhalb eines Bereichs. Wände und Türen sind Grenzen, und ein Typ, der unter eine Wand gemalt wurde, bleibt erhalten, wird aber ignoriert, sodass er zurückkommt, wenn du die Wand abreißt.
Das ergibt ein Verhalten, das sich richtig anfühlt, ohne dass man es erklären muss. Zieh eine Wand durch einen Raum, und er wird zu zwei Räumen desselben Typs. Nimm die Wand weg, und sie verschmelzen. Setz eine Tür, wo eine Wand war, und nichts ändert sich. Ein ungültiger Raum wird nicht gelöscht, denn das wäre das Gegenteil von „ohne Schmerzen“; er wird rot dargestellt, mit den Gründen: nicht umschlossen, mit einem anderen Typ geteilt, zu klein oder ohne Tür. Du kannst ein Rechteck malen, einen ganzen umschlossenen Bereich mit einem Klick füllen oder ihn leeren.
Die Raumliste ist abgeleitete Information, also wird sie nicht gespeichert und ist nicht Teil des Welt-Hashs; nur der Typ auf jeder Kachel ist es. Sie wird nach Befehlen und nach Systemen neu aufgebaut, und eine unveränderte Welt zu prüfen kostet nichts. In diesem Meilenstein wird die geänderte Ebene vollständig neu berechnet, was auf einer Karte von 256×256 etwa eine halbe Millisekunde dauert; Chunk für Chunk kann warten, bis Wegregionen dieselbe Maschinerie brauchen. Die Regel, an die ich mich halte: Ein inkrementeller Neuaufbau muss dasselbe Ergebnis liefern wie ein vollständiger.
Nur neu bauen, was sich geändert hat, Etagen umsonst schneiden
Die Karte wird als ein Mesh pro Chunk und Ebene gezeichnet, nicht als ein Objekt pro Wand. Jeder Chunk hat im Kern einen Versionszähler, und der Renderer merkt sich, welche Version er zuletzt gezeichnet hat. Ändert sich die Welt, kommen nur Chunks mit neuerer Version in eine Warteschlange, und die Warteschlange wird mit einem Zeitbudget pro Frame abgearbeitet, sodass sich eine riesige Änderung über ein paar Frames verteilt, statt einen einzigen einzufrieren. Das Malen eines Raumtyps erhöht die Kartenversion nicht, weckt den Mesh-Bauer also gar nicht erst.
Der Etagenschnitt folgt derselben Idee, weniger zu tun. Etagen über der aktuellen werden einfach ausgeblendet. Die Wände der aktuellen Etage werden von einem Shader auf einen Meter gestaucht, mit einem gemeinsamen Material und einem Höhenwert, sodass das Wechseln der Etage oder das Umschalten auf volle Wände nichts neu aufbaut. Ein Durchschalten durch vier Ebenen löst null Mesh-Neuaufbauten aus.
Ein Spalt von zehn Zentimetern
Als ich bei der Schlussprüfung das erste einstöckige Gefängnis ausprobierte, lag auf der Ebene darüber ein Boden, und von der Seite sah man eine dünne helle Linie zwischen der Oberkante einer Wand und der Platte darüber. An einer Ecke sah man den Boden in der Luft hängen. Ich hatte den Etagenschnitt im Verdacht, dann Z-Fighting, dann die Kantenglättung.
Nichts davon. Der Boden war ein einzelnes flaches Viereck ohne Dicke, gezeichnet auf der Höhe seiner Ebene plus zehn Zentimeter. Die Wände der Ebene darunter reichten genau bis drei Meter. Der Boden der nächsten Ebene begann bei drei Meter zehn. Der Spalt war zehn Zentimeter breit, und durch ihn sah man die beleuchtete Oberseite der Wand. Horizontal passte alles perfekt, deshalb hat es eine Weile gedauert, es zu sehen.
Die Lösung: Ein Boden ist jetzt eine Platte mit echter Dicke, die bis zum Beginn ihrer Ebene hinunterreicht. Die Oberseite bleibt auf derselben Höhe, also bleiben das Auswählen von Kacheln mit der Maus und alle Overlays unverändert. Seitenflächen entfallen, wo ein Boden oder eine Wand im selben Chunk anschließt, und werden an Chunk-Grenzen gezeichnet, sodass das Mesh eines Chunks nie von seinen Nachbarn abhängt. Am Ende der Platte bleibt so eine dunkle Kante von zehn Zentimetern. Ich habe mir Screenshots von vorher und nachher angesehen und beschlossen, sie zu behalten. Sie liest sich wie die Kante eines Bodens, und das ist sie auch.
Hält es stand?
Die letzte Aufgabe des Meilensteins war, mit der Maus ein einstöckiges Gefängnis zu bauen und zu sehen, ob es wehtut. Meistens nicht. Was der Durchlauf neben dem Spalt oben noch fand, war klein und lästig: Die Werkzeugknöpfe verloren ihre Hervorhebung nicht, wenn man mit der Maus ein anderes Werkzeug wählte, und aus dem Dialog „Neues Spiel“ kam man mit Escape nicht mehr heraus. Beides ist behoben. Ein Pausenmenü steht auf der Ideenliste für später.
Als Nächstes
Als Nächstes kommt M2, Bewegung und Tagesablauf. Die Welt bekommt Regionen und Erreichbarkeit, eine Wegfindung über Etagen hinweg und Flussfelder für Menschenmengen. Der Kern hat endlich in jedem Tick echte Arbeit, also hört die Benchmark-Schranke auf zu schlafen. Siehe die Roadmap.