Wie realisiert man ein Bubble Spiel objektorientiert

An dieser Stelle möchte ich euch zeigen, wie man ein Puzzlespiel schnell und einfach erstellen kann. Das Puzzle Spiel soll ein Feld mit verschiedenfarbigen Kugeln haben, mit Klick auf einer dieser Kugeln, sollen die gleichfarbigen Nachbarkugeln entfernt werden. Einstellbare Größen sind Feldbreite und Feldhöhe, minimale Farbkette und maximale Farben pro Feldaufbau. Ist ein Feld komplett abgebaut, startet das neue Level mit mehr Farben, andernfalls beginnt das Spiel von vorne.

Nun müssen wir überlegen, was für Klassen wir brauchen. Wir haben ein Feld und Kugeln, mehr nicht, daher bietet sich an, jeweils eine Klasse für das Feld und eine für die Kugel zu erstellen.  Dann benötigen wir noch einen Levelzyklus und eine Event-Klasse. Somit ergibt eine Struktur wie folgt (kurzer Anriss, eine genau Struktur kann in der jeweiligen Klassendatei eingesehen werden:

Main.as
Hauptprogramm zur Steuerung der Level und ein paar Anzeigen, Punktestand und Level.
Mögliche Methoden:

  • zur Aktualisierung der Anzeige
  • erstellen neuer Level

Field.as
Aufbau des Feldes und Steuerung der Kugeln, also für die Spielelogik.
Mögliche Methoden:

  • Feld erstellen
  • suchen nach Kombinationen
  • Mauseingabe verarbeiten
  • Atome hinzufügen und entfernen
  • Ereignisse auslösen für Levelende und Abbau von Atomen. Natürlich könnte man noch mehr Ereignisse auslösen, das sollte aber fürs erste reichen

Atom.as
Die Kugel, ich nenne das kleinste Teil mal Atom, muss ja nicht unbedingt eine Kugel sein.
Mögliche Methoden:

  • Anzeige der Grafik
  • Positionieren der Grafik

FieldEvent.as
Für die Ereignisse, die von Field geworfen werden. Diese erbt von Event, der Event Klasse von Flash. Dies müssen wir, da der EventDispatcher, mit dem wir Ereignisse werfen können, nur Events verarbeiten kann.

Für ein einfaches Spiel reichen uns diese vier Klassen.

Wir verzichten auch auf ein Grafikasset und Zeichnen uns die Textfelder und Kugeln per ActionScript 3.0, nur für die Soundeffekte benötigen wir eine Flashdatei.

Ich habe das ganze mit FlashDevelop realisiert, wie man ein Projekt einrichtet wurde zuvor unter “Anleitung Teil 1 : ActionScript 3 Projekt mit FlashDevelop erstellen” beschrieben.

Die fertigen Klassen

Main.as

public class Main extends Sprite
	{

		/* Punkte */
		private var _score:int;

		/* Spielfeld */
		private var _field:Field;

		/* Zähler für abgebaute Atome pro Feldaufbau*/
		private var _deleteCount:int;

		public function Main():void
		{
			if (stage) init();
			else addEventListener(Event.ADDED_TO_STAGE, init);
		}

		private function init(e:Event = null):void
		{
			removeEventListener(Event.ADDED_TO_STAGE, init);
			// entry point

			// Verschiebung der Ansicht
			Atom.OFFSET_X = 5;
			Atom.OFFSET_Y = 0;

			// Minimale Anzahl der Kette
			Field.MIN_CHAIN = 2;
			// Maximale Anzahl der Farben
			Field.MAX_COLOR = 1;

			// Feld erstellen
			_field = new Field(17, 15);

			//Listener hinzufügen
			_field.addEventListener(FieldEvent.ATOMS_DELETED, updateHUD);
			_field.addEventListener(FieldEvent.NO_MORE_MOVES, onNoMoreMoves);

			// Auf Bühne legen
			addChild(_field);

			// Texfeld für die Punkte erstellen
			var tf:TextField = new TextField();
			tf.x = 10;
			tf.y = 255;
			tf.defaultTextFormat = new TextFormat("Verdana", 12, 0x000000, true);
			tf.name = "score";
			addChild(tf);

			// Punkte aktualisieren
			score = 0;

			// Atomzähler auf 0 setzen
			_deleteCount = 0;
		}

		/**
		 * Wird von FieldEvent aufgerufen
		 * @param	evt		FieldEvent
		 */
		private function updateHUD(evt:FieldEvent):void
		{
			// Punkte addieren
			score += (evt.amount * evt.amount);

			// Atomzähler setzen
			_deleteCount += evt.amount;
		}

		/**
		 * Wird von FieldEvent aufgerufen wenn keine Züge mehr möglich sind
		 * @param	evt			FieldEvent
		 * @param	count		für den setTimeout
		 */
		private function onNoMoreMoves(evt:FieldEvent, count:int = 0):void
		{
			// Nach Textfeld für den Countdown und Level suchen
			var ctf:TextField = getChildByName("counter_txt") as TextField;
			var tf:TextField = getChildByName("level_info_txt") as TextField;

			// Drei Sekunden Zähler
			if (count < 3)
			{
				if (ctf == null) // Textfeld für Countdown erstellen, falls nicht vorhanden
				{
					ctf = new TextField();
					ctf.defaultTextFormat = new TextFormat("Verdana", 20, 0xFF8000, true);

					ctf.autoSize = TextFieldAutoSize.CENTER;
					ctf.multiline = true;
					ctf.x = 150;
					ctf.y = 150;
					ctf.name = "counter_txt";
					ctf.filters = new Array(new BevelFilter(), new DropShadowFilter(10, 45, 0x4B4B4B, .5, 10, 10));
					addChild(ctf);
				}

				if (tf == null) // Textfeld für Level erstellen, falls nicht vorhanden
				{
					tf = new TextField();
					tf.defaultTextFormat = new TextFormat("Verdana", 20, 0xFF8000, true);

					tf.autoSize = TextFieldAutoSize.CENTER;
					tf.multiline = true;
					tf.x = 150;
					tf.y = 130;
					tf.name = "level_info_txt";
					tf.filters = new Array(new BevelFilter(), new DropShadowFilter(10, 45, 0x4B4B4B, .5, 10, 10));
					addChild(tf);
				}

				// Auswertung nur beim ersten Aufruf
				if (count == 0)
				{
					// Nach drei Aufrufe Feld zurücksetzen
					if (_deleteCount == _field.fieldWidth * _field.fieldHeight)
					{
						// Alle Atome abgebaut
						// Nächstes Level mit mehr Farben, maximale Farben, dann von vorne
						Field.MAX_COLOR = ++Field.MAX_COLOR > Atom.COLOR.length ? 1 : Field.MAX_COLOR;

						tf.text = "Level " + Field.MAX_COLOR + " ...";
					}
					else
					{
						// nicht alle Atome abgebaut
						tf.text = "Nicht geschafft, " + score + " Punkte";

						score = 0;
						Field.MAX_COLOR = 1;
					}
				}

				ctf.text = "Weiter in " + (3 - count) + " Sekunden";

				// nach einer Sekunde noch mal aufrufen
				setTimeout(onNoMoreMoves, 1000, evt, ++count);
				return;
			}

			removeChild(ctf);
			removeChild(tf);
			evt.target.reset();

			_deleteCount = 0;
		}

		/**
		 * Get und Set für Punkte
		 */
		public function set score(value:int):void
		{
			_score = value;
			var tf:TextField = getChildByName("score") as TextField;
			tf.text = _score + " Punkte";
		}

		public function get score():int
		{
			return _score;
		}

	}

Field.as

	public class Field extends MovieClip
	{
		/* Minimale Kette */
		public static var MIN_CHAIN:int = 2;

		/* Maximale Farben */
		public static var MAX_COLOR:int = -1;

		/* Feld Array länge max. Anzahl der Atome*/
		private var _field:Array;

		/* Feldgröße, Anzahl Atome horizontal und vertikal */
		private var _width:int;
		private var _height:int;

		/* Ausgewählte Kette */
		private var _selectedChain:Array;

		/* Angeklickets Atom */
		private var _clickedAtom:Atom;

		/**
		 * Konstruktor Feld
		 * @param	width		Atome horizontal
		 * @param	height		Atome vertikal
		 */
		public function Field(width:int, height:int)
		{

			// Feldhintergrund
			graphics.beginFill(0xffffff);
			graphics.drawRect(0, 0, 300, 300)
			graphics.endFill();

			// Feld maximale Atomanzahl
			_field = new Array(width * height);

			// Größe setzen
			_width = width;
			_height = height;

			// Atome erstellen
			for (var x:int = 0; x < width; x++)
			{
				for (var y:int = 0; y < height; y++)
				{
					var atom:Atom = new Atom(x, y, MAX_COLOR);
					addAtomAt(x, y, atom);
				}
			}

			// Listener für die Maus erstellen
			addEventListener(MouseEvent.MOUSE_DOWN, handleMouse);
			addEventListener(MouseEvent.MOUSE_UP, handleMouse);

		}

		/**
		 * Kobinationen suchen
		 * @param	x				ab Position x
		 * @param	y				ab Position y
		 * @param	depth			Rekursionstiefe
		 * @param	combination		Gefundene Kombinationen
		 * @param	checked			Bereits geprüfte Atome
		 * @return					Kombination
		 */
		public function findCombinations(x:int, y:int, depth:int = 0, combination:Array = null, checked:Dictionary = null):Array
		{
			// Leere Kombination erstellen
			if (combination == null)
				combination = new Array();

			// Für bereits geprüfte steine.
			if (checked == null)
				checked = new Dictionary(true);

			// prüfen ob im zulässigem Bereich
			if (!isInBounds(x, y)) return null;

			// Zuprüfendes Atom holen
			var atom:Atom = getAtomAt(x, y);

			// Wenn leere Position beenden
			if (atom == null)
				return combination;

			// Atom als überprüft merken
			checked[atom] = atom;

			// Atom in die Kobination einfügen
			combination.push(atom);

			if (atom != null)
			{
				// In alle Richtungen suchen
				var a1:Atom = getAtomAt(x + 1, y) ;
				var a2:Atom = getAtomAt(x - 1, y) ;
				var a3:Atom = getAtomAt(x    , y + 1) ;
				var a4:Atom = getAtomAt(x    , y - 1) ;

				// Für Suche in X-Richtung rechts
				// Wenn Atom in Suchrichung existiert (Feldposition nicht leer)
				if (a1 != null)
				{
					// dann, wenn das Atom in Suchrichtung die gleiche Farbe hat und wenn das Atom noch nicht geprüft wurde
					if (a1.color == atom.color && checked[a1] == undefined)
					{
						// weiter in X-Richtung nach rechts suchen.
						findCombinations(x + 1, y    , depth + 1, combination, checked);
					}
				}

				// Kürzere schreibweise in die anderen Richtungen
				if (a2 != null) if (a2.color == atom.color && checked[a2] == undefined ) findCombinations(x - 1, y    , depth + 1, combination, checked);
				if (a3 != null) if (a3.color == atom.color && checked[a3] == undefined ) findCombinations(x    , y + 1, depth + 1, combination, checked);
				if (a4 != null) if (a4.color == atom.color && checked[a4] == undefined ) findCombinations(x    , y - 1, depth + 1, combination, checked);
			}

			// Depth = 0, wenn es der erste Aufruf war (nullte Rekursion) und wenn die Kobination nicht die minimale Kette hat wird null zurück gegeben (nichts gefunden)
			if (depth == 0 && combination.length < MIN_CHAIN)
			{
				combination = null;
			}

			// Ansosnten die gefundenen Atome zurückgeben
			return combination;
		}

		/**
		 * Wird vom MouseEvent aufgerufen
		 * @param	evt		MausEvent
		 */
		public function handleMouse(evt:MouseEvent):void
		{
			// Nacht type schauen
			switch (evt.type)
			{
				case MouseEvent.MOUSE_DOWN:
						/// Maus gedrückt und das Ziel ein Atom ist
						if (evt.target is Atom)
						{
							// Gedrücktes Atom merken
							_clickedAtom = evt.target as Atom;

							if (_selectedChain == null) // Wenn keine aktieve Kombination
							{
								// Klicksound abspielen
								new click().play();
								// Atom als ausgewählt markieren
								_clickedAtom.selected = true;
								// Kombination Auswählen
								selectCombination();
							}
							else
							{
								// Wenn eine Kombination ausgewählt ist
								//Flag für Atome entfernt
								var combiRemoved:Boolean = false;

								// Für alle Atome in Kombination
								for (var i:int = 0; i < _selectedChain.length; i++)
								{
									// Atom nicht mehr ausgewählt markieren
									Atom(_selectedChain[i]).selected = false;

									// Wenn das das geklickte Atom in der Kobination ist, wurde ein weiteres mal auf die Kobination geklickt
									if (_selectedChain[i] == _clickedAtom)
									{
										// Dann Kombination auflösen und Atome löschen
										combiRemoved = true;
										removeAndDeleteAtoms(_selectedChain);

										// Löschsound abspielen
										new push().play();
									}
								}

								// Ausgewählte Kobination auf null setzen
								_selectedChain = null;

								// Wenn Kombination nicht gelöscht, dann wurde eine andere Kombination gewählt
								if (!combiRemoved)
								{
									// Klicksound abspielen
									new click().play();
									// Neue Kombination auswählen
									selectCombination();
								}
							}
						}

					break;

				case MouseEvent.MOUSE_UP:
					// Wenn die Maus losgelassen wurde, keine Kombination und Atom ausgewählt ist
					if (_selectedChain == null && _clickedAtom != null)
					{
						// Das Atom auf normal setzen
						_clickedAtom.selected = false;
					}
					break;

			}
		}

		/**
		 * Eine Kombination auswählen
		 */
		public function selectCombination():void
		{
			// Nach Kombination suchen, anhand des ausgewählten Atoms
			_selectedChain = findCombinations(_clickedAtom.fieldX, _clickedAtom.fieldY);

			// Wenn Kombination gefunden
			if (_selectedChain != null)
			{
				// Alle Atome in der Kombination als ausgewählt markieren
				for (var i:int = 0; i < _selectedChain.length; i++)
				{
					Atom(_selectedChain[i]).selected = true;
				}
			}
		}

		/**
		 * GET und SET für Feldgröße
		 */
		public function get fieldHeight():int
		{
			return _height
		}

		public function get fieldWidth():int
		{
			return _width;
		}

		/**
		 * Holt ein Atom an einer gegebenen Position
		 * @param	x		Feld X
		 * @param	y		Feld Y
		 * @return			Atom oder null
		 */
		public function getAtomAt(x:int, y:int):Atom
		{
			// Bei ungültiger Position null
			if (!isInBounds(x, y)) return null;

			return _field[x + (y * fieldWidth)];
		}

		/**
		 * Fügt ein Atom an eienr gegebenen Feldpositon ein
		 * @param	x		Feld X
		 * @param	y		Feld Y
		 * @param	atom	Atom
		 */
		public function addAtomAt(x:int, y:int, atom:Atom):void
		{
			// Ungültige Position abfangen
			if (!isInBounds(x, y)) return;

			// Atom suchen
			_field[x + (y * fieldWidth)] = atom;

			// Atom gefunden
			if (atom != null)
			{
				// Positionieren und hinzufügen
				atom.fieldX = x;
				atom.fieldY = y;
				atom.move();
				addChild(atom);
			}
		}

		/**
		 * Löscht ein Atom an einer gegebenen Position und überschreibt diese mit null
		 * @param	x		Feld X
		 * @param	y		Feld Y
		 * @return			Gelöschte Atom
		 */
		public function removeAtomAt(x:int, y:int):Atom
		{
			// Ungültige Position abfangen
			if (!isInBounds(x, y)) return null;

			// Atom holen
			var a:Atom = getAtomAt(x, y);

			// Atom gefunden
			if (a != null)
			{
				// Atom löschen
				removeChild(a);
				// Position mit null überschreiben
				addAtomAt(x, y, null);
			}

			// Gelöschtes Atom zurückgeben
			return a;
		}

		/**
		 * Löscht und entfern mehrere Atome
		 * @param	atoms		Atom Array
		 */
		public function removeAndDeleteAtoms(atoms:Array):void
		{
			// Für alle Atome im Array
			for (var i:int = 0; i < atoms.length; i++)
			{
				// Atom löschen
				var a:Atom = removeAtomAt(atoms[i].fieldX, atoms[i].fieldY);

				// Wenn Atom gelöscht
				if (a != null)
				{
					// Frei gewordene Position melden zum nachrücken
					invokeFreePosistion(a.fieldX, a.fieldY);
				}
			}

			// Ereignis werfen, dass eine Kobination gelöscht wurde mit Kobinationslänge
			dispatchEvent(new FieldEvent(FieldEvent.ATOMS_DELETED, atoms.length));

			// Wenn es keine Kobinationen mehr gibt Ende melden
			if (hasMoreMoves() == null)
				dispatchEvent(new FieldEvent(FieldEvent.NO_MORE_MOVES));
		}

		/**
		 * Verarbeitet freigewordene Positionen. Aufrüken von Oben nach Unten und von Links nach Rechts
		 * @param	x		Feld X
		 * @param	y		Feld Y
		 */
		public function invokeFreePosistion(x:int, y:int):void
		{
			// Ungültige Positionnen abfangen
			if (!isInBounds(x, y)) return;

			// Atom von Oben
			var a:Atom = getAtomAt(x, y - 1);

			if (a != null) // Atom von Oben gefunden
			{
				// Atom löschen
				removeAtomAt(a.fieldX, a.fieldY);

				// Und an neue tiefere Position einfügen
				addAtomAt(x, y, a);

				// Nächstes Atom zum Nachrücken suchen
				invokeFreePosistion(x, y - 1);
			}
			else // Kein Atom gefunden, Atome nach rechts
			{
				// Atom in Linksrichtung suchen
				a = getAtomAt(x - 1, y);

				// Atom gefunden
				if (a != null)
				{
					// Atom entfernen
					removeAtomAt(a.fieldX, a.fieldY);

					// An neue Position setzen
					addAtomAt(x, y, a);

					// Weier nach links suchen
					invokeFreePosistion(x -1, y);
				}
			}
		}

		/**
		 * Überprüft ob x und y im gültigem Bereich sind
		 * @param	x		Feld X
		 * @param	y		Feld Y
		 * @return			Gültig true, ungültig false
		 */
		public function isInBounds(x:int, y:int):Boolean
		{
			if (x < 0 || y < 0 || x >= fieldWidth || y >= fieldHeight)
				return false;

			return true;
		}

		/**
		 * Überprüft anch weiteren Kombinationen
		 * @return	Gefundene Kobination oder null
		 */
		public function hasMoreMoves():Array
		{
			// Kombination
			var combi:Array = null;

			// Durch alle Atome im Feld
			for (var x:int = 0; x < fieldWidth; x += MIN_CHAIN)
			{
				for (var y:int = 0; y < fieldHeight; y += MIN_CHAIN) 				{ 					// Nach Kobination suchen 					combi = findCombinations(x, y); 					 					// Kombination gefunden dann Funktion beenden und Kobination zurückgeben  					if (combi != null) 					{ 						if (combi.length > 0)
						{
							return combi;
						}
					}
				}
			}

			// keine Kombinationen mehr
			return null;
		}

		/**
		 * Feld zurücksetzen
		 */
		public function reset():void
		{
			// Alle Atome löschen
			for (var i:int = 0; i < _field.length; i++)
			{
				var a:Atom = _field[i] as Atom;
				if (a != null) removeAtomAt(a.fieldX, a.fieldY);
			}

			// Feld mit neuen Atomen befüllen
			for (var x:int = 0; x < width; x++)
			{
				// Reihe für Reihe
				setTimeout(addOneLine, x * 200, x);
			}
		}

		/**
		 * Fügt eine Reihe vertikal an gegebener Position ein
		 * @param	x	Feld X
		 */
		private function addOneLine(x:int):void
		{
			// Für komplette Spalte
			for (var y:int = 0; y < height; y++)
			{
				var atom:Atom = new Atom(x, y, MAX_COLOR);
				addAtomAt(x, y, atom);
			}
		}

	}

FieldEvent.as

public class FieldEvent extends Event
	{
		public static const ATOMS_DELETED:String = "onAtomsDeleted";
		public static const NO_MORE_MOVES:String = "onNoMoreMoves";

		/* Atomzähler */
		public var amount:int;

		public function FieldEvent(type:String, amount:int = 0)
		{
			super(type, true);

			this.amount = amount;
		}

	}

Atom.as

	public class Atom extends MovieClip
	{
		/* Farbtabelle */
		public static const COLOR:Array = [0xff0000, 0x00ff00, 0x0000ff, 0xffff00, 0xff00ff, 0x00ffff];

		/* Atom größe*/
		public static const SIZE:int = 8;

		/* Verschiebung auf x und y der Bühne*/
		public static var OFFSET_X:Number = 0;
		public static var OFFSET_Y:Number = 0;

		/* X und Y Position im Feld*/
		private var _x:int;
		private var _y:int;

		/* Atomfarbe */
		private var _color:int;

		/**
		 * Konstruktor für ein Atom
		 * @param	x			x Position
		 * @param	y			y Position
		 * @param	maxColor	Maximale Farben
		 */
		public function Atom(x:int, y:int, maxColor:int)
		{
			// x und y der Feldposition setzen nicht x und y der Bühne
			_x = x;
			_y = y;

			// Farbe setzen
			_color = getRandomColor(maxColor);

			// Zeichenen
			draw();

			// Auf Bühnenposition bewegen
			move();

			// Hand an der Maus anzeigen
			buttonMode = true;
			useHandCursor = true;
		}

		/**
		 * Atom zeichnen
		 */
		public function draw():void
		{
			// Wir zeichnen hier eine Kugel
			var g:Graphics = graphics;
			g.lineStyle(1, 0x000000, .2);
			g.beginFill(_color, 8);
			g.drawCircle(0, 0, SIZE);
			g.endFill();
			filters = new Array(new BevelFilter(2));
		}

		/**
		 * An Bühnenposition bringen
		 */
		public function move():void
		{
			// x und y setzen
			x = _x * width + (OFFSET_X + SIZE);
			y = _y * height + (OFFSET_Y + SIZE);
		}

		/**
		 * Zufallsfarbe holen
		 * @param	amout	Anzahl der Farben
		 * @return			HEX Farbwert
		 */
		public function getRandomColor(amout:int = -1):uint
		{
			// Überprüfen ob im zulässigem Farbbereich
			if (amout > COLOR.length || amout == -1)
				amout = COLOR.length;

			// Farbe zurückgeben
			return COLOR[Math.floor(Math.random() * amout)];
		}

		/**
		 * Atom als String
		 * @return		String
		 */
		override public function toString():String
		{
			return "[Atom x=" + _x + ", y=" + _y + ", color=" + _color + "]";
		}

		/**
		 * GET und SET X Feldposition
		 */
		public function get fieldX():int
		{
			return _x;
		}

		public function set fieldX(value:int):void
		{
			_x = value;
		}

		/**
		 * GET und SET Y Feldposition
		 */
		public function get fieldY():int
		{
			return _y;
		}

		public function set fieldY(value:int):void
		{
			_y = value;
		}

		/**
		 * GET Farbe
		 */
		public function get color():uint
		{
			return _color;
		}

		/**
		 * SET Ausgewählt
		 */
		public function set selected(value:Boolean):void
		{
			if (value)
			{
				// Wenn ausgewählt ein paar Filder setzen und Alpha runterschrauben
				filters = new Array(new BevelFilter(2), new GlowFilter(0x000000));
				alpha = .4;
			}
			else
			{
				// Nicht ausgewählt normale Ansicht
				filters = new Array(new BevelFilter(2));
				alpha = 1;
			}
		}
	}

Auf Angabe der Pakete habe ich im Artikel verzichtet.

Das Spiel

Die Sourcen

Balls Flash Game Sourcen

Teilen macht glücklich!
  • facebook
  • Twitter
  • Delicious
  • Digg
  • StumbleUpon
  • Google Bookmarks
  • email
  • RSS

Tags: , , , ,

Kommentar hinterlassen

*