HTML-5-Spiele mit Phaser
Im zweiten Teil fügen wir Animationen, einen Counter und Optionen für das Spielende hinzu. Die Game Engine kümmert sich um den Großteil der Arbeit, sodass wir mit wenigen Zeilen Code schnell vorankommen.
Teil 2 der Webworker-Serie
M it der Open-Source-Game-Engine Phaser entwickeln Sie HTML5-Spiele für Desktop und Mobile ( phaser.io). Wir nutzen die Engine für ein Jump ’n’ Run namens Space Rush. Sie nden den ersten Artikel als PDF auf der Heft-DVD. Darin haben wir ein paar Plattformen positioniert und dafür gesorgt, dass der Spieler eine Figur mit den Pfeiltasten steuern kann. Im zweiten Teil fügen wir Animationen und Spielmechaniken hinzu. Alle Codebeispiele können Sie unter bit.ly/space-rush herunterladen.
In der Spieleentwicklung arbeiten Sie sehr interativ. Es lohnt sich nicht, Gra ken auszuarbeiten, ohne zu wissen, ob die Spielmechaniken überhaupt funktionieren. Wir handhaben das ähnlich und benutzen in diesem Teil 1-Bit-Gra ken. Das bedeutet, dass (bis auf den Hintergrund) in den Assets alle Pixel entweder weiß oder transparent sind. In solch einem 1-Bit-Stil sind zum Beispiel die Indie Games Minit und Gato Roboto umgesetzt. Die neuen Gra ken be nden sich wieder im Ordner /assets. Wir laden in der preload- Funktion zunächst einmal alle Assets, die wir für den zweiten Teil benötigen. Ersetzen Sie die bisherige preloadFunktion durch: function preload () { this.load.image("background",
"assets/background.png"); this.load.image("platform",
"assets/platform_six.png"); this.load.image("oxygen",
"assets/oxygen.png");
this.load.image("spaceship",
"assets/spaceship.png"); this.load.image("game_won",
"assets/game_won.png"); this.load.image("game_over",
"assets/game_over.png"); this.load.spritesheet("astronaut", "assets/astronaut.png", { frameWidth: 16, frameHeight: 32 } ); }
Mit der Funktion this.load.image() haben wir im ersten Teil statische Bilder geladen. Neu ist this.load.spritesheet(). Ein Spritesheet zeigt verschiedene Stufen einer oder mehrerer Animationen (siehe Abbildung), etwa für: Idle (Stillstehen), Run oder Jump. Beim Erstellen des Sprites player müssen wir uns nun entsprechend auf das Keyword astronaut statt square beziehen.
Animation der Spiel gur
Die Game Engine kümmert sich darum, dass die einzelnen Bereiche (Frames) im Spritesheet richtig geschnitten und der Reihe nach angezeigt werden. Das Bild astronaut.png hat die Maße 128 × 32 Pixel. Durch die Angabe von frameWidth und frameHeight schneidet Phaser das Spritesheet automatisch in einzelne Frames, die ab Null gezählt werden. Hier ergeben sich also die Frames 0 bis 7. Vorgesehen sind sehr einfache Animationen für Idle (Frames 0 bis 3) und Run (Frames 4 bis 7). In Phaser bereiten wir diese Bewegungen in der create- Funktion vor. Ergänzen Sie dort: this.anims.create({ key: "idle", frames: this.anims. generateFrameNumbers("astronaut", { start: 0, end: 3 }), frameRate: 5, repeat: -1
}); this.anims.create({ key: "run", frames: this.anims. generateFrameNumbers("astronaut", { start: 4, end: 7 }), frameRate: 5, repeat: -1
});
Hier geben wir jeder Animation ein keyword, über das wir diese Animation gleich ansprechen können. In frames legen wir über start und end fest, zwischen welchen Frames sich die Animation im Spritesheet
astronaut bewegen soll. Die frameRate legt die Geschwindigkeit fest, mit der die Animation abläuft. Der Wert für repeat sorgt dafür, dass die Animation in einer Schleife abgespielt wird. Ergänzen Sie nun die Abfrage der Pfeiltasten in der update- Funktion: if (cursors.left.isDown) { player.setVelocityX(-160); player.flipX = true; player.anims.play("run", true); } else if (cursors.right.isDown) { player.setVelocityX(160); player.flipX = false; player.anims.play("run", true); } else { player.setVelocityX(0); player.anims.play("idle", true); }
Das true in player.anims.play sorgt dafür, dass das jeweils nächste Frame ausgewählt wird. Durch das ipX spiegeln wir die Spiel gur nur dann, wenn ein Pfeil nach rechts oder links gedrückt wurde. In der idle- Animation blickt der Spieler daher automatisch in die Richtung, in die er zuletzt gelaufen ist.
Ein Zähler für den Sauerstoff
In unserem Spiel Space Rush soll es darum gehen, dass der Spieler das Ziel in einem bestimmten Zeitlimit erreicht. Vielleicht hat der Astronaut einen kleinen Unfall gehabt und sein Sauerstoff geht zur Neige. Nun muss er schnell genug zum Raumschiff zurück. Wir wollen dem Spieler mitteilen, wieviel Sauerstoff (Zeit) ihm noch bleibt. Fügen Sie am Anfang des Skriptes, nach der var con g, folgende Variablen hinzu: var text; var timedEvent; var timeLeft = 15;
Nun ergänzen Sie in der create- Funktion: text = this.add.text(16, 16,
"Sauerstoff: " + timeLeft ); timedEvent = this.time.addEvent({ delay: 1000, callback: countdown, callbackScope: this, loop: true });
Hiermit setzen wir den Text Sauerstoff: 15 an die Koordinaten 16, 16. Dann erzeugen wir ein Event, das alle 1.000 Millisekunden, also einmal pro Sekunde, ausgelöst
wird. Darin rufen wir die Callback-Funktion countdown auf. Diese Funktion existiert noch nicht, also fügen wir sie ganz am Ende, nach der update- Funktion, hinzu: function countdown() { timeLeft -= 1; text.setText("Sauerstoff: "
+ timeLeft ); if ( timeLeft <= 0 ) {
playerDeath(); } }
Hier ziehen wir einfach eine Sekunde ab und aktualisieren den text. Bei Null soll der Spieler sterben. Wir sehen hier bereits eine eigene Funktion playerDeath() vor, die wir später hinzufügen. Wir könnten konkret auch timeLeft == 0 abfragen, aber vielleicht gibt es später Mechanismen, wodurch der Spieler schneller Sauerstoff verlieren kann, sodass ein <= 0 exibler ist.
Sauerstoff auftanken
Spätere Level werden vielleicht umfangreicher, daher wollen wir dem Spieler die Möglichkeit geben, unterwegs Sauerstoff aufzutanken. Dazu muss der Spieler ein Sauerstoff-Icon einsammeln, was ihm zusätzliche fünf Sekunden gibt. Die nötige Gra k haben wir bereits in preload geladen. Wir fügen sie in der create- Funktion hinzu: oxygenGroup = this.physics.add.
staticGroup(); oxygenGroup.create(208, 560, "oxygen"); oxygenGroup.create(400, 368, "oxygen");
Hier können Sie so viele Elemente ergänzen, wie Sie für Ihr Spiel benötigen. Wir können nun global die Kollision zwischen Spiel gur und Sauerstoff abfragen. Ergänzen Sie in der create- Funktion die Zeile: this.physics.add.overlap(player, oxygenGroup, countup, null, this);
Diese Zeile muss dabei an einer Stelle stehen, an der player und oygenGroup bereits existieren, sonst gibt es eine entsprechende Fehlermeldung in der Konsole des Browsers. In dieser Zeile entspricht countup einer neuen Funktion, die wir ganz am Ende des Skripts hinzufügen: function countup(player, oxygen) { oxygen.disableBody(true, true); timeLeft += 5; text.setText("Sauerstoff: "+ timeLeft ); }
Phaser weiß an dieser Stelle, welche Elemente zusammengestoßen sind. Wir können uns das einzelne Sauerstoff-Element herauspicken und per disableBody() ausschalten. Das Element wird dann nicht mehr angezeigt, und die Spiel gur kann nicht mehr damit interagieren.
Da alle Spielelemente weiß sind, ist es sinnvoll, dem Spieler deutlich anzuzeigen, dass er mit dem Sauerstoff interagieren kann.
Dazu wollen wir die Gra k etwas animieren. Wir könnten wieder ein Spritesheet und passende Animationen anlegen. Hier ist es einfacher, die Gra ken rotieren zu lassen. Phaser erlaubt es uns, sehr bequem über die einzelnen Kinder einer Gruppe zu iterieren. Fügen Sie in der Update-Funktion folgende Zeilen hinzu: oxygenGroup.children. iterate(function(child) { child.angle += 3;
});
Wir drehen damit jede Gra k in der oxygenGroup 60 mal pro Sekunde um drei Grad.
Ende des Spiels
Das Spiel kann auf zwei Arten enden: Entweder der Spieler erreicht sein Raumschiff oder ihm geht vorher der Sauerstoff aus. Wir sehen dafür zwei Bilder vor, die wir in der create- Funktion einbauen, aber erst einmal unsichtbar machen: messageGameOver = this.add.image
(400, 320, "game_over"); messageGameOver.visible = false; messageGameWon = this.add.image
(400, 320, "game_won"); messageGameWon.visible = false;
Nun haben wir alle Elemente für die Funktion playerDeath(), die wir am Ende des Skriptes hinzufügen: function playerDeath() { messageGameOver.visible = true; player.disableBody(true, true); timedEvent.paused = true; }
Wir zeigen hier den Hinweis Game Over an, entfernen die Spiel gur und stoppen unseren Timer. Für das zweite Spielende benötigen wir zunächst ein Raumschiff. Fügen Sie in der create- Funktion diese Zeilen hinzu: ship = this.physics.add.sprite
(704, 288, "spaceship"); ship.body.setAllowGravity(false);
Das Raumschiff muss als Sprite ergänzt werden und nicht als Image, damit wir eine Kollisionsabfrage nutzen können. Auf Sprites wirkt aber die Gravitation. Also stellen wir diese für das Raumschiff über die zweite Zeile aus. Analog zum Sauerstoff fragen wir in der create- Funktion die Kollision zwischen Spiel gur und Raumschiff ab:
this.physics.add.overlap(player, ship, playerWon, null, this);
Hier rufen wir die neue Funktion playerWon() auf. Also fügen wir diese wieder am Ende des Skriptes hinzu. Das funktioniert genauso wie in playerDeath(), nur zeigen wir ein anderes Bild: function playerWon() { messageGameWon.visible = true; player.disableBody(true, true); timedEvent.paused = true; }
Spielwelt und Kamera
Innerhalb von con g haben wir das canvasElement auf 800 × 640 Pixel festgelegt. Ohne weitere Angaben entspricht das der Größe der Spielwelt. Nun wollen wir eine Welt bauen, die 1600 × 640 Pixel groß ist – ohne die Größe des Canvas zu ändern. Dazu ergänzen Sie zu Beginn der create- Funktion: this.physics.world.bounds.width = 1600; this.physics.world.bounds.height = 640;
Wenn unser Spieler nach rechts läuft, soll ihm die Kamera folgen. Das funktioniert innerhalb der create- Funktion über: this.cameras.main.startFollow(player); this.cameras.main.setBounds(0, 0, this.physics.world.bounds.width, this.physics.world.bounds.height);
Auch hier muss diese Funktion nach der Erstellung des Objektes player eingefügt werden, andernfalls gibt es eine Fehlermeldung, weil player noch nicht existiert. Die erste Zeile sorgt dafür, dass der Spieler in der Mitte des Canvas angezeigt wird. Dann sind außen aber unschöne schwarze Flächen zu sehen. Die zweite Zeile sorgt dafür, dass die Kamera die Höhe und Breite der Spielwelt berücksichtigt. Die Spiel gur wird nun immer in der Mitte des Canvas bleiben; es sei denn, sie bewegt sich auf die Ränder der Spielwelt zu.
Nun können Sie die vorhandenen Assets in der Spielwelt neu verteilen. Im Ordner /assets sind zu diesem Zweck zwei weitere Größen für die Plattformen vorgesehen. Einige Elemente sollen sich aber gar nicht mit der Kamera mitbewegen. Ersetzen Sie die Zeile für den Hintergrund in der createFunktion durch: bg = this.add.image(400, 320, "background"); bg.setScrollFactor(0, 0);
Diese Angabe des setScrollFactor benötigen
Sie ebenso bei den Elementen messageGameOver, messageGameWon und text.
Das Spiel funktioniert nun in den Grundzügen. Den vollständigen Code nden Sie unter bit.ly/space-rush. Um das Spiel interessanter zu machen, fehlen noch gefährliche Hindernisse, Gegner oder vielleicht Türen und Schalter. Vorher kümmern wir uns aber um das grundsätzliche Level-Design. Beim aktuellen Stand ist es ziemlich lästig, Raumschiff, Plattformen und Sauerstoff zu verteilen, weil Sie für alle Elemente die Koordinaten der Mittelpunkte ausrechnen müssen, um alles ordentlich zu positionieren. Deutlich einfacher wird es, wenn wir die Level mit einem entsprechenden Editor auf Basis eines Tilesets bauen. Dafür nutzen wir im nächsten Teil den kostenlosen Level Editor Tiled ( mapeditor.org).