CitrusEngine goes Stage3D with Starling

Hey folks, happy new year! My previous post go back from 1st december, I’ve been really busy : adding Starling in the CitrusEngine, learning iOS and working on my 2nd year school project. And obviously some great holidays between christmas & the new year.

Today I’m very pleased to share with you the Citrus Engine working on Starling! For those who don’t know it, it’s a platformer game engine using Box2D Alchemy and Signal libraries. However, it’s not only made for platformer games, you can use it for every 2D contents even if it isn’t a game thanks to its great Box2D management. So before this update, it has supported : Box2D physics, Signals event, a Sound Manager, a Console for quick debugging & tools, Input management, lots of defined objects like Hero, Baddy, Platform, Coin, Sensor, Moving Platform…, parallax, a Loader manager, level editors thanks to its own (the Level Architect made with Air), or using Flash Pro, or Gleed2D. 2 views : the flash display list and blitting, and a layer management!

Thanks to this update, the CitrusEngine now support the great stage3d framework Starling and I’ve also added 3 new objects Cannon, Teleporter & Treadmill, a Level Manager and an abstract class to store the game’s data. This two last things are optional you may use it or not. In this blog post I will explain how I’ve adapted the CE for Starling, and my updates with some examples. For a quick getting start with the engine please refer on its website or to the tutorials on this blog.

But let’s stop chatting, it’s time to test the demo :
Click here for the demo.

The first level is really simple, some graphics a hero and a bad guy with some sensors, particles and text. The second level is just for performance test with Box2D & Starling.

Is there anything I could do before but not anymore with Starling? NO! The Starling view has not added any restriction on the engine. So you can also make this games with the Starling view : CE’s website with its demo and my 1st year school project.

TOOLS :
Ok, so before starting the explanations let’s start with tools you may use : you need to target the Flash Player 11, grab the last flex SDK and code into FDT/FlashBuilder/FlashDevelop. You can use Flash Pro CS3 and + for creating your levels, but please don’t code with that! Then there are 4 awesome tools that I use : TexturePacker to create SpriteSheets, PhysicsEditor and my template to target the CitrusEngine, and finally two others for mac only ParticleDesigner & GlyphDesigner. Also you may find useful my CE flash extension panel. All the content related with the CitrusEngine is available on its google code.

STARLING VIEW :
First of all, thanks Daniel for this awesome framework and your help! My main constraint, for this third engine view, was to keep a backward compatibility. If you have currently project made with the flash display list and you update the engine on its new version, you shouldn’t have any problem (be careful you need to compile for FP11 since it uses Stage3D due to Starling).
In the Main Class which extends CitrusEngine we used to create a new State like that :

state = new MySpriteGameState();

Now with Starling :

// 2 params available, debug mode and anti-aliasing
setUpStarling(true, 4);
state = new MyStarlingGameState();

You can set up the debugger mode there, it displays the Mr. Doob Stats class adapted for Starling by Nicolas Gans, thank you!
The starling var is protected, so you may defined it an other way. At the moment it does :

public function setUpStarling(debugMode:Boolean = false, antiAliasing:uint = 1):void {
 
	starlingDebugMode = debugMode;
 
	_starling = new Starling(RootClass, stage);
	_starling.antiAliasing = antiAliasing;
	_starling.start();
}

starlingDebugMode is a public static var. The RootClass is an internal class to the CitrusEngine class :

import starling.display.Sprite;
import starling.extensions.utils.Stats;
 
import com.citrusengine.core.CitrusEngine;
 
/**
 * RootClass is the root of Starling, it is never destroyed and only accessed through <code>_starling.stage</code>.
 * It may display a Stats class instance which contains Memory & FPS informations.
 */
internal class RootClass extends Sprite {
 
	public function RootClass() {
 
		if (CitrusEngine.starlingDebugMode)
			addChild(new Stats());
	}
}

The state var is defined as a state’s interface, IState, there are two states : State which extends flash.display.Sprite and StarlingState which extends starling.display.Sprite ; there are very similar. Finally the Starling view is very similar to the old CE Sprite view. Like the Blitting view, these 3 views extends CitrusView. The StarlingView is a clone to the SpriteView, and its Art class too. However there is one nice thing : re open the demo, press tab to open the console, and write :

set Box2D visible true

The Box2D debug view was required, it was on the top of my to-do list. But that wasn’t quite obvious : there isn’t any graphics api with Starling, so the first solution was to create a texture of this graphics and add it as an image, but textures are limited to 2048 * 2048. And in an enter frame that was a performance killer. So, the Box2D debug view is running on the flash display list :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.citrusengine.view.starlingview {
 
	import Box2DAS.Dynamics.b2DebugDraw;
 
	import starling.core.Starling;
	import starling.display.Sprite;
	import starling.events.Event;
 
	import com.citrusengine.physics.Box2D;
 
	/**
	 * This displays Box2D's debug graphics. It does so properly through Citrus Engine's view manager. Box2D by default
	 * sets visible to false, so you'll need to set the Box2D object's visible property to true in order to see the debug graphics. 
	 */
	public class Box2DDebugArt extends Sprite {
 
		private var _box2D:Box2D;
		private var _debugDrawer:b2DebugDraw;
 
		public function Box2DDebugArt() {
 
			addEventListener(Event.ADDED, handleAddedToParent);
			addEventListener(Event.ENTER_FRAME, handleEnterFrame);
			addEventListener(Event.REMOVED, destroy);
		}
 
		private function handleAddedToParent(evt:Event):void {
 
			removeEventListener(Event.ADDED, handleAddedToParent);
 
			_box2D = StarlingArt(parent).citrusObject as Box2D;
 
			_debugDrawer = new b2DebugDraw();
			Starling.current.nativeStage.addChild(_debugDrawer);
			_debugDrawer.world = _box2D.world;
			_debugDrawer.scale = _box2D.scale;
		}
 
		private function destroy(evt:Event):void {
 
			removeEventListener(Event.ADDED, handleAddedToParent);
			removeEventListener(Event.ENTER_FRAME, handleEnterFrame);
			removeEventListener(Event.REMOVED, destroy);
		}
 
		private function handleEnterFrame(evt:Event):void {
 
			_debugDrawer.Draw();
		}
	}
}

Now, let’s take a look on the most important class, the StarlingArt. It manages the art for every object. It handles many objects : png jpg gif pictures, swf (yes!), class reference, a fully qualified class name in string form (useful for a level editor!), or a Starling DisplayObject.
To create a CE object in your state class :

override public function initialize():void {
 
	super.initialize();
 
	var box2d:Box2D = new Box2D("Box2D");
	//box2d.visible = true;
	add(box2d);
 
	var baddy1:Baddy = new Baddy("Baddy1", {view:"baddy.png", x:100, y:100, width:40, height:40})
	add(bady1)
 
	var baddy2:Baddy = new Baddy("Baddy2", {view:"baddy.swf", x:400, y:100, width:40, height:40})
	add(bady2)
}

But how it works with a flash swf ? We have to thanks Emiliano Angelini which have created a Starling extension to make DynamicTextureAtlas, so your swf is transformed in a TextureAtlas “on the fly”. Awesome feature for a quick prototyping.
The StarlingArt class :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
package com.citrusengine.view.starlingview {
 
	import Box2DAS.Dynamics.b2DebugDraw;
 
	import starling.core.Starling;
	import starling.display.DisplayObject;
	import starling.display.Image;
	import starling.display.MovieClip;
	import starling.display.Sprite;
	import starling.extensions.textureAtlas.DynamicAtlas;
	import starling.textures.Texture;
	import starling.textures.TextureAtlas;
	import starling.utils.deg2rad;
 
	import com.citrusengine.view.ISpriteView;
 
	import flash.display.Bitmap;
	import flash.display.Loader;
	import flash.events.Event;
	import flash.events.IOErrorEvent;
	import flash.net.URLRequest;
	import flash.utils.Dictionary;
	import flash.utils.getDefinitionByName;
 
	/**
	 * This is the class that all art objects use for the StarlingView state view. If you are using the StarlingView (as opposed to the blitting view, for instance),
	 * then all your graphics will be an instance of this class. There are 2 ways to manage MovieClip :
	 * - specify a "object.swf" in the view property of your object's creation.
	 * - add an AnimationSequence to your view property of your object's creation, see the AnimationSequence for more informations about it.
	 * The AnimationSequence is more optimized than the .swf which creates textures "on the fly" thanks to the DynamicAtlas class.
	 * 
	 * This class does the following things:
	 * 
	 * 1) Creates the appropriate graphic depending on your CitrusObject's view property (loader, sprite, or bitmap), and loads it if it is a non-embedded graphic.
	 * 2) Aligns the graphic with the appropriate registration (topLeft or center).
	 * 3) Calls the MovieClip's appropriate frame label based on the CitrusObject's animation property.
	 * 4) Updates the graphic's properties to be in-synch with the CitrusObject's properties once per frame.
	 * 
	 * These objects will be created by the Citrus Engine's StarlingView, so you should never make them yourself. When you use state.getArt() to gain access to your game's graphics
	 * (for adding click events, for instance), you will get an instance of this object. It extends Sprite, so you can do all the expected stuff with it, 
	 * such as add click listeners, change the alpha, etc.
	 **/
	public class StarlingArt extends Sprite {
 
		/**
		 * The content property is the actual display object that your game object is using. For graphics that are loaded at runtime
		 * (not embedded), the content property will not be available immediately. You can listen to the COMPLETE event on the loader
		 * (or rather, the loader's contentLoaderInfo) if you need to know exactly when the graphic will be loaded.
		 */
		public var content:DisplayObject;
 
		/**
		 * For objects that are loaded at runtime, this is the object that loades them. Then, once they are loaded, the content
		 * property is assigned to loader.content.
		 */
		public var loader:Loader;
 
		// properties :
 
		// determines animations playing in loop. You can add one in your state class : StarlingArt.setLoopAnimations(["walk", "climb"]);
		private static var _loopAnimation:Dictionary = new Dictionary();
 
		private var _citrusObject:ISpriteView;
		private var _registration:String;
		private var _view:*;
		private var _animation:String;
		private var _group:int;
 
		// fps for this MovieClip, it can be different between objects, to set it : view.getArt(myHero).fpsMC = 25; 
		private var _fpsMC:uint = 30;
 
		private var _texture:Texture;
		private var _textureAtlas:TextureAtlas;
 
		public function StarlingArt(object:ISpriteView) {
 
			_citrusObject = object;
 
			if (_loopAnimation["walk"] != true) {
				_loopAnimation["walk"] = true;
			}
		}
 
		public function destroy():void {
 
			if (content is MovieClip) {
				Starling.juggler.remove(content as MovieClip);
				_textureAtlas.dispose();
				content.dispose();
 
			} else if (content is AnimationSequence) {
 
				(content as AnimationSequence).destroy();
				content.dispose();
 
			} else if (content is Image) {
				_texture.dispose();
				content.dispose();
			}
 
		}
 
		/**
		 * Add a loop animation to the Dictionnary.
		 * @param tab an array with all the loop animation names.
		 */
		static public function setLoopAnimations(tab:Array):void {
 
			for each (var animation:String in tab) {
				_loopAnimation[animation] = true;
			}
		}
 
		static public function get loopAnimation():Dictionary {
			return _loopAnimation;
		}
 
		public function get registration():String {
			return _registration;
		}
 
		public function set registration(value:String):void {
 
			if (_registration == value || !content)
				return;
 
			_registration = value;
 
			if (_registration == "topLeft") {
				content.x = 0;
				content.y = 0;
			} else if (_registration == "center") {
				content.x = -content.width / 2;
				content.y = -content.height / 2;
			}
		}
 
		public function get view():* {
			return _view;
		}
 
		public function set view(value:*):void {
 
			if (_view == value)
				return;
 
			_view = value;
 
			if (_view) {
				if (_view is String) {
					// view property is a path to an image?
					var classString:String = _view;
					var suffix:String = classString.substring(classString.length - 4).toLowerCase();
					if (suffix == ".swf" || suffix == ".png" || suffix == ".gif" || suffix == ".jpg") {
						loader = new Loader();
						loader.contentLoaderInfo.addEventListener(Event.COMPLETE, handleContentLoaded);
						loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, handleContentIOError);
						loader.load(new URLRequest(classString));
					}
					// view property is a fully qualified class name in string form. 
					else {
						var artClass:Class = getDefinitionByName(classString) as Class;
						content = new artClass();
						addChild(content);
					}
				} else if (_view is Class) {
					// view property is a class reference
					content = new citrusObject.view();
					addChild(content);
 
				} else if (_view is DisplayObject) {
					// view property is a Display Object reference
					content = _view;
					addChild(content);
				} else {
					throw new Error("SpriteArt doesn't know how to create a graphic object from the provided CitrusObject " + citrusObject);
					return;
				}
 
				if (content && content.hasOwnProperty("initialize"))
					content["initialize"](_citrusObject);
			}
		}
 
		public function get animation():String {
			return _animation;
		}
 
		public function set animation(value:String):void {
 
			if (_animation == value)
				return;
 
			_animation = value;
 
			if (_animation != null && _animation != "") {
 
				var animLoop:Boolean = _loopAnimation[_animation];
 
				if (content is MovieClip)
					(content as MovieClip).changeTextures(_textureAtlas.getTextures(_animation), _fpsMC, animLoop);
 
				if (content is AnimationSequence)
					(content as AnimationSequence).changeAnimation(_animation, _fpsMC, animLoop);
			}
		}
 
		public function get group():int {
			return _group;
		}
 
		public function set group(value:int):void {
			_group = value;
		}
 
		public function get fpsMC():uint {
			return _fpsMC;
		}
 
		public function set fpsMC(fpsMC:uint):void {
			_fpsMC = fpsMC;
		}
 
		public function get citrusObject():ISpriteView {
			return _citrusObject;
		}
 
		public function update(stateView:StarlingView):void {
 
			if (content is Box2DDebugArt) {
 
				// Box2D view is not on the Starling display list, but on the classical flash display list.
				// So we need to move its view here, not in the StarlingView.
 
				var box2dDebugArt:b2DebugDraw = (Starling.current.nativeStage.getChildAt(1) as b2DebugDraw);
 
				if (stateView.cameraTarget) {
 
					var diffX:Number = (-stateView.cameraTarget.x + stateView.cameraOffset.x) - box2dDebugArt.x;
					var diffY:Number = (-stateView.cameraTarget.y + stateView.cameraOffset.y) - box2dDebugArt.y;
					var velocityX:Number = diffX * stateView.cameraEasing.x;
					var velocityY:Number = diffY * stateView.cameraEasing.y;
					box2dDebugArt.x += velocityX;
					box2dDebugArt.y += velocityY;
 
					// Constrain to camera bounds
					if (stateView.cameraBounds) {
						if (-box2dDebugArt.x <= stateView.cameraBounds.left || stateView.cameraBounds.width < stateView.cameraLensWidth)
							box2dDebugArt.x = -stateView.cameraBounds.left;
						else if (-box2dDebugArt.x + stateView.cameraLensWidth >= stateView.cameraBounds.right)
							box2dDebugArt.x = -stateView.cameraBounds.right + stateView.cameraLensWidth;
 
						if (-box2dDebugArt.y <= stateView.cameraBounds.top || stateView.cameraBounds.height < stateView.cameraLensHeight)
							box2dDebugArt.y = -stateView.cameraBounds.top;
						else if (-box2dDebugArt.y + stateView.cameraLensHeight >= stateView.cameraBounds.bottom)
							box2dDebugArt.y = -stateView.cameraBounds.bottom + stateView.cameraLensHeight;
					}
				}
 
				box2dDebugArt.visible = _citrusObject.visible;
 
			} else {
 
				// The position = object position + (camera position * inverse parallax)
				x = _citrusObject.x + (-stateView.viewRoot.x * (1 - _citrusObject.parallax)) + _citrusObject.offsetX;
				y = _citrusObject.y + (-stateView.viewRoot.y * (1 - _citrusObject.parallax)) + _citrusObject.offsetY;
				visible = _citrusObject.visible;
				rotation = deg2rad(_citrusObject.rotation);
				scaleX = _citrusObject.inverted ? -1 : 1;
				registration = _citrusObject.registration;
				view = _citrusObject.view;
				animation = _citrusObject.animation;
				group = _citrusObject.group;
			}
		}
 
		private function handleContentLoaded(evt:Event):void {
 
			if (evt.target.loader.content is flash.display.MovieClip) {
 
				_textureAtlas = DynamicAtlas.fromMovieClipContainer(evt.target.loader.content, 1, 0, true, true);
				content = new MovieClip(_textureAtlas.getTextures(animation), _fpsMC);
				Starling.juggler.add(content as MovieClip);
			}
 
			if (evt.target.loader.content is Bitmap) {
 
				_texture = Texture.fromBitmap(evt.target.loader.content);
				content = new Image(_texture);
			}
 
			addChild(content);
		}
 
		private function handleContentIOError(evt:IOErrorEvent):void {
			throw new Error(evt.text);
		}
 
	}
}

There are three new important things :
– the static var loopAnimation dictionnary, to determine if the animation will play as a loop. Try to don’t have same animations name if one is a loop whereas the other isn’t!
– the var fpsMC, thanks to Starling each MovieClip may have a different fps, default is 30. This property is not integrated like other object’s properties (x, visible, view…) , because it is not available with the other views.
– the AnimationSequence class. With Starling there isn’t a class to manage the switch between animations, so I’ve created this one :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package com.citrusengine.view.starlingview {
 
	import starling.core.Starling;
	import starling.display.MovieClip;
	import starling.display.Sprite;
	import starling.textures.TextureAtlas;
 
	import flash.utils.Dictionary;
 
	/**
	 * The Animation Sequence class represents all object animations in one sprite sheet. You have to create your texture atlas in your state class.
	 * Example : var hero:Hero = new Hero("Hero", {x:400, width:60, height:130, view:new AnimationSequence(textureAtlas, ["walk", "duck", "idle", "jump"], "idle")});
	 * 
	 * @param textureAtlas : a TextureAtlas object with all your object's animations
	 * @param animations : an array with all your object's animations as a String
	 * @param firstAnimation : a string of your default animation at its creation
	 */
	public class AnimationSequence extends Sprite {
 
		private var _textureAtlas:TextureAtlas;
		private var _animations:Array;
		private var _mcSequences:Dictionary;
		private var _previousAnimation:String;
 
		public function AnimationSequence(textureAtlas:TextureAtlas, animations:Array, firstAnimation:String) {
 
			super();
 
			_textureAtlas = textureAtlas;
 
			_animations = animations;
 
			_mcSequences = new Dictionary();
 
			for each (var animation:String in animations) {
 
				if (_textureAtlas.getTextures(animation).length == 0) {
					throw new Error("One object doesn't have the " + animation + " animation in its TextureAtlas");
				}
 
				_mcSequences[animation] = new MovieClip(_textureAtlas.getTextures(animation));
 
			}
 
			addChild(_mcSequences[firstAnimation]);
			Starling.juggler.add(_mcSequences[firstAnimation]);
 
			_previousAnimation = firstAnimation;			
		}
 
		/**
		 * Called by StarlingArt, managed the MC's animations.
		 * @param animation : the MC's animation
		 * @param fps : the MC's fps
		 * @param animLoop : true if the MC is a loop
		 */
		public function changeAnimation(animation:String, fps:Number, animLoop:Boolean):void {
 
			if (!(_mcSequences[animation])) {
				throw new Error("One object doesn't have the " + animation + " animation set up in its initial array");
				return;
			}
 
			removeChild(_mcSequences[_previousAnimation]);
			Starling.juggler.remove(_mcSequences[_previousAnimation]);
 
			addChild(_mcSequences[animation]);
			Starling.juggler.add(_mcSequences[animation]);
			_mcSequences[animation].fps = fps;
			_mcSequences[animation].loop = animLoop;
 
			_previousAnimation = animation;
		}
 
		public function destroy():void {
 
			removeChild(_mcSequences[_previousAnimation]);
			Starling.juggler.remove(_mcSequences[_previousAnimation]);
 
			for each (var animation : String in _animations)
				_mcSequences[animation].dispose();
 
			_textureAtlas.dispose();
 
			_mcSequences = null;
		}
	}
}

For using it, you’ve to create a SpriteSheet. I use TexturePacker. Create all your different MovieClip in flash, each one represent a different state. They all should have the same scene width/height. Then export the swfs and import them in TexturePacker using Sparrow data format. Use the trim and enabe auto alias option, but not the crop! Export, you have your SpriteSheet with a xml.
And then embed them in your state class, and use it like that :

var bitmap:Bitmap = new _heroPng();
var texture:Texture = Texture.fromBitmap(bitmap);
var xml:XML = XML(new _heroConfig());
var sTextureAtlas:TextureAtlas = new TextureAtlas(texture, xml);
 
_hero = Hero(getFirstObjectByType(Hero));
_hero.view = new AnimationSequence(sTextureAtlas, ["walk", "duck", "idle", "jump", "hurt"], "idle");

You can find the swfs here : zip.
Maybe I will include the png & xml directly in the constructor param, so we will not have to create each time the texture altas. But if we have several objects with the same texture, it will be less optimized… Don’t hesitate to comment to tell me your preferences!

So I think this is it for the Starling view. You will find other informations in the demo code, showed later.

ABSTRACT GAME DATA
That was a request of the community : having a simple way to save game informations, datas… By the way, thanks Roger Clark, for helping lots of people this last month on the forum!!
The problem was : how to create a CE class which will not be modified by users, but will help them? An abstract dynamic class did the trick :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package com.citrusengine.utils {
 
	import org.osflash.signals.Signal;
 
	/**
	 * This is an (optional) abstract class to store your game's data such as lives, score, levels...
	 * You should extend this class & instantiate it into your main class using the gameData variable.
	 * You can dispatch a signal, dataChanged, if you update one of your data.
	 * For more information, watch the example below. 
	 */
	dynamic public class AGameData {
 
		public var dataChanged:Signal;
 
		protected var _lives:int = 3;
		protected var _score:int = 0;
		protected var _timeleft:int = 300;
 
		protected var _levels:Array;
 
		public function AGameData() {
 
			dataChanged = new Signal(String, Object);
		}
 
		public function get lives():int {
			return _lives;
		}
 
		public function set lives(lives:int):void {
 
			_lives = lives;
 
			dataChanged.dispatch("lives", _lives);
		}
 
		public function get score():int {
			return _score;
		}
 
		public function set score(score:int):void {
 
			_score = score;
 
			dataChanged.dispatch("score", _score);
		}
 
		public function get timeleft():int {
			return _timeleft;
		}
 
		public function set timeleft(timeleft:int):void {
 
			_timeleft = timeleft;
 
			dataChanged.dispatch("timeleft", _timeleft);
		}
 
		public function destroy():void {
 
			dataChanged.removeAll();
		}
	}
}

Finally we just create our GameData class which extends AGameData, and we use it like this :

gameData = new MyGameData();
//example in the main class :
levelManager.levels = gameData.levels;
 
//examples in the state class :
CitrusEngine.getInstance().gameData.dataChanged.add(myFunctionDataChanged);
CitrusEngine.getInstance().gameData.lives--;

LEVEL MANAGER :
The Level Manager was an other community request, so I’ve added mine. It is quite complex, but very powerful :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
package com.citrusengine.utils {
 
	import org.osflash.signals.Signal;
 
	import flash.display.Loader;
	import flash.events.Event;
	import flash.net.URLRequest;
 
	/**
	 * The LevelManager is a complex but powerful class, you can use simple states for levels with SWC/SWF/XML.
	 * Before using it, be sure that you have good OOP knowledge. For using it, you must use an Abstract state class 
	 * that you give as constructor parameter : Alevel. 
	 * 
	 * The four ways to set up your level : 
	 * <code>levelManager.levels = [Level1, Level2];
	 * levelManager.levels = [[Level1, "level1.swf"], [level2, "level2.swf"]];
	 * levelManager.levels = [[Level1, "level1.xml"], [level2, "level2.xml"]];
	 * levelManager.levels = [[Level1, Level1_SWC], [level2, Level2_SWC]];
	 * </code>
	 * 
	 * An instanciation exemple in your Main class (you may also use the AGameData to store your levels) :
	 * <code>levelManager = new LevelManager(ALevel);
	 * levelManager.onLevelChanged.add(_onLevelChanged);
	 * levelManager.levels = [Level1, Level2];
	 * levelManager.gotoLevel();</code>
	 * 
	 * The _onLevelChanged function gives in parameter the Alevel that you associate to your state : <code>state = lvl;</code>
	 * Then you can associate other function :
	 * <code>lvl.lvlEnded.add(_nextLevel);
	 * lvl.restartLevel.add(_restartLevel);</code>
	 * And their respective actions :
	 * <code>_levelManager.nextLevel();
	 * state = _levelManager.currentLevel as IState;</code>
	 * 
	 * The ALevel class must implement public var lvlEnded & restartLevel Signals in its constructor.
	 * If you have associated a SWF or SWC file to your level, you must add a flash MovieClip as a parameter into its constructor, 
	 * or a XML if it is one!
	 */
	public class LevelManager {
 
		static private var _instance:LevelManager;
 
		public var onLevelChanged:Signal;
 
		private var _ALevel:Class;
		private var _levels:Array;
		private var _currentIndex:uint;
		private var _currentLevel:Object;
 
		public function LevelManager(ALevel:Class) {
 
			_instance = this;
 
			_ALevel = ALevel;
 
			onLevelChanged = new Signal(_ALevel);
			_currentIndex = 0;
		}
 
		static public function getInstance():LevelManager {
			return _instance;
		}
 
 
		public function destroy():void {
 
			onLevelChanged.removeAll();
 
			_currentLevel = null;
		}
 
		public function nextLevel():void {
 
			if (_currentIndex < _levels.length - 1) {
				++_currentIndex;
			}
 
			gotoLevel();
		}
 
		public function prevLevel():void {
 
			if (_currentIndex > 0) {
				--_currentIndex;
			}
 
			gotoLevel();
		}
 
		/**
		 * Call the LevelManager instance's gotoLevel() function to launch your first level, or you may specify it.
		 * @param index : the level index from 1 to ... ; different from the levels' array indexes.
		 */
		public function gotoLevel(index:int = -1):void {
 
			if (_currentLevel != null) {
				_currentLevel.lvlEnded.remove(_onLevelEnded);
			}
 
			var loader:Loader = new Loader();
 
			if (index != -1) {
				_currentIndex = index - 1;
			}
 
			// Level SWF and SWC are undefined
			if (_levels[_currentIndex][0] == undefined) {
 
				_currentLevel = _ALevel(new _levels[_currentIndex]);
				_currentLevel.lvlEnded.add(_onLevelEnded);
 
				onLevelChanged.dispatch(_currentLevel);
 
			// It's a SWC ?
			} else if (_levels[_currentIndex][1] is Class) {
 
				_currentLevel = _ALevel(new _levels[_currentIndex][0](new _levels[_currentIndex][1]()));
				_currentLevel.lvlEnded.add(_onLevelEnded);
 
				onLevelChanged.dispatch(_currentLevel);
 
			// So it's a SWF or XML, we load it 
			} else {
 
				loader.load(new URLRequest(_levels[_currentIndex][1]));
				loader.contentLoaderInfo.addEventListener(Event.COMPLETE,_levelLoaded);
			}
		}
 
		private function _levelLoaded(evt:Event):void {
 
			_currentLevel = _ALevel(new _levels[_currentIndex][0](evt.target.loader.content));
			_currentLevel.lvlEnded.add(_onLevelEnded);
 
			onLevelChanged.dispatch(_currentLevel);
 
			evt.target.removeEventListener(Event.COMPLETE, _levelLoaded);
			evt.target.loader.unloadAndStop();
		}
 
		private function _onLevelEnded():void {
 
		}
 
		public function get levels():Array {
			return _levels;
		}
 
		public function set levels(levels:Array):void {
			_levels = levels;
		}
 
		public function get currentLevel():Object {
			return _currentLevel;
		}
 
		public function set currentLevel(currentLevel:Object):void {
			_currentLevel = currentLevel;
		}
 
		public function get nameCurrentLevel():String {
			return _currentLevel.nameLevel;
		}
	}
}

Let’s see how it is used with the demo.

THE DEMO CODE :
You’re always here ? This is great 😀 It’s time for all the demo code :

Main :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package {
 
	import com.citrusengine.core.CitrusEngine;
	import com.citrusengine.core.IState;
	import com.citrusengine.utils.LevelManager;
 
	[SWF(backgroundColor="#FFFFFF", frameRate="60", width="640", height="480")]
 
	/**
	 * @author Aymeric
	 */
	public class Main extends CitrusEngine {
 
		public function Main() {
 
			setUpStarling(true);
 
			gameData = new MyGameData();
 
			levelManager = new LevelManager(ALevel);
			levelManager.onLevelChanged.add(_onLevelChanged);
			levelManager.levels = gameData.levels;
			levelManager.gotoLevel();
		}
 
		private function _onLevelChanged(lvl:ALevel):void {
 
			state = lvl;
 
			lvl.lvlEnded.add(_nextLevel);
			lvl.restartLevel.add(_restartLevel);
		}
 
		private function _nextLevel():void {
 
			levelManager.nextLevel();
		}
 
		private function _restartLevel():void {
 
			state = levelManager.currentLevel as IState;
		}
	}
}

MyGameData :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package {
 
	import com.citrusengine.utils.AGameData;
 
	/**
	 * @author Aymeric
	 */
	public class MyGameData extends AGameData {
 
		public function MyGameData() {
 
			super();
 
			_levels = [[Level1, "levels/A1/LevelA1.swf"], [Level2, "levels/A2/LevelA2.swf"]];
		}
 
		public function get levels():Array {
			return _levels;
		}
 
		override public function destroy():void {
 
			super.destroy();
		}
 
	}
}

ALevel:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
package {
 
	import Box2DAS.Dynamics.ContactEvent;
 
	import starling.display.Quad;
	import starling.text.BitmapFont;
	import starling.text.TextField;
	import starling.textures.Texture;
	import starling.textures.TextureAtlas;
	import starling.utils.Color;
 
	import com.citrusengine.core.CitrusEngine;
	import com.citrusengine.core.StarlingState;
	import com.citrusengine.math.MathVector;
	import com.citrusengine.objects.CitrusSprite;
	import com.citrusengine.objects.platformer.Baddy;
	import com.citrusengine.objects.platformer.Hero;
	import com.citrusengine.objects.platformer.Platform;
	import com.citrusengine.objects.platformer.Sensor;
	import com.citrusengine.physics.Box2D;
	import com.citrusengine.utils.ObjectMaker;
	import com.citrusengine.view.starlingview.AnimationSequence;
 
	import org.osflash.signals.Signal;
 
	import flash.display.Bitmap;
	import flash.display.MovieClip;
	import flash.geom.Rectangle;
 
	/**
	 * @author Aymeric
	 */
	public class ALevel extends StarlingState {
 
		public var lvlEnded:Signal;
		public var restartLevel:Signal;
 
		protected var _ce:CitrusEngine;
		protected var _level:MovieClip;
 
		protected var _hero:Hero;
 
		[Embed(source="../embed/Hero.xml", mimeType="application/octet-stream")]
		private var _heroConfig:Class;
 
		[Embed(source="../embed/Hero.png")]
		private var _heroPng:Class;
 
		[Embed(source="../embed/ArialFont.fnt", mimeType="application/octet-stream")]
		private var _fontConfig:Class;
 
		[Embed(source="../embed/ArialFont.png")]
		private var _fontPng:Class;
 
		protected var _maskDuringLoading:Quad;
		protected var _percentTF:TextField;
 
		public function ALevel(level:MovieClip = null) {
 
			super();
 
			_ce = CitrusEngine.getInstance();
 
			_level = level;
 
			lvlEnded = new Signal();
			restartLevel = new Signal();
 
			// Useful for not forgetting to import object from the Level Editor
			var objectsUsed:Array = [Hero, Platform, Baddy, Sensor, CitrusSprite];
		}
 
		override public function initialize():void {
 
			super.initialize();
 
			var box2d:Box2D = new Box2D("Box2D");
			//box2d.visible = true;
			add(box2d);
 
			// hide objects loading in the background
			_maskDuringLoading = new Quad(stage.stageWidth, stage.stageHeight);
			_maskDuringLoading.color = 0x000000;
			_maskDuringLoading.x = (stage.stageWidth - _maskDuringLoading.width) / 2;
			_maskDuringLoading.y = (stage.stageHeight - _maskDuringLoading.height) / 2;
			addChild(_maskDuringLoading);
 
			// create a textfield to show the loading %
			var bitmap:Bitmap = new _fontPng();
			var ftTexture:Texture = Texture.fromBitmap(bitmap);
			var ftXML:XML = XML(new _fontConfig());
			TextField.registerBitmapFont(new BitmapFont(ftTexture, ftXML));
 
			_percentTF = new TextField(400, 200, "", "ArialMT");
			_percentTF.fontSize = BitmapFont.NATIVE_SIZE;
			_percentTF.color = Color.WHITE;
			_percentTF.autoScale = true;
			_percentTF.x = (stage.stageWidth - _percentTF.width) / 2;
			_percentTF.y = (stage.stageHeight - _percentTF.height) / 2;
 
			addChild(_percentTF);
 
			// when the loading is completed...
			view.loadManager.onLoadComplete.addOnce(_handleLoadComplete);
 
			// create objects from our level made with Flash Pro
			ObjectMaker.FromMovieClip(_level);
 
			// the hero view come from a sprite sheet, for the baddy that was a swf
			bitmap = new _heroPng();
			var texture:Texture = Texture.fromBitmap(bitmap);
			var xml:XML = XML(new _heroConfig());
			var sTextureAtlas:TextureAtlas = new TextureAtlas(texture, xml);
 
			_hero = Hero(getFirstObjectByType(Hero));
			_hero.view = new AnimationSequence(sTextureAtlas, ["walk", "duck", "idle", "jump", "hurt"], "idle");
			_hero.hurtDuration = 500;
 
			view.setupCamera(_hero, new MathVector(320, 240), new Rectangle(0, 0, 1550, 450), new MathVector(.25, .05));
		}
 
		protected function _changeLevel(cEvt:ContactEvent):void {
 
			if (cEvt.other.GetBody().GetUserData() is Hero) {
				lvlEnded.dispatch();
			}
		}
 
		protected function _handleLoadComplete():void {
 
			removeChild(_percentTF);
			removeChild(_maskDuringLoading);
		}
 
		override public function update(timeDelta:Number):void {
 
			super.update(timeDelta);
 
			var percent:uint = view.loadManager.bytesLoaded / view.loadManager.bytesTotal * 100;
 
			if (percent < 99) {
				_percentTF.text = percent.toString() + "%";
			}
		}
 
		override public function destroy():void {
 
			TextField.unregisterBitmapFont("ArialMT");
 
			super.destroy();
		}
	}
}

Level1 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package {
 
	import Box2DAS.Dynamics.ContactEvent;
 
	import starling.core.Starling;
	import starling.extensions.particles.ParticleDesignerPS;
	import starling.extensions.particles.ParticleSystem;
	import starling.text.BitmapFont;
	import starling.text.TextField;
	import starling.textures.Texture;
	import starling.utils.Color;
 
	import com.citrusengine.objects.platformer.Hero;
	import com.citrusengine.objects.platformer.Sensor;
 
	import flash.display.MovieClip;
 
	/**
	 * @author Aymeric
	 */
	public class Level1 extends ALevel {
 
		[Embed(source="../embed/Particle.pex", mimeType="application/octet-stream")]
		private var _particleConfig:Class;
 
		[Embed(source="../embed/ParticleTexture.png")]
		private var _particlePng:Class;
 
		private var _particleSystem:ParticleSystem;
 
		private var _bmpFontTF:TextField;
 
		public function Level1(level:MovieClip = null) {
			super(level);
		}
 
		override public function initialize():void {
 
			super.initialize();
 
			var psconfig:XML = new XML(new _particleConfig());
			var psTexture:Texture = Texture.fromBitmap(new _particlePng());
 
			_particleSystem = new ParticleDesignerPS(psconfig, psTexture);
			_particleSystem.start();
			Starling.juggler.add(_particleSystem);
 
			var endLevel:Sensor = Sensor(getObjectByName("endLevel"));
			endLevel.view = _particleSystem;
 
			_bmpFontTF = new TextField(400, 200, "The Citrus Engine goes on Stage3D thanks to Starling", "ArialMT");
			_bmpFontTF.fontSize = BitmapFont.NATIVE_SIZE;
			_bmpFontTF.color = Color.WHITE;
			_bmpFontTF.autoScale = true;
			_bmpFontTF.x = (stage.stageWidth - _bmpFontTF.width) / 2;
			_bmpFontTF.y = (stage.stageHeight - _bmpFontTF.height) / 2;
 
			addChild(_bmpFontTF);
			_bmpFontTF.visible = false;
 
			var popUp:Sensor = Sensor(getObjectByName("popUp"));
 
			endLevel.onBeginContact.add(_changeLevel);
 
			popUp.onBeginContact.add(_showPopUp);
			popUp.onEndContact.add(_hidePopUp);
		}
 
		private function _showPopUp(cEvt:ContactEvent):void {
 
			if (cEvt.other.GetBody().GetUserData() is Hero) {
				_bmpFontTF.visible = true;
			}
		}
 
		private function _hidePopUp(cEvt:ContactEvent):void {
 
			if (cEvt.other.GetBody().GetUserData() is Hero) {
				_bmpFontTF.visible = false;
			}
		}
 
		override public function destroy():void {
 
			Starling.juggler.remove(_particleSystem);
			_particleSystem.stop();
			_particleSystem.dispose();
 
			removeChild(_bmpFontTF);
 
			super.destroy();
		}
 
	}
}

Level2 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package {
 
	import starling.text.BitmapFont;
	import starling.text.TextField;
	import starling.utils.Color;
 
	import flash.display.MovieClip;
	import flash.events.TimerEvent;
	import flash.utils.Timer;
 
	/**
	 * @author Aymeric
	 */
	public class Level2 extends ALevel {
 
		private var _timer:Timer;
 
		private var _bmpFontTF:TextField;
 
		public function Level2(level:MovieClip = null) {
 
			super(level);
		}
 
		override public function initialize():void {
 
			super.initialize();
 
			_timer = new Timer(3000);
			_timer.addEventListener(TimerEvent.TIMER, _onTick);
 
			_bmpFontTF = new TextField(400, 200, "This is a performance test level. Box2D physics become some time unstable. You can see box2d bodies thanks to the console.", "ArialMT");
			_bmpFontTF.fontSize = BitmapFont.NATIVE_SIZE;
			_bmpFontTF.color = Color.WHITE;
			_bmpFontTF.autoScale = true;
			_bmpFontTF.x = (stage.stageWidth - _bmpFontTF.width) / 2;
			_bmpFontTF.y = (stage.stageHeight - _bmpFontTF.height) / 2;
		}
 
		override protected function _handleLoadComplete():void {
 
			super._handleLoadComplete();
 
			addChild(_bmpFontTF);
			_timer.start();
		}
 
		private function _onTick(tEvt:TimerEvent):void {
 
			if (_timer.currentCount == 2)
				removeChild(_bmpFontTF);
 
			// PhysicsEditorObjects class is created by the software PhysicsEditor and its additional CitrusEngine template.
			// Muffins are not in front of everything due to the foreground group param set to 1 in the Level Editor, default is 0.
			var muffin:PhysicsEditorObjects = new PhysicsEditorObjects("muffin", {peObject:"muffin", view:"muffin.png", registration:"topLeft", x:Math.random() * view.cameraBounds.width});
			add(muffin);
		}
 
		override public function destroy():void {
 
			_timer.removeEventListener(TimerEvent.TIMER, _onTick);
			_timer.stop();
			_timer = null;
 
			super.destroy();
		}
	}
}

And finally the PhysicsEditorObjects :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
package {
 
	import Box2DAS.Collision.Shapes.b2PolygonShape;
	import Box2DAS.Common.V2;
 
	import com.citrusengine.objects.platformer.Crate;
 
	/**
	 * @author Aymeric
	 * <p>This is a class created by the software http://www.physicseditor.de/</p>
	 * <p>Just select the CitrusEngine template, upload your png picture, set polygons and export.</p>
	 * <p>Be careful, the registration point is topLeft !</p>
	 * @param peObject : the name of the png file
	 */
    public class PhysicsEditorObjects extends Crate {
 
		[Inspectable(defaultValue="")]
		public var peObject:String = "";
 
		private var _tab:Array;
 
		public function PhysicsEditorObjects(name:String, params:Object = null) {
 
			super(name, params);
		}
 
		override public function destroy():void {
 
			super.destroy();
		}
 
		override public function update(timeDelta:Number):void {
 
			super.update(timeDelta);
		}
 
		override protected function defineFixture():void {
 
			super.defineFixture();
 
			_createVertices();
 
			_fixtureDef.density = _getDensity();
			_fixtureDef.friction = _getFriction();
			_fixtureDef.restitution = _getRestitution();
 
			for (var i:uint = 0; i < _tab.length; ++i) {
				var polygonShape:b2PolygonShape = new b2PolygonShape();
				polygonShape.Set(_tab[i]);
				_fixtureDef.shape = polygonShape;
 
				body.CreateFixture(_fixtureDef);
			}
		}
 
        protected function _createVertices():void {
 
			_tab = [];
			var vertices:Vector.<V2> = new Vector.<V2>();
 
			switch (peObject) {
 
				case "muffin":
 
			        vertices.push(new V2(-0.5/_box2D.scale, 81.5/_box2D.scale));
					vertices.push(new V2(10.5/_box2D.scale, 59.5/_box2D.scale));
					vertices.push(new V2(46.5/_box2D.scale, 27.5/_box2D.scale));
					vertices.push(new V2(50.5/_box2D.scale, 27.5/_box2D.scale));
					vertices.push(new V2(92.5/_box2D.scale, 61.5/_box2D.scale));
					vertices.push(new V2(99.5/_box2D.scale, 79.5/_box2D.scale));
					vertices.push(new V2(59.5/_box2D.scale, 141.5/_box2D.scale));
					vertices.push(new V2(17.5/_box2D.scale, 133.5/_box2D.scale));
 
					_tab.push(vertices);
					vertices = new Vector.<V2>();
 
			        vertices.push(new V2(59.5/_box2D.scale, 141.5/_box2D.scale));
					vertices.push(new V2(99.5/_box2D.scale, 79.5/_box2D.scale));
					vertices.push(new V2(83.5/_box2D.scale, 133.5/_box2D.scale));
 
					_tab.push(vertices);
					vertices = new Vector.<V2>();
 
			        vertices.push(new V2(50.5/_box2D.scale, 27.5/_box2D.scale));
					vertices.push(new V2(46.5/_box2D.scale, 27.5/_box2D.scale));
					vertices.push(new V2(42.5/_box2D.scale, -0.5/_box2D.scale));
 
					_tab.push(vertices);
 
					break;
 
			}
		}
 
		protected function _getDensity():Number {
 
			switch (peObject) {
 
				case "muffin":
					return 1;
					break;
 
			}
 
			return 1;
		}
 
		protected function _getFriction():Number {
 
			switch (peObject) {
 
				case "muffin":
					return 0.6;
					break;
 
			}
 
			return 0.6;
		}
 
		protected function _getRestitution():Number {
 
			switch (peObject) {
 
				case "muffin":
					return 0.3;
					break;
 
			}
 
			return 0.3;
		}
	}
}

This is it! Again, everything is available on the CE’s google code. But hey, this is the CitrusEngine V3 BETA 1. What is next !?

LOOKING FOR CONTRIBUTOR
The CE is currently looking for some new contributors for more amazing features! If you’re an advanced game developer, or a simple student (like me), you can contribute!

What about the Inspectable metadata tag?
I would like to add it, but now with Starling support it means that Flash Pro must be able to target FP11 that’s a bit complicated. So not at the moment unless you ask for it!

LEVEL ARCHITECT
The Level Architect is a great tool but not quite reached. It needs always some work. Personnaly I prefer using Flash Pro, some people don’t. That would be cool if someone would contribute to it.

MOBILE
I’ll be honest : if you want to create a mobile game and you have more than 5 dynamics objects, forget the CitrusEngine for the moment. Box2D is a performance killer on mobile with Flash. However Eric started a simpler class for collision management : CitrusSolver. It is well advanced, just waiting for some more work.

ENTITY/COMPONENT SYSTEM
I haven’t include my ladder management in the Hero class. Because the Hero class would become more & more complex… what if it uses a sword, a gun, a rope… ? Extending it is not enough. In a previous post, I’ve explained why we need a simple entity system. Richard Lord has made an amazing blog post : What is an entity framework for game development?. Now I’ve fully understand how it works. But mixing it with box2d, frame animation, input management doesn’t sound easy…

CONCLUSION
If you have read everything, that’s awesome! Feel free to comment / request features for the CitrusEngine, and if you want to be a contributor you’re welcome !
For the next months, I will continue to be active for the CE’s community, always playing with Flash AS3, learning iOS, learning haXe nme, and work hard on my 2nd year school project. And finally I’m looking for job, beginning in July or later.

Oh and I will be at the World Wide haXe conf on Friday 13 – Saturday 14 April, at Paris. Hope to meet some of you there 🙂

23 thoughts on “CitrusEngine goes Stage3D with Starling

  1. Great work Aymeric!
    I would like to know if the Nape physic engine could be integrated in CE instead of Box2D? I believe that Nape is less perfomance killer (but I am not sure) and maybe it could be more relevant than Box2D?

  2. Thanks guys !
    @Skeddio it shouldn’t be too complicated. I’ve never used Nape, does it run well on mobile devices ? Alchemy Box2D is not a problem on computers, but on mobiles… :/

  3. Actually I never used Nape too… I just have seen the video tutorial from Lee Brimelow website and I have read next this kind of benchmark : Flash 2D Physics engines fast comparison: Nape (haXe) vs Box2D (Alchemy)
    http://blog.codestage.ru/2011/11/09/2dphysics/

    According to this result, it seems that Nape is much more efficient than Box2D, specially on mobiles… but I have read other comments from guys who have a different feedback so I wonder what to think…

    For Box2D, your feedback is bad for mobile but Angry Birds is built upon it and runs pretty well… ok it is Objective-C but I guess that performances with Starling last release must be not so far from Objective-C???

    Anyway, thanks again for your great work and to share your experiences. It is a bit weird to talk with you in English cause I am french living in Geneva (so we are almost neighbors 😉

  4. It seems to run very well !
    When I’ll have a bit time, I will try a simple version on my phone.

    Starling is really great for graphics (GPU), but doesn’t change anything on the CPU, as far as I know.
    A simple test : 10 dynamic squares (without graphics) on my iPhone 4S with Box2D and it runs at 3 – 7 fps.

    Hey, I live in Annecy! If you want to speak in French : aymeric.lamboley at gmail.com 😉

  5. Hey,

    Great work!

    As I know for mobile blitting is very noneffective technique. The best is caching animations as bitmapdata and just change them in GPU mode.
    Like it is described here:
    http://esdot.ca/site/2012/fast-rendering-in-air-cached-spritesheets

    Does CE support something similar? Can I do something similar by converting movieclips into spritesheets runtime?

    I just found CE and I want to know if I understand it correctly. CE let me change rendering method with very small effort. For example I can use blitting for desktop, cached bitmap for mobile, and when I decide FP11 is enough popular I can change it to Starling Stage3D?

  6. Hi Kuba, you can create your own view. But when stage3d will go on mobile, I hope it will crush those old methods.

    It is not very hard, but it is not automatic. You will use 3 different states e.g. SpriteState, BlittingState & StarlingState with lots of similar code. And finally just check in your Main class, if it is a device or anything else.

  7. Hi, I wrote that blog post you’ve linked to. I’ve tested Starling alot on mobile, and it will not out do cached bitmaps in GPU Mode. In fact, GPU Mode will still yield a 30-50% better performance than Starling in my tests.

  8. Hi Aymeric (bonjour Aymeric)

    First I would like to thank you for your wonderful contribution on CE and Starling.

    I tested your V3 Beta1 and I can see very low performances like 10-15 fps on the 1st Level. Then the performance come back on the good 60 fps when I disabled the particles effects.
    What do you think about it?

    (FD 4.0.0.RC2 – Flex SDK 4.5.1 with Citrus Engine V3 Beta1)

    Franck
    (le bonjour de Thailande)

  9. Hey Franck,

    Do you compile and test with the non debugger version ? If we compile in debug mode, and run it into a debugger we have really bad performances, that’s “normal”.
    Do you have bad performance on my online version too ? What is your system & specifications ?

    Il doit faire bien bon en Thailande 😉
    Aymeric

  10. Hi Aymeric (bonjour)

    You were right! (c’était effectivement ça).
    It works like a charm at high speed using the release mode in Flash Develop.

    Thanks again for your reply (et un grand merci pour votre réponse).
    I will look more in details all your previous Citrus Engine posts in order to try to embed all the graphic assets…and of course, waiting for the next release of the Level Architect)

    Yes, it is very hot in Thailand now (il fait 34.6 °C à l’intérieur)
    Have a nice day (bonne journée)
    Franck

  11. Hey man!

    Thanks you for this great introduction!

    Am trying to update the starling build that comes bundled with CitrusEngine to the latest build of starling, and am running into a problem. In ‘animation’ setter of “StarlingArt” class, you seem to be calling a MovieClip. changeTextures method, but I can’t seem to find that method in the starling builds. May I know which version of Starling comes bundled with the CitrusEngine? Did you patch Starling to include that method?

    Thanks again!

  12. Thanks for your reply! I copied over the changeTextures method over to the updated Starling port code and it worked.

    Do you have any thoughts on how to do this better so we can have Starling as an external dependency instead of patching it every time? I spent some time thinking but couldn’t come up with a neat solution..

    Thanks again for your work!

  13. I get some problems in the level manager file.. Dont know why

    Type was not found or was not a compile-time constant: Loader.. it is some namespace problem i cant fix


    package com.citrusengine.utils {

    import org.osflash.signals.Signal;

    import flash.display.Loader;
    import flash.events.Event;
    import flash.net.URLRequest;

    /**
    * The LevelManager is a complex but powerful class, you can use simple states for levels with SWC/SWF/XML.
    * Before using it, be sure that you have good OOP knowledge. For using it, you must use an Abstract state class
    * that you give as constructor parameter : Alevel.
    *
    * The four ways to set up your level :
    *
    levelManager.levels = [Level1, Level2];
    * levelManager.levels = [[Level1, "level1.swf"], [level2, "level2.swf"]];
    * levelManager.levels = [[Level1, "level1.xml"], [level2, "level2.xml"]];
    * levelManager.levels = [[Level1, Level1_SWC], [level2, Level2_SWC]];
    *

    *
    * An instanciation exemple in your Main class (you may also use the AGameData to store your levels) :
    * levelManager = new LevelManager(ALevel);
    * levelManager.onLevelChanged.add(_onLevelChanged);
    * levelManager.levels = [Level1, Level2];
    * levelManager.gotoLevel();

    *
    * The _onLevelChanged function gives in parameter the Alevel that you associate to your state : state = lvl;
    * Then you can associate other function :
    * lvl.lvlEnded.add(_nextLevel);
    * lvl.restartLevel.add(_restartLevel);

    * And their respective actions :
    * _levelManager.nextLevel();
    * state = _levelManager.currentLevel as IState;

    *
    * The ALevel class must implement public var lvlEnded & restartLevel Signals in its constructor.
    * If you have associated a SWF or SWC file to your level, you must add a flash MovieClip as a parameter into its constructor,
    * or a XML if it is one!
    */
    public class LevelManager {

    static private var _instance:LevelManager;

    public var onLevelChanged:Signal;

    private var _ALevel:Class;
    private var _levels:Array;
    private var _currentIndex:uint;
    private var _currentLevel:Object;

    public function LevelManager(ALevel:Class) {

    _instance = this;

    _ALevel = ALevel;

    onLevelChanged = new Signal(_ALevel);
    _currentIndex = 0;
    }

    static public function getInstance():LevelManager {
    return _instance;
    }

    public function destroy():void {

    onLevelChanged.removeAll();

    _currentLevel = null;
    }

    public function nextLevel():void {

    if (_currentIndex 0) {
    --_currentIndex;
    }

    gotoLevel();
    }

    /**
    * Call the LevelManager instance's gotoLevel() function to launch your first level, or you may specify it.
    * @param index : the level index from 1 to ... ; different from the levels' array indexes.
    */
    public function gotoLevel(index:int = -1):void {

    if (_currentLevel != null) {
    _currentLevel.lvlEnded.remove(_onLevelEnded);
    }

    if (index != -1) {
    _currentIndex = index - 1;
    }

    // Level SWF and SWC are undefined
    if (_levels[_currentIndex][0] == undefined) {

    _currentLevel = _ALevel(new _levels[_currentIndex]);
    _currentLevel.lvlEnded.add(_onLevelEnded);

    onLevelChanged.dispatch(_currentLevel);
    return;

    // It's a SWC ?
    } else if (_levels[_currentIndex][1] is Class) {
    _currentLevel = _ALevel(new _levels[_currentIndex][0](new _levels[_currentIndex][1]()));
    _currentLevel.lvlEnded.add(_onLevelEnded);
    onLevelChanged.dispatch(_currentLevel);
    return;
    }
    var loader:Loader = new Loader();
    loader.load(new URLRequest(_levels[_currentIndex][1]));
    loader.contentLoaderInfo.addEventListener(Event.COMPLETE,_levelLoaded);
    }

    private function _levelLoaded(evt:Event):void {

    _currentLevel = _ALevel(new _levels[_currentIndex][0](evt.target.loader.content));
    _currentLevel.lvlEnded.add(_onLevelEnded);

    onLevelChanged.dispatch(_currentLevel);

    evt.target.removeEventListener(Event.COMPLETE, _levelLoaded);
    evt.target.loader.unloadAndStop();
    }

    private function _onLevelEnded():void {

    }

    public function get levels():Array {
    return _levels;
    }

    public function set levels(levels:Array):void {
    _levels = levels;
    }

    public function get currentLevel():Object {
    return _currentLevel;
    }

    public function set currentLevel(currentLevel:Object):void {
    _currentLevel = currentLevel;
    }

    public function get nameCurrentLevel():String {
    return _currentLevel.nameLevel;
    }
    }
    }

Leave a Reply

Your email address will not be published. Required fields are marked *