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.








