1. Code
  2. Game Development

Vermeiden des Blob-Antipatterns: Ein pragmatischer Ansatz zur Zusammensetzung von Entitäten

Scroll to top

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.