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 🙂
Great job ! Bravo 🙂
Man, This is really a great work.
Thanks
Fantastic work Aymeric.
Brilliant work, Aymeric — thanks for all your efforts! I can’t wait to see the games that will be created with the latest Citrus Engine. 🙂
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?
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… :/
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 😉
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 😉
Hey Aymeric, congrats and thanks for this update!!!!! I’m sure that this new combination of CE, Starling and Box2d will be explosive!
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?
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.
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.
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)
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
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
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!
Hey ! Indeed, I’ve added the changeTextures method into Starling library (shame on me). It was used to create dynamic texture atlas thanks to swf. The method is very similar to the MovieClip constructor’s method. You will find the changeTextures method there : http://code.google.com/p/citrus-engine/source/browse/trunk/starling/display/MovieClip.as
It was the 0.9.1 version.
It was the only change to the Starling framework.
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!
We could just create a class which extends Starling’s MovieClip and add this method 😉
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;
}
}
}
Hey, have you grab the last version in the SVN (in the trunk) ?
Really informative and great work done! Awesome features for citrus engine.