Erstelle ein einfaches Asteroiden-Spiel mit komponentenbasierten Entitäten
German (Deutsch) translation by Władysław Łucyszyn (you can also view the original English article)
Im vorherigen Lernprogramm haben wir ein bodennahes komponentenbasiertes Entity-System erstellt. Jetzt werden wir dieses System verwenden, um ein einfaches Asteroids-Spiel zu erstellen.
Finale Ergebnisvorschau
Hier ist das einfache Asteroids-Spiel, das wir in diesem Tutorial erstellen werden. Es ist mit Flash und AS3 geschrieben, aber die allgemeinen Konzepte gelten für die meisten Sprachen.
Der vollständige Quellcode ist auf GitHub verfügbar.
Klassenübersicht
Es gibt sechs Klassen:
-
AsteroidsGame
, das die Basis-Spielklasse erweitert und die für unser Space-Shoot-Em-Up spezifische Logik hinzufügt. -
Schiff
, das ist die Sache, die du kontrollierst. -
Asteroid
, auf das du schießt. -
Bullet
, das ist die Sache, die Sie feuern. -
Pistole
, die diese Kugeln erzeugt. -
EnemyShip
, ein wandernder Alien, der nur dazu da ist, dem Spiel ein bisschen Abwechslung zu geben.
Lassen Sie uns nacheinander diese Entitätstypen durchgehen.
Die Schiffsklasse
Wir beginnen mit dem Spielerschiff:
package asteroids { import com.iainlobb.gamepad.Gamepad; import com.iainlobb.gamepad.KeyCode; import engine.Body; import engine.Entity; import engine.Game; import engine.Health; import engine.Physics; import engine.View; import flash.display.GraphicsPathWinding; import flash.display.Sprite; /** * ... * @author Iain Lobb - iainlobb@gmail.com */ public class Ship extends Entity { protected var gamepad:Gamepad; public function Ship() { body = new Body(this); body.x = 400; body.y = 300; physics = new Physics(this); physics.drag = 0.9; view = new View(this); view.sprite = new Sprite(); view.sprite.graphics.lineStyle(1.5, 0xFFFFFF); view.sprite.graphics.drawPath(Vector.<int>([1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]), Vector.<Number>([ -7.3, 10.3, -5.5, 10.3, -7, 0.6, -0.5, -2.8, 6.2, 0.3, 4.5, 10.3, 6.3, 10.3, 11.1, -1.4, -0.2, -9.6, -11.9, -1.3, -7.3, 10.3]), GraphicsPathWinding.NON_ZERO); health = new Health(this); health.hits = 5; health.died.add(onDied); weapon = new Gun(this); gamepad = new Gamepad(Game.stage, false); gamepad.fire1.mapKey(KeyCode.SPACEBAR); } override public function update():void { super.update(); body.angle += gamepad.x * 0.1; physics.thrust(-gamepad.y); if (gamepad.fire1.isPressed) weapon.fire(); } protected function onDied(entity:Entity):void { destroy(); } } }
Es gibt eine ganze Reihe von Implementierungsdetails hier, aber die Hauptsache ist, dass wir im Konstruktor Körper
-, Physik
-, Gesundheits
-, Sicht
- und Waffenkomponenten
instanziieren und
konfigurieren. (Die Waffenkomponente
ist tatsächlich eine Instanz von Gun
und nicht die Waffenbasisklasse.)
Ich verwende die Flash-Grafik-Zeichnungs-APIs, um mein Schiff zu erstellen (Zeilen 29-32), aber wir könnten genauso gut ein Bitmap-Bild verwenden. Ich erstelle auch eine Instanz meiner Gamepad-Klasse - dies ist eine Open-Source-Bibliothek, die ich vor ein paar Jahren geschrieben habe, um die Tastatureingabe in Flash zu vereinfachen.
Ich habe auch die Update
-Funktion von der Basisklasse überschrieben, um ein benutzerdefiniertes Verhalten hinzuzufügen: Nach dem Auslösen des gesamten Standardverhaltens mit super.update ()
rotieren wir das Schiff basierend auf der Tastatureingabe und feuern die Waffe, wenn der Feuerschlüssel ist gedrückt.
Wenn wir dem abgestorbenen
Signal der Gesundheitskomponente zuhören, lösen wir die onDied
-Funktion aus, wenn dem Spieler die
Trefferpunkte ausgehen. Wenn das passiert, sagen wir dem Schiff, dass es sich selbst zerstören soll.
Die Waffen
klasse
Als nächstes lass uns den Gun
-Kurs starten:
package asteroids { import engine.Entity; import engine.Weapon; /** * ... * @author Iain Lobb - iainlobb@gmail.com */ public class Gun extends Weapon { public function Gun(entity:Entity) { super(entity); } override public function fire():void { var bullet:Bullet = new Bullet(); bullet.targets = entity.targets; bullet.body.x = entity.body.x; bullet.body.y = entity.body.y; bullet.body.angle = entity.body.angle; bullet.physics.thrust(10); entity.entityCreated.dispatch(bullet); super.fire(); } } }
Das ist ein schöner kurzer! Wir überschreiben einfach die Funktion fire ()
, um einen neuen Bullet
zu erstellen, wann immer der Spieler feuert. Nachdem wir die Position und Rotation des Geschosses auf das Schiff abgestimmt und in die richtige Richtung verschoben haben, entsenden wir die EntityCreated
, so dass sie dem Spiel hinzugefügt werden kann.
Eine tolle Sache an dieser Gun
-Klasse ist, dass sie sowohl vom
Spieler als auch von gegnerischen Schiffen benutzt wird.
Die Bullet
-Klasse
Eine Waffe
erstellt eine Instanz dieser Bullet
-Klasse:
package asteroids { import engine.Body; import engine.Entity; import engine.Physics; import engine.View; import flash.display.Sprite; /** * ... * @author Iain Lobb - iainlobb@gmail.com */ public class Bullet extends Entity { public var age:int; public function Bullet() { body = new Body(this); body.radius = 5; physics = new Physics(this); view = new View(this); view.sprite = new Sprite(); view.sprite.graphics.beginFill(0xFFFFFF); view.sprite.graphics.drawCircle(0, 0, body.radius); } override public function update():void { super.update(); for each (var target:Entity in targets) { if (body.testCollision(target)) { target.health.hit(1); destroy(); return; } } age++; if (age > 20) view.alpha -= 0.2; if (age > 25) destroy(); } } }
Der Konstruktor instanziiert und konfiguriert den Körper, die Physik und die Ansicht. In der Update-Funktion können Sie jetzt sehen, dass die Liste der Ziele
nützlich ist, da wir alle Dinge, die wir treffen wollen, durchgehen und sehen, ob einer von ihnen das Geschoss schneidet.
Dieses Kollisionssystem würde nicht auf Tausende von Kugeln skalieren, aber für die meisten Gelegenheitsspiele ist es in Ordnung.
Wenn das Geschoss mehr als 20 Frames alt ist, fangen wir an, es auszublenden, und wenn es älter als 25 Frames ist, zerstören wir es. Wie bei der Waffe
wird die Kugel
sowohl vom Spieler als auch vom Gegner benutzt - die Instanzen haben nur eine andere Zielliste.
Apropos ...
Die feindliche Schiff
sklasse
Sehen wir uns nun dieses feindliche Schiff an:
package asteroids { import engine.Body; import engine.Entity; import engine.Health; import engine.Physics; import engine.View; import flash.display.GraphicsPathWinding; import flash.display.Sprite; /** * ... * @author Iain Lobb - iainlobb@gmail.com */ public class EnemyShip extends Entity { protected var turnDirection:Number = 1; public function EnemyShip() { body = new Body(this); body.x = 750; body.y = 550; physics = new Physics(this); physics.drag = 0.9; view = new View(this); view.sprite = new Sprite(); view.sprite.graphics.lineStyle(1.5, 0xFFFFFF); view.sprite.graphics.drawPath(Vector.<int>([1, 2, 2, 2, 2]), Vector.<Number>([ 0, 10, 10, -10, 0, 0, -10, -10, 0, 10]), GraphicsPathWinding.NON_ZERO); health = new Health(this); health.hits = 5; health.died.add(onDied); weapon = new Gun(this); } override public function update():void { super.update(); if (Math.random() < 0.1) turnDirection = -turnDirection; body.angle += turnDirection * 0.1; physics.thrust(Math.random()); if (Math.random() < 0.05) weapon.fire(); } protected function onDied(entity:Entity):void { destroy(); } } }
Wie Sie sehen können, ist es ziemlich ähnlich der Klasse des
Spielerschiffs. Der einzige wirkliche Unterschied ist, dass wir in der update ()
- Funktion, anstatt die Kontrolle des Spielers über die Tastatur zu haben, etwas "künstliche Dummheit" haben, um das Schiff wandern zu lassen und zufällig zu feuern.
Die Asteroiden
klasse
Der andere Entitätstyp, auf den der Spieler schießen kann, ist der Asteroid selbst:
package asteroids { import engine.Body; import engine.Entity; import engine.Health; import engine.Physics; import engine.View; import flash.display.Sprite; /** * ... * @author Iain Lobb - iainlobb@gmail.com */ public class Asteroid extends Entity { public function Asteroid() { body = new Body(this); body.radius = 20; body.x = Math.random() * 800; body.y = Math.random() * 600; physics = new Physics(this); physics.velocityX = (Math.random() * 10) - 5; physics.velocityY = (Math.random() * 10) - 5; view = new View(this); view.sprite = new Sprite(); view.sprite.graphics.lineStyle(1.5, 0xFFFFFF); view.sprite.graphics.drawCircle(0, 0, body.radius); health = new Health(this); health.hits = 3; health.hurt.add(onHurt); } override public function update():void { super.update(); for each (var target:Entity in targets) { if (body.testCollision(target)) { target.health.hit(1); destroy(); return; } } } protected function onHurt(entity:Entity):void { body.radius *= 0.75; view.scale *= 0.75; if (body.radius < 10) { destroy(); return; } var asteroid:Asteroid = new Asteroid(); asteroid.targets = targets; group.push(asteroid); asteroid.group = group; asteroid.body.x = body.x; asteroid.body.y = body.y; asteroid.body.radius = body.radius; asteroid.view.scale = view.scale; entityCreated.dispatch(asteroid); } } }
Hoffentlich werden Sie sich daran gewöhnen, wie diese Entitätsklassen jetzt aussehen.
Im Konstruktor initialisieren wir unsere Komponenten und randomisieren die Position und Geschwindigkeit.
In der update ()
- Funktion überprüfen wir auf Kollisionen mit
unserer Zielliste - die in diesem Beispiel nur einen einzigen
Gegenstand hat - dem Schiff des Spielers. Wenn wir eine Kollision finden, beschädigen wir das Ziel und zerstören dann den Asteroiden. Auf der anderen Seite, wenn der Asteroid selbst beschädigt ist (d. H. Er wird von einer Kugel eines Spielers getroffen), schrumpfen wir ihn und erzeugen einen zweiten Asteroiden, was die Illusion erzeugt, dass er in zwei Teile gesprengt wurde. Wir wissen, wann dies zu tun ist, indem wir das "weh" -Signal der Gesundheitskomponente hören.
Die AsteroidsGame
-Klasse
Sehen wir uns schließlich die AsteroidsGame-Klasse an, die die gesamte Show steuert:
package asteroids { import engine.Entity; import engine.Game; import flash.events.MouseEvent; import flash.filters.GlowFilter; import flash.text.TextField; /** * ... * @author Iain Lobb - iainlobb@gmail.com */ public class AsteroidsGame extends Game { public var players:Vector.<Entity> = new Vector.<Entity>(); public var enemies:Vector.<Entity> = new Vector.<Entity>(); public var messageField:TextField; public function AsteroidsGame() { } override protected function startGame():void { var asteroid:Asteroid; for (var i:int = 0; i < 10; i++) { asteroid = new Asteroid(); asteroid.targets = players; asteroid.group = enemies; enemies.push(asteroid); addEntity(asteroid); } var ship:Ship = new Ship(); ship.targets = enemies; ship.destroyed.add(onPlayerDestroyed); players.push(ship); addEntity(ship); var enemyShip:EnemyShip = new EnemyShip(); enemyShip.targets = players; enemyShip.group = enemies; enemies.push(enemyShip); addEntity(enemyShip); filters = [new GlowFilter(0xFFFFFF, 0.8, 6, 6, 1)]; update(); render(); isPaused = true; if (messageField) { addChild(messageField); } else { createMessage(); } stage.addEventListener(MouseEvent.MOUSE_DOWN, start); } protected function createMessage():void { messageField = new TextField(); messageField.selectable = false; messageField.textColor = 0xFFFFFF; messageField.width = 600; messageField.scaleX = 2; messageField.scaleY = 3; messageField.text = "CLICK TO START"; messageField.x = 400 - messageField.textWidth; messageField.y = 240; addChild(messageField); } protected function start(event:MouseEvent):void { stage.removeEventListener(MouseEvent.MOUSE_DOWN, start); isPaused = false; removeChild(messageField); stage.focus = stage; } protected function onPlayerDestroyed(entity:Entity):void { gameOver(); } protected function gameOver():void { addChild(messageField); isPaused = true; stage.addEventListener(MouseEvent.MOUSE_DOWN, restart); } protected function restart(event:MouseEvent):void { stopGame(); startGame(); stage.removeEventListener(MouseEvent.MOUSE_DOWN, restart); isPaused = false; removeChild(messageField); stage.focus = stage; } override protected function stopGame():void { super.stopGame(); players.length = 0; enemies.length = 0; } override protected function update():void { super.update(); for each (var entity:Entity in entities) { if (entity.body.x > 850) entity.body.x -= 900; if (entity.body.x < -50) entity.body.x += 900; if (entity.body.y > 650) entity.body.y -= 700; if (entity.body.y < -50) entity.body.y += 700; } if (enemies.length == 0) gameOver(); } } }
Diese Klasse ist ziemlich lang (gut, mehr als 100 Zeilen!), Weil es eine Menge Dinge tut.
In startGame ()
erstellt und konfiguriert es 10 Asteroiden, das Schiff und das feindliche Schiff, und erstellt auch die "CLICK TO START" -Meldung.
Die Funktion start ()
pausiert das Spiel und entfernt die Nachricht, während die Funktion gameOver
das Spiel erneut pausiert und die Nachricht wiederherstellt. Die restart ()
Funktion wartet auf einen Mausklick auf den Game Over Bildschirm - wenn dies passiert, stoppt das Spiel und startet es erneut.
Die update ()
-Funktion durchläuft alle Feinde und krümmt alle, die vom Bildschirm verschwunden sind, sowie die Win-Bedingung, die besagt, dass keine Feinde mehr in der Liste der Feinde sind.
Nimm es weiter
Dies ist eine ziemlich nackte Knochenmaschine und ein einfaches Spiel, also lasst uns jetzt darüber nachdenken, wie wir es erweitern könnten.
- Wir könnten für jede Entität einen Prioritätswert hinzufügen und die Liste vor jeder Aktualisierung sortieren, sodass wir sicherstellen können, dass einige Entitätstypen immer nach anderen Typen aktualisiert werden.
- Wir könnten das Objekt-Pooling verwenden, so dass wir tote Objekte (z. B. Kugeln) wiederverwenden, anstatt nur Hunderte neuer Objekte zu erstellen.
- Wir könnten ein Kamerasystem hinzufügen, damit wir die Szene scrollen und zoomen können. Wir könnten die Body- und Physics-Komponenten erweitern, um Unterstützung für Box2D oder eine andere Physik-Engine hinzuzufügen.
- Wir könnten eine Inventarkomponente erstellen, sodass Entitäten Elemente transportieren können.
Neben der Erweiterung der einzelnen Komponenten müssen wir manchmal die IEntity
-Schnittstelle erweitern, um spezielle Arten von Entitäten mit speziellen Komponenten zu erstellen.
Zum Beispiel, wenn wir ein Plattform-Spiel machen, und wir haben eine neue Komponente, die all die spezifischen Dinge behandelt, die eine Plattform-Spiel-Figur braucht - sind sie auf dem Boden, berühren sie eine Mauer, wie lange waren sie schon in der Luft, können sie doppelt springen usw. - andere Entitäten müssen möglicherweise ebenfalls auf diese Informationen zugreifen. Aber es ist nicht Teil der Kern-Entity-API, die absichtlich sehr allgemein gehalten wird. Daher müssen wir eine neue Schnittstelle definieren, die Zugriff auf alle standardmäßigen Entitätskomponenten bietet, aber den Zugriff auf die PlatformController
-Komponente hinzufügt.
Dafür würden wir etwas tun wie:
package platformgame { import engine.IEntity; /** * ... * @author Iain Lobb - iainlobb@gmail.com */ public interface IPlatformEntity extends IEntity { function set platformController(value:PlatformController):void; function get platformController():PlatformController; } }
Jede Entität, die "Plattformfunktionalität" benötigt, implementiert diese Schnittstelle und ermöglicht anderen Entitäten die Interaktion mit der PlatformController
-Komponente.
Schlussfolgerungen
Ich wage sogar, über die Spielarchitektur zu schreiben, ich fürchte, ich rühre ein Hornissennest der Meinung auf - aber das ist (meistens) immer eine gute Sache, und ich hoffe zumindest, dass ich dich darüber nachdenken lasse, wie du dich organisierst Code.
Letztendlich glaube ich nicht, dass Sie sich zu sehr darauf festlegen sollten, wie Sie die Dinge strukturieren. Was immer für dich funktioniert, um dein Spiel zu machen, ist die beste Strategie. Ich weiß, dass es weit fortgeschrittenere Systeme gibt, die ich hier umreiße, die eine Reihe von Problemen lösen, die über die besprochenen hinausgehen, aber sie können dazu neigen, sehr ungewohnt zu erscheinen, wenn Sie an eine traditionelle vererbungsbasierte Architektur gewöhnt sind.
Ich mag den Ansatz, den ich hier vorgeschlagen habe, weil er es
ermöglicht, Code nach Zweck, in kleinen fokussierten Klassen zu
organisieren, während er eine statisch typisierte, erweiterbare
Schnittstelle bereitstellt und sich nicht auf dynamische
Sprachfunktionen oder String
-Lookups verlässt. Wenn Sie das Verhalten einer bestimmten Komponente ändern möchten, können Sie diese Komponente erweitern und die Methoden überschreiben, die Sie ändern möchten. Die Klassen bleiben meist sehr kurz, so dass ich nie durch Tausende von Zeilen scrollen muss, um den Code zu finden, nach dem ich suche.
Das Beste von allem, ich bin in der Lage, eine einzige Engine zu haben, die flexibel genug ist, um alle Spiele, die ich mache, zu verwenden, was mir eine Menge Zeit spart.
Envato Tuts+ tutorials are translated into other languages by our community members—you can be involved too!
Translate this post