HTML-Spiele mit Phaser
Im dritten Teil vereinfachen wir das Leveldesign. Statt alle Spiel-Elemente im Code zu platzieren, bauen wir den Level mithilfe eines visuellen Tools. Dafür ergänzen wir unseren Work ow mit dem Editor Tiled.
Das erste Level entsteht
N ach den ersten beiden Teilen beinhaltet unser Spiel bereits alle grundlegenden Funktionen. Sie nden diese beiden Artikel als PDFs auf der beiliegenden DVD. Bisher mussten wir alle Spielelemente – beispielsweise die Plattformen oder den Sauerstoff – einzeln platzieren. Sie können so arbeiten; aber es wird recht mühsam, wenn Sie größere Level konzipieren und schnell Änderungen ausprobieren möchten. In diesem Teil vereinfachen wir daher die Arbeit am Leveldesign, indem wir den frei verfügbaren Tile Map Editor Tiled in unseren Work ow einbinden ( mapeditor.org). Installieren Sie die Software mit dem Tiled installer for Windows unter thorbjorn.itch.io/tiled. Das Tool bietet eine ganze Reihe nützlicher Funktionen, die unter doc.mapeditor.org dokumentiert sind. Für unser Spiel benötigen wir allerdings nur einen kleinen Teil davon. In den nächsten Schritten werden wir einen neuen Level in Tiled aufbauen und die Daten als JSON-Datei exportieren, um sie dann in Phaser zu importieren.
In Tiled klicken Sie zu Beginn direkt auf Neue Karte. Legen Sie die folgenden Eigenschaften fest: Orientierung: Orthogonal, Kachelebenenformat: Base64 (unkomprimiert), Kachel-Zeichenreihenfolge: Rechts Runter. Unter Kachelgröße wählen Sie 32 × 32 Pixel. Bei der Kartengröße legen Sie 50 × 20 Kacheln fest. Das entspricht effektiv der Größe 1600 × 640 Pixel, die wir im letzten Teil benutzt haben. Dann speichern Sie die Datei zum Beispiel unter level.tmx, für unsere Zwecke am besten im Ordner /assets.
In der Mitte sehen Sie nun ein Raster von 50 × 20 Kacheln. Diese wollen wir mit Gra ken füllen. Dazu klicken Sie im Reiter rechts unten auf Neues Tilset. Ein Tileset beinhaltet einzelne Tiles, kleine Kachel-Elemente, die sich zu beliebigen Leveln zusammensetzen lassen (siehe Abbildung). Geben Sie als Namen spacerush ein und wählen als Quelle das Tileset tileset.png in den /assets. Den Haken bei In Karte einbetten können Sie rausnehmen. Die Kachelbreite und -höhe entspricht jeweils 32 Pixel. Tiled lädt nun das tileset.png in den Reiter und unterteilt die Gra k in einzelne Kacheln mit der Größe 32 × 32 Pixel. Sie können eines dieser Teilstücke anklicken und damit in der Mitte des Programms den Level füllen. Dazu reichen uns aus der Icon-Zeile oben drei Funktionen. Mit dem Stempel füllen Sie ein Feld im Raster mit dem Ausschnitt, der im Tileset ausgewählt ist. Mit dem Füllwerkzeug überschreiben Sie leere Felder – oder aber zusammenhängende Felder mit dem gleichen Tile – mit der ausgewählten Kachel. Das hilft Ihnen zum Beispiel, um den Hintergrund schnell mit demselben Tile zu füllen. Mit dem Radiergummi wiederum löschen Sie einzelne Tiles.
Level bauen in Tiled
Für unser Spiel benötigen wir in Tiled vier verschiedene Ebenen. back: Eine Ebene für den Hintergrund. Diese ersetzt das bisherige Hintergrundbild. sprites: Eine Hilfs-Ebene, um Zahnräder und Sauerstoff abzubilden. Beim Import werden wir diese Elemente später durch Sprites ersetzen. platforms: Eine Ebene, die verschiedene Arten von Plattformen enthalten kann. Das sind Kacheln, auf denen sich die Spiel gur bewegen kann. front: Eine Ebene für Elemente, die im Vordergrund liegen. Das funktioniert analog zum Hintergrund, nur dass die Spiel gur hinter diesen Elementen stehen wird.
Tiled kennt verschiedene Arten von Ebenen. In unseren Fall können wir alle benötigten Funktionen über Kachelebenen abbilden. Legen Sie im Reiter rechts oben vier Ebenen an: back, sprites, platforms, front.
Nun bauen Sie sich aus den möglichen Elementen einen Level zusammen. Sie klicken auf das passende Element aus dem Tileset, wählen die richtige Ebene und platzieren das Element überall dort, wo es für Ihren Level sinnvoll ist.
Das Tileset ist so aufgebaut, dass die erste Zeile die Plattform-Tiles enthält. Die unteren Tiles sind für den Hintergrund gedacht, die vier Tiles mit Kristallen für den Vordergrund. Das Sägeblatt und das SauerstoffIcon werden für die Ebene sprites benötigt. Der Level kann zum Beispiel so aussehen wir in der Abbildung. Spiel gur und Raumschiff sind in diesem Fall nicht im Tileset vorgesehen. Sie könnten beide Elemente ebenso wie Sägeblatt und Sauerstoff handhaben – sie also ins Tileset aufnehmen, in Tiled einbauen und beim Import ersetzen. Da es sich aber um zwei einzelne Elemente handelt, können wir sie schneller direkt im Code platzieren. Über Datei > Exportieren als speichern Sie Ihr Leveldesign nun als level.json in den Ordner /assets.
Vorbereiten der preload-Funktion
Für Teil 3 lassen wir das monochrome PixelArt-Design aus Teil 2 hinter uns und testen stattdessen ein vektorbasiertes Neon-Design. Außer den neuen Gra ken entspricht die Code-Basis dem letzten Stand aus Teil 2. Sie können sich alle benötigten Dateien unter bit.ly/spacerush herunterladen. Darin ist auch der nale Stand nach Teil 3 enthalten. Da wir die Plattformen nun über Tiled und das Tileset einbauen, benötigen wir die alten Gra ken nicht mehr. Löschen Sie in der preload- Funktion die Zeilen 24 bis 27. Ergänzen Sie stattdessen eine Gra k für Sägeblätter, denen die Spiel gur später ausweichen muss:
In der create- Funktion können wir nun der Reihe nach die verschiedenen Ebenen abfragen und verarbeiten. Dabei werden Gra ken der Reihe nach von hinten nach vorne angeordnet. Wir starten also mit dem Hintergrund. Ausgehend vom aktuellen Stand löschen Sie in der create- Funktion die beiden Zeilen 40 und 41 zum bisherigen Hintergrund und ersetzen sie durch:
var map = this.add.tilemap("level"); var tileset = map.addTilesetImage
("spacerush", "tiles"); var backLayer = map.createStaticLayer
("back", tileset, 0, 0);
Mit der ersten Zeilen beziehen wir uns auf die JSON-Datei aus preload, die wir nun als Tilemap-Variable map ansprechen können. In der zweiten Zeile verknüpfen wir diese Daten mit dem Tileset-Bild. Hierbei entspricht der erste Parameter spacerush dem Namen, mit dem Sie das Tileset in Tiled eingefügt haben (der Name wurde in der JSONDatei übernommen); der zweite Parameter ist der Name des Tilesets aus der PreloadFunktion. Mit der dritten Zeile lesen wir die Ebene back aus der JSON-Datei aus und bauen daraus einen statischen Layer. Effektiv wird nun die Anordnung der Ebene in Phaser nachgebaut, und zwar so, dass die Spiel gur nicht damit interagieren kann.
Import der Sprites
Den Layer mit den Sprites handhaben wir etwas anders. Wir lesen die Ebene zunächst wie zuvor ein: var spriteLayer = map.createStaticLayer ("sprites", tileset, 0, 0);
Löschen Sie dann die bisherigen drei Zeilen zur oxygenGroup (das dürften aktuell die Zeilen 51 bis 53 sein). Stattdessen bereiten wir zwei Gruppen vor: eine für den Sauerstoff und eine für die Zahnräder: oxygenGroup =
this.physics.add.staticGroup(); sawGroup =
this.physics.add.staticGroup();
Nun gehen wir die Tiles aus dem spriteLayer der Reihe nach durch. Je nach Kachel, das heißt je nach Index des Tiles, unterscheiden wir zwei Fälle: Beim Index 8 handelt es sich um Sauerstoff, den wir bei der oxygenGroup ergänzen. Bei Index 6 handelt es sich um die linke obere Ecke vom Zahnrad, das wir der sawGroup hinzufügen wollen. Hier lesen wir zunächst die Mitte des Tiles aus, addieren aber jeweils 16 Pixel für die x- und y-Position, um die Mitte des Zahrades zu ermitteln. Die anderen Indizes für das Zahnrad können wir beim Import ignorieren. Im Code regeln wir das über: spriteLayer.forEachTile(tile => { if ( tile.index == 8) { var x = tile.getCenterX(); var y = tile.getCenterY(); var oxygen = oxygenGroup.create(x, y, "oxygen"); } if ( tile.index == 6) { var x = tile.getCenterX(); var y = tile.getCenterY(); var saw = sawGroup.create(x+16, y+16, "saw"); } });
Zunächst liegen die neuen Sprites für den Sauerstoff und die Sägeblätter noch über den Tiles aus dem spriteLayer. Da wir diesen Hilfs-Layer nicht mehr benötigen, blenden wir ihn wie folgt aus:
spriteLayer.visible = false; Import der Plattformen
Als nächstes übernehmen wir die Ebene platforms. Ausgehend vom aktuellen Stand löschen Sie in der create- Funktion die Zeilen der bisherigen platforms (Zeilen 61 bis 64) und ersetzen sie durch: var platforms = map.createStaticLayer
("platforms", tileset, 0, 0); platforms.setCollisionBetween(1, 5);
Wir nutzen hier wieder einen StaticLayer. Neu ist die zweite Zeile, die festlegt, dass Kollisionen auftreten können. Und zwar bei Tiles mit Indizes zwischen 1 und 5. Das entspricht der ersten Zeile aus dem Tilset mit den Plattform-Tiles. Da unser Programm weiter unten immer noch die Zeile this.physics.add.collider(player,platforms) enthält, werden Kollisionen zwischen der Spiel gur und dem StaticLayer platforms abgefragt.
Falls Sie ein anderes Tilset für Ihr Spiel benutzen, das an verschiedenen, nicht zusammenhängenden Positionen Tiles für Plattformen enthält, könnten Sie die nötigen Indizes auch nacheinander hinzufügen, zum Beispiel für die Indizes 5 und 8: platforms.setCollisionBetween(5, 5); platforms.setCollisionBetween(8, 8);
Nun kann die Spiel gur zwar auf den Plattformen stehen, aber nicht mehr springen. Das liegt daran, dass die Abfrage player. body.touching.down nur für dynamische Elemente gedacht ist und bei einem StaticLayer nicht greift. Ersetzen Sie das Springen in der update- Funktion durch: if (cursors.up.isDown && ( player.body. touching.down || player.body.blocked. down ) ) { player.setVelocityY(-400); }
Dadurch haben Sie beide Fälle abgedeckt. Das player.body.touching.down greift, wenn die Spiel gur unten ein anderes Sprite berührt. Das player.body.blocked.down greift, wenn die Spiel gur unten von Tiles blockiert wird. Nun können Sie die Sprites
für ship und player hinzufügen. Das funktioniert so wie im letzten Teil. Aber je nach Levelaufbau müssen Sie wahrscheinlich die Koordinaten anpassen.
Der letzten Ergänzungen
Als letzten Element kommt nun der frontLayer hinzu. Dieser Layer wird genauso eingebaut wie der backLayer: var frontLayer = map.createStaticLayer ("front", tileset, 0, 0);
Sobald sich die Kamera bewegt, kann es ein kleines Problem mit dem Tileset geben: Eventuell sehen Sie unerwünschte Linien. Das liegt daran, dass Phaser für die Position der Spiel gur Nachkommastellen vorsieht. Da die Kamera dem Spieler folgt, führt das effektiv dazu, dass das Tilset nicht pixelgenau eingesetzt wird. Um das zu korrigieren, ergänzen Sie in der update- Funktion: player.body.x = Math.round(player.body.x);
Dadurch gibt es bei der x-Position keine Nachkommastellen mehr. Scrollt Ihr Spiel auch in y-Richtung, ergänzen Sie einfach eine analoge Zeile für player.body.y.
Sägeblätter in Aktion
Die Gra ken für den Sauerstoff drehen sich bereits, weil unsere Programmierung aus dem letzten Teil hier automatisch greift. Für die Sägeblätter kopieren wir das Prinzip in der update- Funktion. Wir ändern lediglich die Geschwindigkeit: sawGroup.children. iterate(function(child) { child.angle += 1;
});
Jetzt müssen wir noch dafür sorgen, dass der Spieler auch dann verliert, wenn er mit seiner Spiel gur in eines der Sägeblätter läuft. Ergänzen Sie dazu in der create- Funktion eine weitere Kollisionsabfrage. Am besten an der Stelle, an der auch die anderen collider- und overlap-Abfragen zu nden sind (um Zeile 75 herum). this.physics.add.overlap(player, sawGroup, playerDeath, null, this);
Die Zeile greift, sobald sich die Sprites der Spiel gur player und einem Sägeblatt aus der sawGroup überlappen. Dann wird die Funktion playerDeath aufgerufen. Da wir diese Funktion bereits in Teil 2 geschrieben haben, sind wir hier auch schon fertig. Im nächsten Teil schauen wir uns den Scene Manager an, fügen einen Titelscreen hinzu und denken uns weitere Hindernisse aus, um dem Spieler das Leben schwerer zu machen.