senäh

17senäh und so…

Flash/AS3
12. Jul 2011
Kommentare: 0

Levelscrolling, Objectspawning und die UI

Kategorien: Flash/AS3 | 12. Jul 2011 | Kommentare: 0

Serie: How to develop a Doodle Jump clone in 30 minutes

Schön, dass ihr euch durch den ersten Teil des Tutorials gekämpft. Der Prototyp ist damit schon fast fertig. Es geht nur noch um die Fragen wann etwas und was erstellt werden soll und um das LevelSystem. Das LevelSystem übernimmt Aufgaben wie das Scrollen durch das Level, dem Erstellen und Löschen von Plattformen und Gegner und die UI. Damit ist sie ganz schön umfangreich. Schauen wir sie uns deswegen Schritt für Schritt an.

LevelSystem – UI

Wenn ihr das letzte Tutorial richtig verfolgt habt, sollte euer LevelSystem im Augenblick so aussehen:

package de.senaeh.pipo.gingerjump.systems
{
    import de.senaeh.pipo.game.systems.System;
    import de.senaeh.pipo.gingerjump.GJFactory;
    import flash.display.Sprite;
    /**
     * You can use this code as you wish,
     * but please recommend our site http://www.senaeh.de.
     * @author Philipp Zins/Donald Pipowitch
     */
    public class LevelSystem extends System
    {
        private var _factory:GJFactory;
        private var _levelContainer:Sprite;
        private var _uiContainer:Sprite;

        private var _levelWidth:int;
        private var _levelHeight:int;

        public function LevelSystem(factory:GJFactory, levelContainer:Sprite, levelWidth:int, levelHeight:int,
            uiContainer:Sprite):void
        {
            _factory = factory;

            _levelContainer = levelContainer;
            _levelWidth = levelWidth;
            _levelHeight = levelHeight;

            _uiContainer = uiContainer;
        }
    }
}

Kümmern wir uns als erstes um die UI. In den Assets findet ihr zwei UI-Elemente: AHighscore und AEndscreen. AHighscore besitzt ein Textfeld in dem die aktuell höchste Höhe festgehalten wird und AEndscreen besitzt das gleiche Textfeld, sowie zusätzlich ein Button um das Spiel neuzustarten. Dieser Button wird genauso animiert, wie zuvor die Buttons im Spielmenü. Während des Spiels wird nur AHighscore angezeigt. Stirbt die Spielfigur, wird nur AEndscreen angezeigt. Hier findet also nichts kompliziertes statt. Erstellt folgende klassenübergreifenden Variablen:

		private var _highscoreUI:AHighscore = new AHighscore();
		private var _endScreenUI:AEndScreen = new AEndScreen();
		private var _buttonTween:TweenMax;

Und fügt am Ende des Konstruktors folgenden Code ein, um die Assets zu platzieren:

			_highscoreUI.x = _levelWidth / 2;
			_highscoreUI.y = 10;
			_highscoreUI.visible = false;
			_uiContainer.addChild(_highscoreUI);
			_endScreenUI.x = _levelWidth / 2;
			_endScreenUI.y = _levelHeight / 2;
			_buttonTween = new TweenMax(_endScreenUI.retryBtn, 1, { repeat: -1, yoyo: true, scaleX: 0.8, scaleY: 0.8 } );
			_endScreenUI.retryBtn.buttonMode = _endScreenUI.retryBtn.useHandCursor = true;
			_endScreenUI.retryBtn.addEventListener(MouseEvent.CLICK, onRetryBtnClick);
			_endScreenUI.visible = false;
			_uiContainer.addChild(_endScreenUI);

Das reicht, um das UI vorzubereiten. Mit Daten können wir es zum jetzigen Zeitpunkt noch nicht füttern. Bei einem Klick auf den Retry-Button von AEndscreen wird folgende Funktion aufgerufen:

		private function onRetryBtnClick(e:MouseEvent):void
		{
			_endScreenUI.visible = false;
			sendNotification(GJNotificationList.CLICK_RETRY_BTN);
		}

Dadurch wird AEndscreen unsichtbar geschalten und eine Notification mit der Benachrichtigung über den Klick versendet. Dies führt später zu einem GameState-Wechsel zurück zum Startbildschirm.
Wenden wir uns als nächsten dem Scrolling zu.

LevelSystem – Scrolling

Zunächst benötigen wir wieder ein paar Variablen:

		private var _isScrolling:Boolean = false;
		private var _gingerGraphic:Sprite;
		private var _lastHeightPoint:Number;	// highest point the player reached
		private var _isFalling:Boolean = false;
		private var _deadline:Number;
		private var _fallingTime:Number = 3;

Was haben wir hier schönes? _isScrolling gibt an, ob das Level gerade scrollt oder still steht. Diese Variable brauchen wir, weil wir später Gegenr und Plattformen nur dann Erstellen wollen, wenn sich das Level bewegt. _gingerGraphic ist ein Verweis auf den MovieClip der Spielfigur. An die müssen wir noch irgendwie rankommen. Behaltet das im Hinterkopf. _lastHeightPoint hält die erreichte Höhe der Spielfigur fest – basiert auf _gingerGraphic.y. _isFalling gibt an, ob die Spielfigur fällt. Damit ist aber nicht das Fallen nach einem Sprung gemeint, sondern das endgültige Fallen, wenn keine Plattform mehr erreicht werden kann. Diese Information brauchen wir, damit wir wissen, wann wir von AHighscore auf AEndscreen wechseln müssen und wann die Spielfigur tot ist. _deadline gibt die unterste Grenze an, aber der die Spielfigur dann endgültig fällt – basiert auf _lastHeightPoint und der Hälfte von _levelHeight (= unterer Rand des Views). _fallingTime ist ein kleiner zeitlicher Versatz bis AEndscreen angezeigt wird, damit wir die Spielfigur noch schön fallen sehen 😉
Wir ihr seht benötigen wir irgendwie die Grafik der Spielfigur. Die bekommen wir von außen über GJFactory an diese Funktion in LevelSystem übergeben:

		public function setGingerGraphic(graphic:Sprite):void
		{
			_gingerGraphic = graphic;
			_lastHeightPoint = _gingerGraphic.y;
		}

Überschreibt jetzt die update()-Funktion, um das Scrolling pro Update der GameLoop zu berechnen:

		override public function update(delta:uint):void
		{
			handleScrolling();
		}

Und fügt die Funktion handleScrolling() ein:

		// scroll if ginger is higher than middle of screen (while jumping)
		private function handleScrolling():void
		{
			// scrolling by falling
			if (_gingerGraphic.y > _deadline && !_endScreenUI.visible)
			{
				if (!_isFalling)
				{
					TweenMax.delayedCall(_fallingTime, gameOver);
					dispatchEvent(new SoundEvent(SoundEvent.PLAY, GJSoundList.FALLING_SOUND));
					sendNotification(GJNotificationList.GINGER_IS_DEAD);
					_isFalling = true;
				}
				_levelContainer.y = GameMath.lerp(_levelContainer.y, -_gingerGraphic.y + 0 / 2, 0.1);
			}
			// scrolling by jumping
			if (_gingerGraphic.y < _levelContainer.y + _levelHeight / 2 && _lastHeightPoint > _gingerGraphic.y)
			{
				_levelContainer.y = -_gingerGraphic.y + _levelHeight / 2;
				_lastHeightPoint = _gingerGraphic.y;
				_deadline = _lastHeightPoint + _levelHeight / 2;
				_highscoreUI.highscoreTxt.text = String(int(_levelContainer.y));
				_isScrolling = true;
			}
			else
				_isScrolling = false;
		}

Gucken wir uns den ersten Teil „// scrolling by falling“ an, der relevant ist, wenn die Spielfigur die Deadline überschreitet (also unter den unteren Rand des Views fällt), so lange der Endscreen noch nicht eingeblendet wurde. Sobald dieses Ereignis auftritt, ist das Spiel vorbei. Über TweenMax wird mit einer Verzögerung (= _fallingTime) die Funktion gameOver() aufgerufen. Es wird ein SoundEvent ausgelöst, um den Fallen-Soundeffekt abzuspielen. Außerdem wird eine GINER_IS_DEAD-Notification abgeschickt, welche von der Ginger-Klasse empfangen wird. Das könnt ihr im ersten Teil des Tutorials nachlesen. Anschließend wird _isFalling auf true gesetzt, damit die eben ausgeführten Funktionen nicht noch einmal ausgeführt werden. Mit der Zeile „_levelContainer.y = GameMath.lerp(_levelContainer.y, -_gingerGraphic.y + 0 / 2, 0.1);“ wird das Fallen animiert. Der Levelausschnitt bewegt sich mit der Fallbewegung der Spielfigur mit. Damit diese Bewegung weich abläuft verwende ich die GameMath.lerp()-Funktion, welche zwischen der aktuellen Position von _levelContainer und von _gingerGraphic linear interpoliert.
Nun zu „// scrolling by jumping“. So lange die Spielfigur noch lebt, läuft hier die eigentliche Berechnung des Scrollings ab. Dies ist immer dann der Fall, wenn die Spielfigur die alte Höchstgrenze überschreitet. In diesem Fall wird der Levelausschnitt so gescrollt, dass die derzeitige Position (und damit die neue Höchstmarke) in der Mitte des Views liegt. Anschließend werden die neue Höchstmarke und die _deadline festgehalten. Die neue Höchstmarke wird in AHighscore angezeigt und _isScrolling auf true gesetzt. Fällt die Spielfigur wieder unter die Höchstmarke (aber nicht unter _deadline!) wird _isScrolling auf false gesetzt.
Die gameOver()-Funktion, die aufgerufen wird, wenn die Spielfigur unter die _deadline fällt, schaltet nur AHighscore auf unsichtbar und AEndscreen auf sichtbar. Außerdem wird die Höchstmarke bei AEndscreen eingetragen:

		private function gameOver():void
		{
			_highscoreUI.visible = false;
			_endScreenUI.highscoreTxt.text = _highscoreUI.highscoreTxt.text;
			_endScreenUI.visible = true;
		}

LevelSystem – Plattformen spawnen

Zunächst wieder die obligatorischen Variablen:

		private var _platformContainer:Sprite = new Sprite();
		private var _lastPlatformPosition:Vector2D;
		private var _nextPlatformPosition:Vector2D = new Vector2D();
		private var _nextPlatformDirection:Vector2D = new Vector2D();
		private var _minPlatformDistance:Number = 100;
		private var _maxPlatformDistance:Number = 250;
		private var _platformList:Vector.<Platform> = new Vector.<Platform>();
		private var _xSpread:Number = 4;

_platformContainer hält die Plattform-Grafiken. _lastPlatformPosition speichert die Position der zuletzt erstellten Plattform. Vector2D ist übrigens eine spezielle Vektor-Klasse aus dem utils Package des PIPO Frameworks. _nextPlatformPosition speichert die Position der nächsten Plattform abhängig von _nextPlatformDirection und _lastPlatformPosition. _minPlatformDistance und _maxPlatformDistance beeinflussen den Abstand zwischen den Plattformen. _platformList hällt alle Plattformen in einer Liste fest und _xSpread beeinflusst die Verteilung der Plattformen auf der x-Achse beim Erstellen. Fügt _platformContainer im Konstruktor dem _levelContainer hinzu:

			_levelContainer.addChild(_platformContainer);

Passen wir nun zuerst wieder die update()-Funktion an:

		override public function update(delta:uint):void
		{
			handleScrolling();
			handleSpawning();
		}

Und erstellen handleSpawning():

		private function handleSpawning():void
		{
			if(_isScrolling)
			{
				removePlatforms();
				while(_lastPlatformPosition.y > -_levelContainer.y)
					createPlatform();

			}
		}

Wenn das Level scrollt, wird hier removePlatforms() aufgerufen, welche überprüfen wird, ob Plattformen vom Spiel entfernt werden können und erstellt zusätzlich so lange neue Plattformen über createPlatform() bis die zuletzt erstellte Platform über dem sichtbaren Levelausschnitt liegt. Es existiert als immer genau eine Platform, die über dem sichtbaren Bereich ist, so dass keine neue Plattformen aus dem Nichts auftauchen, wenn das Level scrollt.
Die removePlatforms()-Funktion ist so aufgebaut:

		private function removePlatforms():void
		{
			var removeCount:int = 0;
			for (var i:int = 0; i < _platformList.length; i++)
			{
				if (-_platformList[i].y + _levelHeight < _levelContainer.y )
				{
					sendNotification(GJNotificationList.REMOVE_PHYSICAL_GAME_OBJECT, _platformList[i]);
					removeCount++;
				}
				else
					break;
			}
			_platformList.splice(0, removeCount);
		}

Hier zähle ich die in _platformList gespeichert Plattformen von hinten auf bis ich eine Plattform erreiche, die im sichtbaren Levelausschnitt liegt. Dies kann ich machen, weil ich weiß, dass jede neue Plattform über der alten liegen muss. Alle Plattformen die unter dem sichtbaren Levelausschnitt liegen können dann entfernt werden. Dazu verwende ich eine entsprechende Notification, welche später von der GJFactory empfangen wird, um die Plattformen zu löschen und wir löschen die Plattform auf der _platformList.
Jetzt zum Erstellen der Plattformen. Das ist etwas tricky:

		private function createPlatform():void
		{
			// direction for new platform (set "how" the direction should be i.e. spread more on x- or y-axis)
			_nextPlatformDirection.x = GameMath.randomNumber( -_xSpread, _xSpread);	// x spread
			if (_xSpread > 1) _xSpread *= 0.999;	// decrease the xSpread slowly
			_nextPlatformDirection.y = GameMath.randomNumber(-0.25, -0.75);	// y spread
			// distance between old and new platform - make distance slowly bigger
			_nextPlatformDirection.length = GameMath.randomNumber(_minPlatformDistance, _maxPlatformDistance);
			if (_minPlatformDistance < _maxPlatformDistance)
			{
				_minPlatformDistance *= 1.001;
				if (_minPlatformDistance > _maxPlatformDistance)
					_minPlatformDistance = _maxPlatformDistance;
			}
			// add distance and direction to last platform position to get the new platform position
			// and check if platform is within level
			_nextPlatformPosition = _lastPlatformPosition.add(_nextPlatformDirection);
			if (_nextPlatformPosition.x < 0)
				_nextPlatformPosition.x += _levelWidth;
			else if (_nextPlatformPosition.x > _levelWidth)
				_nextPlatformPosition.x -= _levelWidth;
			// create and add new platform
			var platform:Platform = _factory.createPlatform(_nextPlatformPosition);
			_platformList.push(platform);
			_platformContainer.addChild(platform.graphic);
			// save new platform position
			_lastPlatformPosition.x = _nextPlatformPosition.x;
			_lastPlatformPosition.y = _nextPlatformPosition.y;
		}

Zu erst legen wir die Richtung fest, in der die neue Plattform verglichen zur alten liegt. Diese zeigt immer nach oben und zufällig nach links oder rechts. Wie sehr sie nach links bzw. rechts zeigt, hängt von _xSpread ab. Dieses verringern wir je weiter der Spieler kommt, damit die Plattformen im Laufe der Zeit immer weiter auf der y-Achse von einander entfernt liegen. Anders formuliert: Zu Beginn des Spiels verteilen sich die Plattformen mehr in die Breite und später in die Höhe. Dabei muss der Abstand zwischen zwei Plattformen immer innerhalb eine Minimal- und einer Maximaldistanz liegen, wobei der Abstand im fortschreitenden Spiel immer größer wird. Wir kombinieren die Richtung mit der Distanz und nehmen als Ausgangspunkt die Position der letzten Plattform und erhalten anschließend die neue Plattformsposition. Hier überprüfen wir nur noch, ob sie innerhalb des Levels liegt oder links bzw. rechts davon und schieben sie ggf. zurück. Anschließend lassen wir die Plattform über die GJFactory erstellen und übergeben ihr die neue Position als Parameter. Die Funktion zum Erstellen der Plattform in der GJFactory fügen wir erst später hinzu. Nehmen wir aber erst einmal an, sie wäre schon vorhanden und würde uns die Plattform als GameObject wieder zurück geben. Dann fügen wir die neue Plattform der _platformList hinzu und platzieren die Plattform-Grafik im _platformContainer. Nun speichern wir noch die aktuelle Position der neuen Plattform als neue _lastPlatformPosition. Fertsch!

LevelSystem – Gegner spawnen

Das Erstellen der Gegner läuft sehr ähnlich ab. Wir benötigen folgende Variablen:

		private var _enemyContainer:Sprite = new Sprite();
		private var _lastEnemyY:int;
		private var _spawnRatioEnemy:Number = 0.11;
		private var _minEnemyDistance:Number = 500;
		private var _maxEnemyYMoveArea:Number = 300;
		private var _enemyList:Vector.<Enemy> = new Vector.<Enemy>();

Anstatt der letzten Position reicht uns dieses Mal die Höhe des letzten Gegners, weil die x-Position des alten Gegners irrelevant für die Position des neuen Gegners ist. Außerdem haben wir eine _spawnRatioEnemy, um das Erstellen der Gegner etwas zufallsbasiert ablaufen zu lassen. Dies brauchten wir bei den Plattformen nicht, weil wir dort zwangsläufig immer genug Plattformen brauchten, damit die Spielfigur im Spiel immer höher kommen kann. Wenn ein Gegner mal etwas länger auf sich warten lässt, ist das nicht so schlimm 😉 Ebenfalls neu ist _maxEnemyYMoveArea, welche die maximale Auslenkung auf der y-Achse festhält. Denkt daran, dass sich eure Gegner ja bewegen! Die x-Auslenkung wird begrenzt durch die Levelbreite, aber die y-Auslenkung müssen wir mit _maxEnemyYMoveArea begrenzen. Fügt jetzt wieder _enemyContainer dem _levelContainer im Konstruktor hinzu:

			_levelContainer.addChild(_enemyContainer);

That’s it. Kümmern wir uns um das eigentliche Spawning.
Erweitert handleSpawning() folgendermaßen:

		private function handleSpawning():void
		{
			if(_isScrolling)
			{
				removeEnemies();
				if(_lastEnemyY - _minEnemyDistance - _maxEnemyYMoveArea > -_levelContainer.y)
					createEnemy();
				removePlatforms();
				while(_lastPlatformPosition.y > -_levelContainer.y)
					createPlatform();

			}
		}

Zuerst überprüfen wir wieder, ob wir schon erstelllte Gegner entfernen müssen:

		private function removeEnemies():void
		{
			for (var i:int = 0; i < _enemyList.length; i++)
			{
				if (-_enemyList[i].y + 2 * _levelHeight < _levelContainer.y )
				{
					sendNotification(GJNotificationList.REMOVE_PHYSICAL_GAME_OBJECT, _enemyList[i]);
					_enemyList.splice(i, 1);
					i--;
				}
			}
		}

Die Funktion läuft ähnlich ab wie removePlatforms() und sollte für den erfahrenen AS3-Programmierer selbsterklärend sein 🙂
Jetzt folgt createEnemy():

		private function createEnemy():void
		{
			// create enemy, if the spawn ratio is matched
			if (Math.random() >= _spawnRatioEnemy)
				return;
			// create enemy
			var startPos:Vector2D = new Vector2D(_levelWidth * Math.random(), -_levelContainer.y - _levelHeight);
			var endPos:Vector2D = new Vector2D(_levelWidth * Math.random(), startPos.y - Math.random() * _maxEnemyYMoveArea);
			var enemy:Enemy = _factory.createEnemy(startPos, endPos);
			_enemyList.push(enemy);
			_enemyContainer.addChild(enemy.graphic);
			// save y
			_lastEnemyY = enemy.y;
			// decrease distance between enemies
			_minEnemyDistance *= 0.9;
		}

Beachtet hier wieder, dass wir den richtigen Gegner erst in der GJFactory erstellen. LevelSystem berechnet nur wann und wo wir einen Gegner erstellen. Die entsprechende Funktion in der GJFactory programmieren wir anschließend. Dieses Mal übergeben und berechnen wir nicht nur eine Start-, sondern auch eine Endposition für den Gegner. Zwischen diesen zwei Punkten wird sich der Gegner hin- und herbewegen. In der letzten Zeile verringern wir außerdem im fortschreitenden Spiel die Abstände zwischen zwei Gegner. Alles andere läuft analog zu den Plattformen ab.

LevelSystem – Level erstellen und entfernen

Auf zum Entspurt im LevelSystem! Bevor wir die Klasse endgültig ad acta legen können, müssen wir noch zwei Funktionen einbauen. Die eine Funktion wird unser Level zum Start initialisieren, es also schon vor dem ersten Scrollen mit Plattformen füllen, und die andere Funktion wird den kompletten Levelinhalt bei einem Neustart löschen. Auf geht’s!
So initialisieren wir das Level:

		public function initLevel():void
		{
			// scrolling and spawning configs
			_levelContainer.y = 0;
			_deadline = _levelHeight;
			_lastPlatformPosition = new Vector2D(_levelWidth / 2, _levelHeight);
			_lastEnemyY = _levelHeight * 2;
			_isFalling = false;
			// highscore
			_highscoreUI.highscoreTxt.text = "0";
			_highscoreUI.visible = true;
			// ground
			var ground:Platform = _factory.createPlatform(_lastPlatformPosition, true);
			_platformList.push(ground);
			_platformContainer.addChild(ground.graphic);
			// other platforms
			while (_lastPlatformPosition.y > -_levelContainer.y)
			{
				createPlatform();
			};
		}

Es werden alle Parameter zurückgesetzt und eine ground-Plattform erstellt. Dieser ground ist das gleiche wie eine normale Plattform, nur viel breiter. Das wir eine ground-Plattform zu Beginn erstellen wollen wird im zweiten Parameter der _factory.createPlatform()-Funktion festgelegt. Anschließend füllen wir den Levelausschnitt mit normalen Plattformen.
Um den kompletten Levelinhalt zu Löschen reicht diese einfache Funktion:

		public function resetLevel():void
		{
			_levelContainer.y = 99999999;
			removePlatforms();
			removeEnemies();
		}

Der _levelContainer wird manuell extrem hochgesetzt, wodurch die Plattformen und Gegner denken, sie wären nicht mehr im sichtbaren Bereich. Dadurch werden sie entfernt.
Whoo! Ein Haufen Arbeit, was? Eure fertig LevelSystem-Klasse sollte jetzt ungefähr so aussehen:

package de.senaeh.pipo.gingerjump.systems
{
	import com.greensock.TweenMax;
	import de.senaeh.pipo.game.systems.System;
	import de.senaeh.pipo.gingerjump.gameobjects.Enemy;
	import de.senaeh.pipo.gingerjump.gameobjects.Platform;
	import de.senaeh.pipo.gingerjump.GJFactory;
	import de.senaeh.pipo.gingerjump.notifications.GJNotificationList;
	import de.senaeh.pipo.gingerjump.sounds.GJSoundList;
	import de.senaeh.pipo.sofluffy.assets.AEndScreen;
	import de.senaeh.pipo.sofluffy.assets.AHighscore;
	import de.senaeh.pipo.sofluffy.assets.SFalling;
	import de.senaeh.pipo.utils.events.GameEvent;
	import de.senaeh.pipo.utils.events.SoundEvent;
	import de.senaeh.pipo.utils.graphics.GameSprite;
	import de.senaeh.pipo.utils.math.GameMath;
	import de.senaeh.pipo.utils.math.Vector2D;
	import flash.display.Sprite;
	import flash.events.MouseEvent;
	import flash.geom.Point;
	/**
	 * You can use this code as you wish,
     * but please recommend our site http://www.senaeh.de.
	 * @author Philipp Zins/Donald Pipowitch
	 */
	public class LevelSystem extends System
	{
		private var _factory:GJFactory;
		private var _levelContainer:Sprite;
		private var _uiContainer:Sprite;
		private var _highscoreUI:AHighscore = new AHighscore();
		private var _endScreenUI:AEndScreen = new AEndScreen();
		private var _buttonTween:TweenMax;

		// graphics, scrolling
		private var _levelWidth:int;
		private var _levelHeight:int;
		private var _isScrolling:Boolean = false;
		private var _gingerGraphic:Sprite;
		private var _lastHeightPoint:Number;	// highest point the player reached
		private var _isFalling:Boolean = false;
		private var _deadline:Number;
		private var _fallingTime:Number = 3;

		// platform
		private var _platformContainer:Sprite = new Sprite();
		private var _lastPlatformPosition:Vector2D;
		private var _nextPlatformPosition:Vector2D = new Vector2D();
		private var _nextPlatformDirection:Vector2D = new Vector2D();
		private var _minPlatformDistance:Number = 100;
		private var _maxPlatformDistance:Number = 250;
		private var _platformList:Vector.<Platform> = new Vector.<Platform>();
		private var _xSpread:Number = 4;

		// enemy
		private var _enemyContainer:Sprite = new Sprite();
		private var _lastEnemyY:int;
		private var _spawnRatioEnemy:Number = 0.11;
		private var _minEnemyDistance:Number = 500;
		private var _maxEnemyYMoveArea:Number = 300;
		private var _enemyList:Vector.<Enemy> = new Vector.<Enemy>();

		public function LevelSystem(factory:GJFactory, levelContainer:Sprite, levelWidth:int, levelHeight:int,
			uiContainer:Sprite):void
		{
			_factory = factory;

			// level
			_levelContainer = levelContainer;
			_levelContainer.addChild(_platformContainer);
			_levelContainer.addChild(_enemyContainer);
			_levelWidth = levelWidth;
			_levelHeight = levelHeight;

			// ui
			_uiContainer = uiContainer;
			_highscoreUI.x = _levelWidth / 2;
			_highscoreUI.y = 10;
			_highscoreUI.visible = false;
			_uiContainer.addChild(_highscoreUI);
			_endScreenUI.x = _levelWidth / 2;
			_endScreenUI.y = _levelHeight / 2;
			_buttonTween = new TweenMax(_endScreenUI.retryBtn, 1, { repeat: -1, yoyo: true, scaleX: 0.8, scaleY: 0.8 } );
			_endScreenUI.retryBtn.buttonMode = _endScreenUI.retryBtn.useHandCursor = true;
			_endScreenUI.retryBtn.addEventListener(MouseEvent.CLICK, onRetryBtnClick);
			_endScreenUI.visible = false;
			_uiContainer.addChild(_endScreenUI);
		}

		public function setGingerGraphic(graphic:Sprite):void
		{
			_gingerGraphic = graphic;
			_lastHeightPoint = _gingerGraphic.y;
		}

		/*
		 * Logic
		 */

		public function initLevel():void
		{
			// scrolling and spawning configs
			_levelContainer.y = 0;
			_deadline = _levelHeight;
			_lastPlatformPosition = new Vector2D(_levelWidth / 2, _levelHeight);
			_lastEnemyY = _levelHeight * 2;
			_isFalling = false;
			// highscore
			_highscoreUI.highscoreTxt.text = "0";
			_highscoreUI.visible = true;
			// ground
			var ground:Platform = _factory.createPlatform(_lastPlatformPosition, true);
			_platformList.push(ground);
			_platformContainer.addChild(ground.graphic);
			// other platforms
			while (_lastPlatformPosition.y > -_levelContainer.y)
			{
				createPlatform();
			};
		}

		public function resetLevel():void
		{
			_levelContainer.y = 99999999;
			removePlatforms();
			removeEnemies();
		}

		override public function update(delta:uint):void
		{
			handleScrolling();
			handleSpawning();
		}

		/*
		 * Scrolling.
		 */

		// scroll if ginger is higher than middle of screen (while jumping)
		private function handleScrolling():void
		{
			// scrolling by falling
			if (_gingerGraphic.y > _deadline && !_endScreenUI.visible)
			{
				if (!_isFalling)
				{
					TweenMax.delayedCall(_fallingTime, gameOver);
					dispatchEvent(new SoundEvent(SoundEvent.PLAY, GJSoundList.FALLING_SOUND));
					sendNotification(GJNotificationList.GINGER_IS_DEAD);
					_isFalling = true;
				}
				_levelContainer.y = GameMath.lerp(_levelContainer.y, -_gingerGraphic.y + 0 / 2, 0.1);
			}
			// scrolling by jumping
			if (_gingerGraphic.y < _levelContainer.y + _levelHeight / 2 && _lastHeightPoint > _gingerGraphic.y)
			{
				_levelContainer.y = -_gingerGraphic.y + _levelHeight / 2;
				_lastHeightPoint = _gingerGraphic.y;
				_deadline = _lastHeightPoint + _levelHeight / 2;
				_highscoreUI.highscoreTxt.text = String(int(_levelContainer.y));
				_isScrolling = true;
			}
			else
				_isScrolling = false;
		}

		/*
		 * Spawning.
		 */

		private function handleSpawning():void
		{
			if(_isScrolling)
			{
				removeEnemies();
				if(_lastEnemyY - _minEnemyDistance - _maxEnemyYMoveArea > -_levelContainer.y)
					createEnemy();
				removePlatforms();
				while(_lastPlatformPosition.y > -_levelContainer.y)
					createPlatform();

			}
		}

		private function createPlatform():void
		{
			// direction for new platform (set "how" the direction should be i.e. spread more on x- or y-axis)
			_nextPlatformDirection.x = GameMath.randomNumber( -_xSpread, _xSpread);	// x spread
			if (_xSpread > 1) _xSpread *= 0.999;	// decrease the xSpread slowly
			_nextPlatformDirection.y = GameMath.randomNumber(-0.25, -0.75);	// y spread
			// distance between old and new platform - make distance slowly bigger
			_nextPlatformDirection.length = GameMath.randomNumber(_minPlatformDistance, _maxPlatformDistance);
			if (_minPlatformDistance < _maxPlatformDistance)
			{
				_minPlatformDistance *= 1.001;
				if (_minPlatformDistance > _maxPlatformDistance)
					_minPlatformDistance = _maxPlatformDistance;
			}
			// add distance and direction to last platform position to get the new platform position
			// and check if platform is within level
			_nextPlatformPosition = _lastPlatformPosition.add(_nextPlatformDirection);
			if (_nextPlatformPosition.x < 0)
				_nextPlatformPosition.x += _levelWidth;
			else if (_nextPlatformPosition.x > _levelWidth)
				_nextPlatformPosition.x -= _levelWidth;
			// create and add new platform
			var platform:Platform = _factory.createPlatform(_nextPlatformPosition);
			_platformList.push(platform);
			_platformContainer.addChild(platform.graphic);
			// save new platform position
			_lastPlatformPosition.x = _nextPlatformPosition.x;
			_lastPlatformPosition.y = _nextPlatformPosition.y;
		}

		private function removePlatforms():void
		{
			var removeCount:int = 0;
			for (var i:int = 0; i < _platformList.length; i++)
			{
				if (-_platformList[i].y + _levelHeight < _levelContainer.y )
				{
					sendNotification(GJNotificationList.REMOVE_PHYSICAL_GAME_OBJECT, _platformList[i]);
					removeCount++;
				}
				else
					break;
			}
			_platformList.splice(0, removeCount);
		}

		private function createEnemy():void
		{
			// create enemy, if the spawn ratio is matched
			if (Math.random() >= _spawnRatioEnemy)
				return;
			// create enemy
			var startPos:Vector2D = new Vector2D(_levelWidth * Math.random(), -_levelContainer.y - _levelHeight);
			var endPos:Vector2D = new Vector2D(_levelWidth * Math.random(), startPos.y - Math.random() * _maxEnemyYMoveArea);
			var enemy:Enemy = _factory.createEnemy(startPos, endPos);
			_enemyList.push(enemy);
			_enemyContainer.addChild(enemy.graphic);
			// save y
			_lastEnemyY = enemy.y;
			// decrease distance between enemies
			_minEnemyDistance *= 0.9;
		}

		private function removeEnemies():void
		{
			for (var i:int = 0; i < _enemyList.length; i++)
			{
				if (-_enemyList[i].y + 2 * _levelHeight < _levelContainer.y )
				{
					sendNotification(GJNotificationList.REMOVE_PHYSICAL_GAME_OBJECT, _enemyList[i]);
					_enemyList.splice(i, 1);
					i--;
				}
			}
		}

		/*
		 * GameOver.
		 */

		private function gameOver():void
		{
			_highscoreUI.visible = false;
			_endScreenUI.highscoreTxt.text = _highscoreUI.highscoreTxt.text;
			_endScreenUI.visible = true;
		}

		private function onRetryBtnClick(e:MouseEvent):void
		{
			_endScreenUI.visible = false;
			sendNotification(GJNotificationList.CLICK_RETRY_BTN);
		}
	}
}

Es wird Zeit, dass die GJFactory wirklich GameObejcts erstellt!

GJFactory – Part II

Nach dem letzten Tutorial sollte eure GJFactory so aussehen:

package de.senaeh.pipo.gingerjump
{
    import de.senaeh.pipo.game.facade.IGameFacade;
    import de.senaeh.pipo.game.factory.GameFactory;
    import de.senaeh.pipo.game.systems.logic.LogicSystem;
    import de.senaeh.pipo.game.systems.physic.Box2DSystem;
    import de.senaeh.pipo.gingerjump.systems.LevelSystem;
    import de.senaeh.pipo.utils.events.SoundEvent;
    import flash.display.Sprite;
    /**
     * You can use this code as you wish,
     * but please recommend our site http://www.senaeh.de.
     * @author Philipp Zins/Donald Pipowitch
     */
    public class GJFactory extends GameFactory
    {
        private var _levelContainer:Sprite;
        private var _uiContainer:Sprite;

        private var _box2DSystem:Box2DSystem;
        private var _logicSystem:LogicSystem;
        private var _levelSystem:LevelSystem;

        public function GJFactory(facade:IGameFacade):void
        {
            super(facade);
        }

        override protected function init():void
        {
            // graphic container
            _levelContainer = new Sprite();
            _facade.addSprite(_levelContainer);
            _uiContainer = new Sprite();
            _facade.addSprite(_uiContainer);
            // systems
            _box2DSystem = new Box2DSystem(_facade, 0, 17);
            _logicSystem = new LogicSystem();
            _levelSystem = new LevelSystem(this, _levelContainer, _facade.view.stageWidth, _facade.view.stageHeight, _uiContainer);
        }

        /*
         * Systems
         */
        public function get box2DSystem():Box2DSystem
        {
            return _box2DSystem;
        }

        public function get logicSystem():LogicSystem
        {
            return _logicSystem;
        }

        public function get levelSystem():LevelSystem
        {
            return _levelSystem;
        }
    }
}

Beginnen wir nun mit den Funktionen zum Erstellen der GameObjects. Als erstes ist das Spielmenü dran, welches wir zusätzlich in einer klassenweiten Variable speichern:

		private var _menu:Menu;

Und die Funktion:

		public function createMenu():void
		{
			// graphic
			var graphic:AMenu = new AMenu();
			_facade.addSprite(graphic);
			// go
			_menu = new Menu(graphic, graphic.startBtn, graphic.audioBtn);
		}

Wir erzeugen die Grafik, fügen sie dem View zu und übergeben die Grafik und die dazugehörigen Buttons dem Menu-Konstruktor. Die neue Menu-Instanz speichern wir in _menu.
Die Plattformen erstellt ihr mit folgender Funktion:

		public function createPlatform(position:Vector2D, isGround:Boolean = false):Platform
		{
			// graphic
			var graphic:APlatform = new APlatform();
			if (isGround)
				graphic.width = _facade.view.stageWidth;
			// body
			var body:b2Body = _box2DSystem.createBody(
				_box2DSystem.createBodyDef(position.x, position.y, 0, Box2DSystem.BODY_TYPE_STATIC),
				box2DSystem.createFixtureDef(box2DSystem.createShapeBox(graphic.width, graphic.height)));
			// game object
			var go:Platform = new Platform(graphic, body);
			return go;
		}

Zuerst erzeugt ihr wieder die Grafik, fügt sie aber dieses Mal nicht dem View zu. Warum? Weil wir sie ja im LevelSystem dem _platformContainer zufügen, welcher wiederum am _levelContainer hängt, welcher wiederum bereits dem View hinzugefügt wurde. Puuh! Außerdem wird überprüft, ob die Plattform eine ground-Plattform sein soll. Wenn ja, wird sie auf Levelbreite gezogen.
Da die Platform-Klasse aber nicht nur von GameObject, sondern von PhysicalGameObject erbt, benötigen wir noch einen Box2D-Body, den wir über das Box2DSystem erstellen. Dazu weisen wir dem Body die Position, die Form/Größe und den Typ (= statisch) zu. Falls ihr mehr über die Box2D-Body-Typen erfahren wollt, empfehle ich euch wieder das Box2D-Manual. Anschließend erstellen wir die Plattform und geben sie zurück.
Genau das Gleiche läuft beim Erstellen der Gegner ab, nur das wir dieses Mal keinen rechteckigen, sondern einen kreisförmigen Körper haben, dessen Body-Typ kinematisch ist:

		public function createEnemy(startPos:Vector2D, endPos:Vector2D):Enemy
		{
			// graphic
			var graphic:AEnemy = new AEnemy();
			// body
			var diameter:Number = graphic.width - 5;
			var body:b2Body = _box2DSystem.createBody(
				_box2DSystem.createBodyDef(startPos.x, startPos.y, 0, Box2DSystem.BODY_TYPE_KINEMATIC),
				box2DSystem.createFixtureDef(box2DSystem.createShapeCircle(diameter)));
			// game object
			var go:Enemy = new Enemy(graphic, body, endPos);
			return go;
		}

Nun zur Spielfigur. Diese Funktion ist etwas umfangreicher:

		public function createGinger():void
		{
			// graphic
			var graphic:AGinger = new AGinger();
			_levelContainer.addChild(graphic);
			// clone graphic
			var cloneGraphic:AGinger = new AGinger();
			_levelContainer.addChild(cloneGraphic);
			// body
			var x:int = _facade.view.stageWidth / 2;
			var y:int = _facade.view.stageHeight - graphic.height / 2 - 15;
			var diameter:Number = graphic.width - 5;
			var body:b2Body = _box2DSystem.createBody(
				_box2DSystem.createBodyDef(x, y),
				box2DSystem.createFixtureDef(box2DSystem.createShapeCircle(diameter)));
			body.SetFixedRotation(true);
			body.SetSleepingAllowed(false);
			// game object
			var go:Ginger = new Ginger(graphic, body, cloneGraphic, _facade.view.stageWidth);
			_logicSystem.addGameObject(go);
			_levelSystem.setGingerGraphic(graphic);
			// events
			go.addEventListener(SoundEvent.PLAY, playSound);
		}

Zunächst erstellen wir die Spielfigurgrafik und eine zusätzliche Kopie von ihr. Dann folgt wieder der Box2D-Body. Bei diesem stellen wir zusätzlich ein, dass er nicht rotieren kann und dass Sleeping nicht erlaubt ist. Sleeping ist ein Box2D-Begriff, der eine Inaktivität des Box2D-Bodies beschreibt. Sollte der Body einschlafen, könnten wir ihn nicht mehr steuern. Der Body-Typ ist dynamisch. Nachdem wir die Spielfigur erstellt haben, fügen wir sie dem logicSystem hinzu, damit sie bei jeden GameLoop-Durchgang aktualisiert wird. Außerdem übergeben wir dem LevelSystem die Spielfigurgrafik. Zuletzt hängen wir der Spielfigur einen SoundEventListener an, damit wir die ausgelösten SoundEvents abfangen können. Die Funktion dafür sieht so aus:

		private function playSound(e:SoundEvent):void
		{
			_facade.playSound(e.sound, e.volume, e.loops);
		}

Könnt ihr euch noch erinnern, welche Klasse ebenfalls SoundEvents auslöst? Das LevelSystem! Fügt deswegen in der init()-Funktion noch als letzte Zeile folgendes hinzu:

			_levelSystem.addEventListener(SoundEvent.PLAY, playSound);

Die GameObjects sind erstellt, aber es gibt noch einige  Notifications, auf die GJFactory reagieren muss. Darunter fallen unter anderem auch Notifications zum Entfernen der GameObjects. Es folgt das Notification-Handling:

		override public function get notificationInterests():Array
		{
			return [
				GJNotificationList.CLICK_AUDIO_BTN,
				GJNotificationList.CLICK_START_BTN,
				GJNotificationList.CLICK_RETRY_BTN,
				GJNotificationList.REMOVE_PHYSICAL_GAME_OBJECT,
				GJNotificationList.REMOVE_GINGER
				]
		}

		override public function receiveNotification(notification:INotification):void
		{
			switch(notification.name)
			{
				case GJNotificationList.CLICK_AUDIO_BTN:
					onClickAudioBtn();
					break;
				case GJNotificationList.CLICK_START_BTN:
					onClickStartBtn();
					break;
				case GJNotificationList.CLICK_RETRY_BTN:
					onClickRetryBtn();
					break;
				case GJNotificationList.REMOVE_PHYSICAL_GAME_OBJECT:
					removePhysicalGameObject(notification.gameObject as PhysicalGameObject);
					break;
				case GJNotificationList.REMOVE_GINGER:
					removeGinger(notification.gameObject as Ginger);
					break;
			}
		}

Es werden fünf Notifications abgefangen und verarbeitet. Dazu zählt der Klick auf den Audio-Button, um den Spielsound ein- und auszustellen:

		private function onClickAudioBtn():void
		{
			if (_menu.getAudioBtnState())
			{
				_facade.setMainVolume(0.0);
				_menu.setAudioBtnState(false);
			}
			else
			{
				_facade.setMainVolume(1.0);
				_menu.setAudioBtnState(true);
			}
		}

Dazu zählt der Klick auf den Start-Button, welcher einen einfachen GameState-Wechsel auslöst:

		private function onClickStartBtn():void
		{
			_facade.switchToState(GJStateManager.STATE_GAME);
		}

Ähnliches passiert bei einem Klick auf den Retry-Button:

		private function onClickRetryBtn():void
		{
			_facade.switchToState(GJStateManager.STATE_MENU);
		}

Und es folgt das Entfernen der Spielfigur und der restlichen PhysicalGameObjects:

		private function removePhysicalGameObject(go:PhysicalGameObject):void
		{
			_box2DSystem.destroyBody(go.body);
			go.kill();
		}

		private function removeGinger(go:Ginger):void
		{
			go.removeEventListener(SoundEvent.PLAY, playSound);
			logicSystem.removeGameObject(go);
			removePhysicalGameObject(go);
		}

Ihr vermisst das Entfernen des Spielmenüs oder? Dieser Aufruf erfolgt nicht über eine Notification, sondern wird vom GJStateManager ausgelöst. Die Funktion sieht so aus:

		public function removeMenu():void
		{
			_facade.removeSprite(_menu.graphic);
			_menu.kill();
			_menu = null;
		}

Fast fertig! Jetzt müssen wir nur noch die GameState-Wechsel im GJStateManager beschreiben und das Spiel läuft 😉 Eure GJFactory sollte jetzt ungefähr so aussehen:

package de.senaeh.pipo.gingerjump
{
	import Box2D.Common.Math.b2Vec2;
	import Box2D.Dynamics.b2Body;
	import de.senaeh.pipo.game.facade.IGameFacade;
	import de.senaeh.pipo.game.factory.GameFactory;
	import de.senaeh.pipo.game.gameobjects.physic.IPhysicalGameObject;
	import de.senaeh.pipo.game.gameobjects.physic.PhysicalGameObject;
	import de.senaeh.pipo.game.notifications.INotification;
	import de.senaeh.pipo.game.statemanager.GameStateManager;
	import de.senaeh.pipo.game.systems.logic.LogicSystem;
	import de.senaeh.pipo.game.systems.physic.Box2DSystem;
	import de.senaeh.pipo.gingerjump.gameobjects.Enemy;
	import de.senaeh.pipo.gingerjump.gameobjects.Ginger;
	import de.senaeh.pipo.gingerjump.gameobjects.Menu;
	import de.senaeh.pipo.gingerjump.gameobjects.Platform;
	import de.senaeh.pipo.gingerjump.notifications.GJNotificationList;
	import de.senaeh.pipo.gingerjump.systems.LevelSystem;
	import de.senaeh.pipo.sofluffy.assets.AEnemy;
	import de.senaeh.pipo.sofluffy.assets.AGinger;
	import de.senaeh.pipo.sofluffy.assets.AHighscore;
	import de.senaeh.pipo.sofluffy.assets.AMenu;
	import de.senaeh.pipo.sofluffy.assets.APlatform;
	import de.senaeh.pipo.utils.events.GameEvent;
	import de.senaeh.pipo.utils.events.SoundEvent;
	import de.senaeh.pipo.utils.math.Vector2D;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.MouseEvent;
	/**
	 * You can use this code as you wish,
     * but please recommend our site http://www.senaeh.de.
	 * @author Philipp Zins/Donald Pipowitch
	 */
	public class GJFactory extends GameFactory
	{
		private var _levelContainer:Sprite;
		private var _uiContainer:Sprite;

		private var _box2DSystem:Box2DSystem;
		private var _logicSystem:LogicSystem;
		private var _levelSystem:LevelSystem;

		private var _menu:Menu;

		public function GJFactory(facade:IGameFacade):void
		{
			super(facade);
		}

		override protected function init():void
		{
			// graphic container
			_levelContainer = new Sprite();
			_facade.addSprite(_levelContainer);
			_uiContainer = new Sprite();
			_facade.addSprite(_uiContainer);
			// systems
			_box2DSystem = new Box2DSystem(_facade, 0, 17);
			_logicSystem = new LogicSystem();
			_levelSystem = new LevelSystem(this, _levelContainer, _facade.view.stageWidth, _facade.view.stageHeight, _uiContainer);
			_levelSystem.addEventListener(SoundEvent.PLAY, playSound);
		}

		/*
		 * NotificationInterest.
		 */

		override public function get notificationInterests():Array
		{
			return [
				GJNotificationList.CLICK_AUDIO_BTN,
				GJNotificationList.CLICK_START_BTN,
				GJNotificationList.CLICK_RETRY_BTN,
				GJNotificationList.REMOVE_PHYSICAL_GAME_OBJECT,
				GJNotificationList.REMOVE_GINGER
				]
		}

		override public function receiveNotification(notification:INotification):void
		{
			switch(notification.name)
			{
				case GJNotificationList.CLICK_AUDIO_BTN:
					onClickAudioBtn();
					break;
				case GJNotificationList.CLICK_START_BTN:
					onClickStartBtn();
					break;
				case GJNotificationList.CLICK_RETRY_BTN:
					onClickRetryBtn();
					break;
				case GJNotificationList.REMOVE_PHYSICAL_GAME_OBJECT:
					removePhysicalGameObject(notification.gameObject as PhysicalGameObject);
					break;
				case GJNotificationList.REMOVE_GINGER:
					removeGinger(notification.gameObject as Ginger);
					break;
			}
		}

		/*
		 * Systems
		 */
		public function get box2DSystem():Box2DSystem
		{
			return _box2DSystem;
		}

		public function get logicSystem():LogicSystem
		{
			return _logicSystem;
		}

		public function get levelSystem():LevelSystem
		{
			return _levelSystem;
		}

		/*
		 * GameObjects
		 */
		public function createMenu():void
		{
			// graphic
			var graphic:AMenu = new AMenu();
			_facade.addSprite(graphic);
			// go
			_menu = new Menu(graphic, graphic.startBtn, graphic.audioBtn);
		}

		public function createPlatform(position:Vector2D, isGround:Boolean = false):Platform
		{
			// graphic
			var graphic:APlatform = new APlatform();
			if (isGround)
				graphic.width = _facade.view.stageWidth;
			// body
			var body:b2Body = _box2DSystem.createBody(
				_box2DSystem.createBodyDef(position.x, position.y, 0, Box2DSystem.BODY_TYPE_STATIC),
				box2DSystem.createFixtureDef(box2DSystem.createShapeBox(graphic.width, graphic.height)));
			// game object
			var go:Platform = new Platform(graphic, body);
			return go;
		}

		public function createEnemy(startPos:Vector2D, endPos:Vector2D):Enemy
		{
			// graphic
			var graphic:AEnemy = new AEnemy();
			// body
			var diameter:Number = graphic.width - 5;
			var body:b2Body = _box2DSystem.createBody(
				_box2DSystem.createBodyDef(startPos.x, startPos.y, 0, Box2DSystem.BODY_TYPE_KINEMATIC),
				box2DSystem.createFixtureDef(box2DSystem.createShapeCircle(diameter)));
			// game object
			var go:Enemy = new Enemy(graphic, body, endPos);
			return go;
		}

		public function createGinger():void
		{
			// graphic
			var graphic:AGinger = new AGinger();
			_levelContainer.addChild(graphic);
			// clone graphic
			var cloneGraphic:AGinger = new AGinger();
			_levelContainer.addChild(cloneGraphic);
			// body
			var x:int = _facade.view.stageWidth / 2;
			var y:int = _facade.view.stageHeight - graphic.height / 2 - 15;
			var diameter:Number = graphic.width - 5;
			var body:b2Body = _box2DSystem.createBody(
				_box2DSystem.createBodyDef(x, y),
				box2DSystem.createFixtureDef(box2DSystem.createShapeCircle(diameter)));
			body.SetFixedRotation(true);
			body.SetSleepingAllowed(false);
			// game object
			var go:Ginger = new Ginger(graphic, body, cloneGraphic, _facade.view.stageWidth);
			_logicSystem.addGameObject(go);
			_levelSystem.setGingerGraphic(graphic);
			// events
			go.addEventListener(SoundEvent.PLAY, playSound);
		}

		/*
		 * EventHandler or NotificationHandler.
		 */
		private function onClickAudioBtn():void
		{
			if (_menu.getAudioBtnState())
			{
				_facade.setMainVolume(0.0);
				_menu.setAudioBtnState(false);
			}
			else
			{
				_facade.setMainVolume(1.0);
				_menu.setAudioBtnState(true);
			}
		}

		private function onClickStartBtn():void
		{
			_facade.switchToState(GJStateManager.STATE_GAME);
		}

		private function onClickRetryBtn():void
		{
			_facade.switchToState(GJStateManager.STATE_MENU);
		}

		private function playSound(e:SoundEvent):void
		{
			_facade.playSound(e.sound, e.volume, e.loops);
		}

		private function removePhysicalGameObject(go:PhysicalGameObject):void
		{
			_box2DSystem.destroyBody(go.body);
			go.kill();
		}

		private function removeGinger(go:Ginger):void
		{
			go.removeEventListener(SoundEvent.PLAY, playSound);
			logicSystem.removeGameObject(go);
			removePhysicalGameObject(go);
		}

		/*
		 * Support Functions.
		 */
		public function removeMenu():void
		{
			_facade.removeSprite(_menu.graphic);
			_menu.kill();
			_menu = null;
		}
	}
}

GJStateManager – Part II

Kurz und schmerzlos – um alle GameStates richtig zu aktivieren und zu deaktivieren, muss euer GJStateManager so aussehn:

package de.senaeh.pipo.gingerjump
{
	import de.senaeh.pipo.game.facade.IGameFacade;
	import de.senaeh.pipo.game.statemanager.GameStateManager;
	import de.senaeh.pipo.gingerjump.notifications.GJNotificationList;
	import de.senaeh.pipo.gingerjump.sounds.GJSoundList;
	import de.senaeh.pipo.sofluffy.assets.SGingerJumpMusic;
	/**
	 * You can use this code as you wish,
     * but please recommend our site http://www.senaeh.de.
	 * @author Philipp Zins/Donald Pipowitch
	 */
	public class GJStateManager extends GameStateManager
	{
		public static const STATE_INIT:String = "STATE_INIT";
		public static const STATE_MENU:String = "STATE_MENU";
		public static const STATE_GAME:String = "STATE_GAME";

		private var _factory:GJFactory;

		override public function init(facade:IGameFacade):void
		{
			super.init(facade);
			_factory = new GJFactory(facade);
		}

		override protected function onDeactivationState(currentState:String, futureState:String):void
		{
			trace("deactivate this state: " + currentState);
			switch(currentState)
			{
				case STATE_MENU:
					// go's
					_factory.removeMenu();
					break;
				case STATE_GAME:
					// go's
					_factory.levelSystem.resetLevel();
					// loops
					_facade.logicLoop.pause();
					break;
			}
		}

		override protected function onActivationState(oldState:String, currentState:String):void
		{
			trace("activate this state: " + currentState);
			switch(currentState)
			{
				case STATE_INIT:
					// music
					_facade.setMainVolume(0.0);
					_facade.playSound(GJSoundList.BACKGROUND_MUSIC, 1.0, -1);
					// systems
					_facade.logicLoop.addSystem(_factory.logicSystem);
					_facade.logicLoop.addSystem(_factory.box2DSystem);
					_facade.logicLoop.addSystem(_factory.levelSystem);
					// state
					switchToState(STATE_MENU);
					break;
				case STATE_MENU:
					// go's
					_factory.createMenu();
					// audioBtn
					if (_facade.getMainVolume() > 0)
						sendNotification(GJNotificationList.CLICK_AUDIO_BTN);
					break;
				case STATE_GAME:
					// view
					_facade.view.focus = _facade.view;
					// go's
					_factory.createGinger();
					_factory.levelSystem.initLevel();
					// loops
					_facade.logicLoop.play();
					break;
			}
		}
	}
}

Hier läuft nichts spannendes ab. Zu erst wird der INIT_STATE ausgelöst. Hier stelle ich die Anfangslautstärke auf null und erstelle die Hintergrundmusik. (Klingt doof Musik zu erstellen und die Lautstärke auf null zu drehen, gell? 😉 Aber ich bevorzuge es, wenn Flash-Spiele erst auf ausdrücklichen Nutzerwunsch Musik abspielen.) Anschließend füge die einzelnen Systems der GameLoop zu. Danach wechsel ich zum MENU_STATE. Hier erstelle ich nur das Menü und konfiguriere die Voreinstellung des Audio-Buttons. Sollte ein Nutzer nämlich die Musik angestellt haben, muss auch nach einem Spielneustart der audio-Button anzeigen, dass der Sound an ist.
Wenn der Nutzer auf den Start-Button klickt wird zum GAME_STATE gewechselt. Hier stelle ich den Fokus auf die View ein, damit wir die Spielfigur mit den Pfeiltasten steuern können. Dann lasse ich das Level initialisieren und die Spielfigur erstellen und schmeiße die GameLoop an.
In den jeweiligen Deaktivierungsvorgängen mache ich entsprechend alles rückgängig, falls nötig.

Ist das Spiel jetzt endlich fertg?

Najaaaaaa… nicht ganz. Es fehlt ja noch die Dokumentklasse. Irgendwo müssen wir ja die GameFacade und den GJStateManager initialisieren und dem GJStateManager sagen, mit welchem State begonnen werden soll. Kopiert euch dazu einfach folgende Klasse als Dokumentklasse ab:

package de.senaeh.pipo.gingerjump
{
	import de.senaeh.pipo.game.facade.GameFacade;
	import de.senaeh.pipo.game.initiator.GameInitiator;
	/**
	 * You can use this code as you wish,
     * but please recommend our site http://www.senaeh.de.
	 * @author Philipp Zins/Donald Pipowitch
	 */
	public class GJInitiator extends GameInitiator
	{
		public function GJInitiator():void
		{
			super(new GameFacade(), new GJStateManager(), GJStateManager.STATE_INIT);
		}
	}
}

Das war’s jetzt aber wirklich! 😀 Als fertiges Ergebnis solltet ihr Ginger Jump erhalten. Ihr könnt ja mal an den Geschwindigkeitsparametern in der Ginger-Klasse oder den Spawning-Parametern im LevelSystem herumschrauben. Dadurch entstehen ganz andere Spieleindrücke. Ich hoffe, ich konnte dem ein oder anderen eine kleine Stütze beim Erstellen des eigenen Doodle Jump Klones sein! Den kompletten Quellcode (ohne die Frameworks) findet ihr hier.
Falls ihr euren Prototypen noch mit unterschiedlichen Plattformen und vielleicht sogar PowerUps aufbohrt, lasst es mich wissen und hinterlasst einen Link in den Kommentaren. 🙂

Autor: Pipo

...kommt ursprünglich aus der Designerecke, ist aber im Laufe seines Studiums in die Webentwicklung gestolpert. Kann sich seit dem nicht so richtig entscheiden wo er hingehört und wagt deswegen vielleicht die Flucht nach vorne in ein komplett neues Gebiet. Vielleicht Management? Niemand weiß es. Auch er nicht.