Vermeiden des Blob-Antipatterns: Ein pragmatischer Ansatz zur Zusammensetzung von Entitäten
German (Deutsch) translation by Alex Grigorovich (you can also view the original English article)
Die Organisation Ihres Spielcodes in die komponentenbasierten Entitäten, anstatt sich nur auf die Klassenvererbung zu verlassen, ist ein beliebter Ansatz in der Spieleentwicklung. In diesem Tutorial sehen wir uns an, warum Sie dies tun könnten, und richten mit dieser Technik eine einfache Spiel-Engine ein.
Einführung
In diesem Tutorial werde ich komponentenbasierte Spielentitäten untersuchen, und finden, warum Sie sie verwenden möchten, und einen pragmatischen Ansatz vorschlagen, um Ihren Zeh ins Wasser zu tauchen.
Da es sich um eine Geschichte über Code-Organisation und -Architektur handelt, beginne ich damit, den üblichen Haftungsausschluss "aus dem Gefängnis zu verlassen" einzufügen: Dies ist nur eine Möglichkeit, Dinge zu tun, es ist nicht "die eine Möglichkeit" oder vielleicht sogar die beste. aber es könnte für dich funktionieren. Persönlich möchte ich möglichst viele Ansätze kennenlernen und dann herausarbeiten, was zu mir passt.
Vorschau des Endergebnisses
In diesem zweiteiligen Tutorial erstellen wir dieses Asteroids-Spiel. (Der vollständige Quellcode ist auf GitHub verfügbar.) In diesem ersten Teil konzentrieren wir uns auf die Kernkonzepte und die allgemeine Spiel-Engine.
Welches Problem lösen wir?
In einem Spiel wie Asteroids haben wir möglicherweise ein paar grundlegende Arten von "Dingen" auf dem Bildschirm: Kugeln, Asteroiden, Spielerschiffe und feindliche Schiffe. Vielleicht möchten wir diese Grundtypen als vier separate Klassen darstellen, von denen jede den gesamten Code enthält, den wir zum Zeichnen, Animieren, Verschieben und Steuern dieses Objekts benötigen.
Obwohl dies funktioniert, ist es möglicherweise besser, dem Prinzip "Nicht wiederholen" (DRY) zu folgen und zu versuchen, einen Teil des Codes zwischen den einzelnen Klassen wiederzuverwenden – schließlich wird der Code zum Bewegen und Zeichnen eines Aufzählungszeichens sehr ähnlich, wenn nicht sogar identisch mit dem Code zum Bewegen und Zeichnen eines Asteroiden oder eines Schiffes.
So können wir unsere Rendering- und Bewegungsfunktionen in eine Basisklasse umgestalten, von der alles ausgeht. Aber Ship und EnemyShip müssen auch schießen können. An diesem Punkt könnten wir der Basisklasse die shoot-Funktion hinzufügen, eine "Giant Blob" -Klasse erstellen, die im Grunde alles kann, und nur sicherstellen, dass Asteroiden und Kugeln niemals ihre shoot-Funktion aufrufen. Diese Basisklasse würde bald sehr groß werden und jedes Mal ansteigen, wenn Entitäten in der Lage sein müssen, neue Dinge zu tun. Das ist nicht unbedingt falsch, aber ich finde kleinere, spezialisiertere Klassen einfacher zu pflegen.
Alternativ können wir die Wurzel der tiefen Vererbung gehen und so etwas wie EnemyShip extends Ship extends ShootingEntity extends Entity. Auch dieser Ansatz ist nicht falsch und funktioniert auch recht gut, aber wenn Sie weitere Arten von Entitäten hinzufügen, werden Sie feststellen, dass Sie die Vererbungshierarchie ständig neu anpassen müssen, um alle möglichen Szenarien zu bewältigen, und Sie können sich in eine Ecke drängen wobei ein neuer Entitätstyp die Funktionalität von zwei verschiedenen Basisklassen haben muss, was eine Mehrfachvererbung erfordert (die die meisten Programmiersprachen nicht bieten).
Ich habe selbst oft den Deep-Hierarchie-Ansatz verwendet, aber ich bevorzuge eigentlich den Giant-Blob-Ansatz, da zumindest dann alle Entitäten eine gemeinsame Schnittstelle haben und neue Entitäten einfacher hinzugefügt werden können (was ist, wenn alle Ihre Bäume eine A*-Pfadfindung haben?!)
Es gibt aber noch einen dritten Weg...
Zusammensetzung über Vererbung
Wenn wir uns das Asteroidenproblem in Bezug auf Dinge vorstellen, die Objekte möglicherweise tun müssen, erhalten wir möglicherweise eine Liste wie diese:
move()shoot()takeDamage()die()render()
Anstatt eine komplizierte Vererbungshierarchie zu erarbeiten, für die Objekte welche Aufgaben ausführen können, modellieren wir das Problem anhand von Komponenten, die diese Aktionen ausführen können.
Wir könnten zum Beispiel eine Health-Klasse mit den Methoden takeDamage(), heal() und die() erstellen. Dann kann jedes Objekt, das Schaden nehmen und sterben muss, eine Instanz der Health-Klasse "komponieren" - wobei "compose" im Grunde bedeutet, "einen Verweis auf seine eigene Instanz dieser Klasse zu behalten".
Wir könnten eine weitere Klasse namens View erstellen, um die Rendering-Funktionalität zu überwachen, eine namens Body für die Bewegung und eine namens Weapon für das Schießen.
Die meisten Entity-Systeme basieren auf dem oben beschriebenen Prinzip, unterscheiden sich jedoch darin, wie Sie auf die in einer Komponente enthaltenen Funktionen zugreifen.
Spiegeln der API
Ein Ansatz besteht beispielsweise darin, die API jeder Komponente in der Entität zu spiegeln, sodass eine Entität, die Schaden nehmen kann, eine Funktion takeDamage() hat, die selbst nur die Funktion takeDamage() ihrer Health-Komponente aufruft.
1 |
class Entity { |
2 |
private var _health:Health; |
3 |
//...other code...//
|
4 |
public function takeDamage(dmg:int) { |
5 |
_health.takeDamage(dmg); |
6 |
}
|
7 |
}
|
Sie müssen dann eine Schnittstelle namens IHealth erstellen, damit Ihre Entität implementieren kann, damit andere Objekte auf die Funktion takeDamage() zugreifen können. So könnte Ihnen ein Java OOP-Leitfaden dazu raten.
getComponent()
Ein anderer Ansatz besteht darin, jede Komponente einfach in einer Schlüsselwertsuche zu speichern, sodass jede Entität eine Funktion namens getComponent("componentName") hat, die eine Referenz auf die bestimmte Komponente zurückgibt. Sie müssen dann die Referenz, die Sie erhalten, wieder in den gewünschten Komponententyp umwandeln - etwa:
1 |
var health:Health = Health(getComponent("Health")); |
Im Grunde funktioniert das Entitäts-/Verhaltenssystem von Unity so. Es ist sehr flexibel, da Sie ständig neue Komponententypen hinzufügen können, ohne Ihre Basisklasse zu ändern oder neue Unterklassen oder Schnittstellen zu erstellen. Es kann auch nützlich sein, wenn Sie Konfigurationsdateien verwenden möchten, um Entitäten zu erstellen, ohne Ihren Code neu zu kompilieren, aber ich überlasse es jemand anderem, dies herauszufinden.
Öffentliche Komponenten
Der von mir bevorzugte Ansatz besteht darin, allen Entitäten eine öffentliche Eigenschaft für jeden Hauptkomponententyp zuzuweisen und Felder null zu lassen, wenn die Entität nicht über diese Funktionalität verfügt. Wenn Sie eine bestimmte Methode aufrufen möchten, "greifen" Sie einfach auf die Entität zu, um die Komponente mit dieser Funktionalität zu erhalten - rufen Sie zum Beispiel enemy.Health.takeDamage(5) auf, um einen Feind anzugreifen.
Wenn Sie versuchen, health.takeDamage() für eine Entität aufzurufen, die keine Health-Komponente hat, wird sie kompiliert, aber Sie erhalten einen Laufzeitfehler, der Sie darüber informiert, dass Sie etwas Dummes getan haben. In der Praxis passiert dies selten, da ziemlich offensichtlich ist, welche Arten von Entitäten welche Komponenten haben (z.B. hat ein Baum natürlich keine Waffe!).
Einige strenge OOP-Befürworter mögen argumentieren, dass mein Ansatz gegen einige OOP-Prinzipien verstößt, aber ich finde, dass er wirklich gut funktioniert, und es gibt einen wirklich guten Präzedenzfall aus der Geschichte von Adobe Flash.
In ActionScript 2 verfügte die MovieClip-Klasse über Methoden zum Zeichnen von Vektorgrafiken: Sie können beispielsweise myMovieClip.lineTo() aufrufen, um eine Linie zu zeichnen. In ActionScript 3 wurden diese Zeichenmethoden in die Graphics-Klasse verschoben, und jeder MovieClip erhält eine Graphics-Komponente, auf die Sie zugreifen können, indem Sie beispielsweise myMovieClip.graphics.lineTo() auf dieselbe Weise aufrufen, die ich für enemy.health.takeDamage() beschrieben habe. Wenn es für die Entwickler der ActionScript-Sprache gut genug ist, ist es für mich gut genug.
Mein System (vereinfacht)
Im Folgenden werde ich eine sehr vereinfachte Version des Systems beschreiben, das ich in allen meinen Spielen verwende. In Bezug auf die Vereinfachung sind es etwa 300 Codezeilen, verglichen mit 6.000 für meine vollständige Engine. Aber mit diesen 300 Zeilen können wir eigentlich schon einiges anstellen!
Ich habe gerade genug Funktionalität belassen, um ein funktionierendes Spiel zu erstellen, während der Code so kurz wie möglich gehalten wird, damit er leichter zu verstehen ist. Der Code wird in ActionScript 3 enthalten sein, aber eine ähnliche Struktur ist in den meisten Sprachen möglich. Es gibt einige öffentliche Variablen, die Eigenschaften sein könnten (d. h. hinter get- und set-Accessor-Funktionen stehen), aber da dies in ActionScript ziemlich ausführlich ist, habe ich sie der besseren Lesbarkeit wegen als öffentliche Variablen belassen.
Die IEntity-Schnittstelle
Beginnen wir mit der Definition einer Schnittstelle, die alle Entitäten implementieren:
1 |
package engine |
2 |
{
|
3 |
import org.osflash.signals.Signal; |
4 |
/**
|
5 |
* ...
|
6 |
* @author Iain Lobb - iainlobb@gmail.com
|
7 |
*/
|
8 |
public interface IEntity |
9 |
{
|
10 |
// ACTIONS
|
11 |
function destroy():void; |
12 |
function update():void; |
13 |
function render():void; |
14 |
|
15 |
// COMPONENTS
|
16 |
function get body():Body; |
17 |
function set body(value:Body):void; |
18 |
function get physics():Physics; |
19 |
function set physics(value:Physics):void |
20 |
function get health():Health |
21 |
function set health(value:Health):void |
22 |
function get weapon():Weapon; |
23 |
function set weapon(value:Weapon):void; |
24 |
function get view():View; |
25 |
function set view(value:View):void; |
26 |
|
27 |
// SIGNALS
|
28 |
function get entityCreated():Signal; |
29 |
function set entityCreated(value:Signal):void; |
30 |
function get destroyed():Signal; |
31 |
function set destroyed(value:Signal):void; |
32 |
|
33 |
// DEPENDENCIES
|
34 |
function get targets():Vector.<Entity>; |
35 |
function set targets(value:Vector.<Entity>):void; |
36 |
function get group():Vector.<Entity>; |
37 |
function set group(value:Vector.<Entity>):void; |
38 |
}
|
39 |
|
40 |
}
|
Alle Entitäten können drei Aktionen ausführen: Sie können sie aktualisieren, rendern und zerstören.
Sie haben jeweils "Slots" für fünf Komponenten:
- Ein
body, Handhabung Position und Größe. -
physics, Umgang mit Bewegung. -
health, Umgang mit Verletzungen. - Eine
weapon, die mit Angriffen umgeht. - Und schließlich eine
view, mit der Sie die Entität rendern können.
Alle diese Komponenten sind optional und können null belassen, aber in der Praxis verfügen die meisten Entitäten über mindestens ein paar Komponenten.
Eine statische Szenerie, mit der der Spieler nicht interagieren kann (vielleicht ein Baum zum Beispiel), würde nur einen Körper und eine Ansicht benötigen. Es würde keine Physik brauchen, da es sich nicht bewegt, es würde keine Gesundheit brauchen, da man es nicht angreifen kann, und es würde sicherlich keine Waffe brauchen. Das Schiff des Spielers in Asteroids hingegen würde alle fünf Komponenten benötigen, da es sich bewegen, schießen und verletzt werden kann.
Durch die Konfiguration dieser fünf Basiskomponenten können Sie die einfachsten Objekte erstellen, die Sie möglicherweise benötigen. Manchmal reichen sie jedoch nicht aus, und an diesem Punkt können wir entweder die Basiskomponenten erweitern oder neue zusätzliche Komponenten erstellen – beides werden wir später besprechen.
Als nächstes haben wir zwei Signale: entityCreated und destroyed.
Signals sind eine Open-Source-Alternative zu den nativen Ereignissen von ActionScript, die von Robert Penner entwickelt wurden. Sie sind sehr benutzerfreundlich, da Sie damit Daten zwischen dem Dispatcher und dem Listener übertragen können, ohne viele benutzerdefinierte Ereignisklassen erstellen zu müssen. Weitere Informationen zu deren Verwendung finden Sie in der Dokumentation.
Mit dem entityCreated Signal kann eine Entität dem Spiel mitteilen, dass eine weitere neue Entität hinzugefügt werden muss - ein klassisches Beispiel ist, wenn eine Waffe eine Kugel erzeugt. Das destroyed Signal informiert das Spiel (und alle anderen hörenden Objekte) darüber, dass diese Entität zerstört wurde.
Schließlich verfügt die Entität über zwei weitere optionale Abhängigkeiten: targets, bei denen es sich um eine Liste von Entitäten handelt, die angegriffen werden sollen, und group, bei der es sich um eine Liste von Entitäten handelt, zu denen sie gehört. Zum Beispiel könnte ein Spielerschiff eine Liste von Zielen haben, die alle Feinde im Spiel sein würde, und könnte zu einer Gruppe gehören, die auch andere Spieler und befreundete Einheiten enthält.
Die Entity-Klasse
Schauen wir uns nun die Entity-Klasse an, die diese Schnittstelle implementiert.
1 |
package engine |
2 |
{
|
3 |
import org.osflash.signals.Signal; |
4 |
|
5 |
/**
|
6 |
* ...
|
7 |
* @author Iain Lobb - iainlobb@gmail.com
|
8 |
*/
|
9 |
|
10 |
public class Entity implements IEntity |
11 |
{
|
12 |
private var _body:Body; |
13 |
private var _physics:Physics; |
14 |
private var _health:Health; |
15 |
private var _weapon:Weapon; |
16 |
private var _view:View; |
17 |
private var _entityCreated:Signal; |
18 |
private var _destroyed:Signal; |
19 |
private var _targets:Vector.<Entity>; |
20 |
private var _group:Vector.<Entity>; |
21 |
|
22 |
/*
|
23 |
* Anything that exists within your game is an Entity!
|
24 |
*/
|
25 |
|
26 |
public function Entity() |
27 |
{
|
28 |
entityCreated = new Signal(Entity); |
29 |
destroyed = new Signal(Entity); |
30 |
}
|
31 |
|
32 |
public function destroy():void |
33 |
{
|
34 |
destroyed.dispatch(this); |
35 |
|
36 |
if (group) group.splice(group.indexOf(this), 1); |
37 |
}
|
38 |
|
39 |
public function update():void |
40 |
{
|
41 |
if (physics) physics.update(); |
42 |
}
|
43 |
|
44 |
public function render():void |
45 |
{
|
46 |
if (view) view.render(); |
47 |
}
|
48 |
|
49 |
public function get body():Body |
50 |
{
|
51 |
return _body; |
52 |
}
|
53 |
|
54 |
public function set body(value:Body):void |
55 |
{
|
56 |
_body = value; |
57 |
}
|
58 |
|
59 |
public function get physics():Physics |
60 |
{
|
61 |
return _physics; |
62 |
}
|
63 |
|
64 |
public function set physics(value:Physics):void |
65 |
{
|
66 |
_physics = value; |
67 |
}
|
68 |
|
69 |
public function get health():Health |
70 |
{
|
71 |
return _health; |
72 |
}
|
73 |
|
74 |
public function set health(value:Health):void |
75 |
{
|
76 |
_health = value; |
77 |
}
|
78 |
|
79 |
public function get weapon():Weapon |
80 |
{
|
81 |
return _weapon; |
82 |
}
|
83 |
|
84 |
public function set weapon(value:Weapon):void |
85 |
{
|
86 |
_weapon = value; |
87 |
}
|
88 |
|
89 |
public function get view():View |
90 |
{
|
91 |
return _view; |
92 |
}
|
93 |
|
94 |
public function set view(value:View):void |
95 |
{
|
96 |
_view = value; |
97 |
}
|
98 |
|
99 |
public function get entityCreated():Signal |
100 |
{
|
101 |
return _entityCreated; |
102 |
}
|
103 |
|
104 |
public function set entityCreated(value:Signal):void |
105 |
{
|
106 |
_entityCreated = value; |
107 |
}
|
108 |
|
109 |
public function get destroyed():Signal |
110 |
{
|
111 |
return _destroyed; |
112 |
}
|
113 |
|
114 |
public function set destroyed(value:Signal):void |
115 |
{
|
116 |
_destroyed = value; |
117 |
}
|
118 |
|
119 |
public function get targets():Vector.<Entity> |
120 |
{
|
121 |
return _targets; |
122 |
}
|
123 |
|
124 |
public function set targets(value:Vector.<Entity>):void |
125 |
{
|
126 |
_targets = value; |
127 |
}
|
128 |
|
129 |
public function get group():Vector.<Entity> |
130 |
{
|
131 |
return _group; |
132 |
}
|
133 |
|
134 |
public function set group(value:Vector.<Entity>):void |
135 |
{
|
136 |
_group = value; |
137 |
}
|
138 |
}
|
139 |
}
|
Es sieht lang aus, aber das meiste davon sind nur diese ausführlichen Getter- und Setter-Funktionen (boo!). Der wichtige Teil, den Sie sich ansehen sollten, sind die ersten vier Funktionen: der Konstruktor, in dem wir unsere Signale erstellen; destroy(), wo wir das zerstörte Signal senden und die Entität aus ihrer Gruppenliste entfernen; update(), wo wir alle Komponenten aktualisieren, die für jede Spielschleife benötigt werden - obwohl dies in diesem einfachen Beispiel nur die physics-Komponente ist - und schließlich render(), wo wir die Ansicht anweisen, ihre Sache zu tun.
Sie werden feststellen, dass wir die Komponenten hier in der Entity-Klasse nicht automatisch instanziieren – das liegt daran, dass, wie ich bereits erläutert habe, jede Komponente optional ist.
Die einzelnen Komponenten
Sehen wir uns nun die Komponenten einzeln an. Zuerst die Karosseriekomponente:
1 |
package engine |
2 |
{
|
3 |
/**
|
4 |
* ...
|
5 |
* @author Iain Lobb - iainlobb@gmail.com
|
6 |
*/
|
7 |
|
8 |
public class Body |
9 |
{
|
10 |
public var entity:Entity; |
11 |
public var x:Number = 0; |
12 |
public var y:Number = 0; |
13 |
public var angle:Number = 0; |
14 |
public var radius:Number = 10; |
15 |
|
16 |
/*
|
17 |
* If you give an entity a body it can take physical form in the world,
|
18 |
* although to see it you will need a view.
|
19 |
*/
|
20 |
|
21 |
public function Body(entity:Entity) |
22 |
{
|
23 |
this.entity = entity; |
24 |
}
|
25 |
|
26 |
public function testCollision(otherEntity:Entity):Boolean |
27 |
{
|
28 |
var dx:Number; |
29 |
var dy:Number; |
30 |
|
31 |
dx = x - otherEntity.body.x; |
32 |
dy = y - otherEntity.body.y; |
33 |
|
34 |
return Math.sqrt((dx * dx) + (dy * dy)) <= radius + otherEntity.body.radius; |
35 |
}
|
36 |
}
|
37 |
}
|
Alle unsere Komponenten benötigen eine Referenz auf ihre Eigentümerentität, die wir an den Konstruktor übergeben. Der Körper hat dann vier einfache Felder: eine x- und y-Position, einen Drehwinkel und einen Radius, um seine Größe zu speichern. (In diesem einfachen Beispiel sind alle Entitäten kreisförmig!)
Diese Komponente hat auch eine einzige Methode: testCollision(), die Pythagoras verwendet, um den Abstand zwischen zwei Entitäten zu berechnen und diesen mit ihren kombinierten Radien zu vergleichen. (Mehr Infos hier.)
Als nächstes schauen wir uns die Physics-Komponente an:
1 |
package engine |
2 |
{
|
3 |
/**
|
4 |
* ...
|
5 |
* @author Iain Lobb - iainlobb@gmail.com
|
6 |
*/
|
7 |
public class Physics |
8 |
{
|
9 |
public var entity:Entity; |
10 |
public var drag:Number = 1; |
11 |
public var velocityX:Number = 0; |
12 |
public var velocityY:Number = 0; |
13 |
|
14 |
/*
|
15 |
* Provides a basic physics step without collision detection.
|
16 |
* Extend to add collision handling.
|
17 |
*/
|
18 |
|
19 |
public function Physics(entity:Entity) |
20 |
{
|
21 |
this.entity = entity; |
22 |
}
|
23 |
|
24 |
public function update():void |
25 |
{
|
26 |
entity.body.x += velocityX; |
27 |
entity.body.y += velocityY; |
28 |
|
29 |
velocityX *= drag; |
30 |
velocityY *= drag; |
31 |
}
|
32 |
|
33 |
public function thrust(power:Number):void |
34 |
{
|
35 |
velocityX += Math.sin(-entity.body.angle) * power; |
36 |
velocityY += Math.cos(-entity.body.angle) * power; |
37 |
}
|
38 |
}
|
39 |
|
40 |
}
|
Wenn Sie sich die Funktion update() ansehen, können Sie sehen, dass die Werte velocityX und velocityY zur Position des Objekts addiert werden, wodurch es bewegt wird, und die Geschwindigkeit wird mit drag multipliziert, wodurch das Objekt allmählich verlangsamt wird. Die Funktion thrust() ermöglicht eine schnelle Beschleunigung der Entity in die Richtung, in die sie zeigt.
Schauen wir uns als Nächstes die Komponente Health an:
1 |
package engine |
2 |
{
|
3 |
import org.osflash.signals.Signal; |
4 |
/**
|
5 |
* ...
|
6 |
* @author Iain Lobb - iainlobb@gmail.com
|
7 |
*/
|
8 |
public class Health |
9 |
{
|
10 |
public var entity:Entity; |
11 |
public var hits:int; |
12 |
public var died:Signal; |
13 |
public var hurt:Signal; |
14 |
|
15 |
public function Health(entity:Entity) |
16 |
{
|
17 |
this.entity = entity; |
18 |
died = new Signal(Entity); |
19 |
hurt = new Signal(Entity); |
20 |
}
|
21 |
|
22 |
public function hit(damage:int):void |
23 |
{
|
24 |
hits -= damage; |
25 |
|
26 |
hurt.dispatch(entity); |
27 |
|
28 |
if (hits < 0) |
29 |
{
|
30 |
died.dispatch(entity); |
31 |
}
|
32 |
}
|
33 |
|
34 |
}
|
35 |
|
36 |
}
|
Die Health-Komponente verfügt über eine Funktion namens hit(), mit der die Entität verletzt werden kann. In diesem Fall wird der hits-Wert reduziert und alle mithörenden Objekte werden durch das Senden des hurt Signals benachrichtigt. Wenn hits kleiner als null sind, ist die Entität tot und wir senden das died Signal.
Sehen wir uns an, was in der Weapon-Komponente enthalten ist:
1 |
package engine |
2 |
{
|
3 |
import org.osflash.signals.Signal; |
4 |
/**
|
5 |
* ...
|
6 |
* @author Iain Lobb - iainlobb@gmail.com
|
7 |
*/
|
8 |
public class Weapon |
9 |
{
|
10 |
public var entity:Entity; |
11 |
public var ammo:int; |
12 |
|
13 |
/*
|
14 |
* Weapon is the base class for all weapons.
|
15 |
*/
|
16 |
|
17 |
public function Weapon(entity:Entity) |
18 |
{
|
19 |
this.entity = entity; |
20 |
}
|
21 |
|
22 |
public function fire():void |
23 |
{
|
24 |
ammo--; |
25 |
}
|
26 |
}
|
27 |
}
|
Nicht viel hier! Das liegt daran, dass dies wirklich nur eine Basisklasse für die eigentlichen Waffen ist – wie Sie später im Gun-Beispiel sehen werden. Es gibt eine Methode fire(), die Unterklassen überschreiben sollten, aber hier reduziert sie nur den ammo-Wert.
Die letzte zu untersuchende Komponente ist View:
1 |
package engine |
2 |
{
|
3 |
import flash.display.Sprite; |
4 |
/**
|
5 |
* ...
|
6 |
* @author Iain Lobb - iainlobb@gmail.com
|
7 |
*/
|
8 |
public class View |
9 |
{
|
10 |
public var entity:Entity; |
11 |
public var scale:Number = 1; |
12 |
public var alpha:Number = 1; |
13 |
public var sprite:Sprite; |
14 |
|
15 |
/*
|
16 |
* View is display component which renders an Entity using the standard display list.
|
17 |
*/
|
18 |
|
19 |
public function View(entity:Entity) |
20 |
{
|
21 |
this.entity = entity; |
22 |
}
|
23 |
|
24 |
public function render():void |
25 |
{
|
26 |
sprite.x = entity.body.x; |
27 |
sprite.y = entity.body.y; |
28 |
sprite.rotation = entity.body.angle * (180 / Math.PI); |
29 |
sprite.alpha = alpha; |
30 |
sprite.scaleX = scale; |
31 |
sprite.scaleY = scale; |
32 |
}
|
33 |
}
|
34 |
}
|
Diese Komponente ist sehr spezifisch für Flash. Das Hauptereignis hier ist die Funktion render(), die ein Flash-Sprite mit den Positions- und Rotationswerten des Körpers sowie den Alpha- und Skalierungswerten aktualisiert, die es selbst speichert. Wenn Sie ein anderes Rendering-System wie copyPixels-Blitting oder Stage3D verwenden möchten (oder tatsächlich ein System, das für eine andere Plattformauswahl relevant ist), würden Sie diese Klasse anpassen.
Die Game-Klasse
Jetzt wissen wir, wie eine Entität und alle ihre Komponenten aussehen. Bevor wir diese Engine verwenden, um ein Beispielspiel zu erstellen, schauen wir uns den letzten Teil der Engine an – die Game-Klasse, die das gesamte System steuert:
1 |
package engine |
2 |
{
|
3 |
import flash.display.Sprite; |
4 |
import flash.display.Stage; |
5 |
import flash.events.Event; |
6 |
/**
|
7 |
* ...
|
8 |
* @author Iain Lobb - iainlobb@gmail.com
|
9 |
*/
|
10 |
|
11 |
public class Game extends Sprite |
12 |
{
|
13 |
public var entities:Vector.<Entity> = new Vector.<Entity>(); |
14 |
public var isPaused:Boolean; |
15 |
static public var stage:Stage; |
16 |
|
17 |
/*
|
18 |
* Game is the base class for games.
|
19 |
*/
|
20 |
|
21 |
public function Game() |
22 |
{
|
23 |
addEventListener(Event.ENTER_FRAME, onEnterFrame); |
24 |
addEventListener(Event.ADDED_TO_STAGE, onAddedToStage); |
25 |
}
|
26 |
|
27 |
protected function onEnterFrame(event:Event):void |
28 |
{
|
29 |
if (isPaused) return; |
30 |
|
31 |
update(); |
32 |
|
33 |
render(); |
34 |
}
|
35 |
|
36 |
protected function update():void |
37 |
{
|
38 |
for each (var entity:Entity in entities) entity.update(); |
39 |
}
|
40 |
|
41 |
protected function render():void |
42 |
{
|
43 |
for each (var entity:Entity in entities) entity.render(); |
44 |
}
|
45 |
|
46 |
protected function onAddedToStage(event:Event):void |
47 |
{
|
48 |
Game.stage = stage; |
49 |
|
50 |
startGame(); |
51 |
}
|
52 |
|
53 |
protected function startGame():void |
54 |
{
|
55 |
|
56 |
}
|
57 |
|
58 |
protected function stopGame():void |
59 |
{
|
60 |
for each (var entity:Entity in entities) |
61 |
{
|
62 |
if (entity.view) removeChild(entity.view.sprite); |
63 |
}
|
64 |
|
65 |
entities.length = 0; |
66 |
}
|
67 |
|
68 |
public function addEntity(entity:Entity):Entity |
69 |
{
|
70 |
entities.push(entity); |
71 |
|
72 |
entity.destroyed.add(onEntityDestroyed); |
73 |
entity.entityCreated.add(addEntity); |
74 |
|
75 |
if (entity.view) addChild(entity.view.sprite); |
76 |
|
77 |
return entity; |
78 |
}
|
79 |
|
80 |
protected function onEntityDestroyed(entity:Entity):void |
81 |
{
|
82 |
entities.splice(entities.indexOf(entity), 1); |
83 |
|
84 |
if (entity.view) removeChild(entity.view.sprite); |
85 |
|
86 |
entity.destroyed.remove(onEntityDestroyed); |
87 |
}
|
88 |
|
89 |
|
90 |
}
|
91 |
}
|
Hier gibt es viele Implementierungsdetails, aber lassen Sie uns nur die Highlights herausgreifen.
In jedem Frame durchläuft die Game-Klasse alle Entitäten und ruft deren Update- und Render-Methoden auf. In der Funktion addEntity fügen wir die neue Entität zur Entitätenliste hinzu, hören auf ihre Signale und fügen, wenn sie eine Ansicht hat, ihr Sprite zur Bühne hinzu.
Wenn onEntityDestroyed ausgelöst wird, entfernen wir die Entität aus der Liste und entfernen ihr Sprite von der Bühne. In der Funktion stopGame, die Sie nur aufrufen, wenn Sie das Spiel beenden möchten, entfernen wir alle Sprites der Entitäten von der Bühne und löschen die Entitätenliste, indem wir ihre Länge auf Null setzen.
Nächstes Mal...
Wow, wir haben es geschafft! Das ist die ganze Spiel-Engine! Von diesem Ausgangspunkt aus könnten wir viele einfache 2D-Arcade-Spiele ohne viel zusätzlichen Code erstellen. Im nächsten Tutorial verwenden wir diese Engine, um ein Weltraum-Shoot-em-Up im Stil von Asteroiden zu erstellen.



