Posts tagged flash
Android 4.2.2. resolve problem with AIR mobile deployment
2Google has recently pushed a new Android version: 4.2.2. Concerning the new features, this update brings about a new security feature in regards to USB debugging: now there is a gatekeeper on your phone! You will have to accept the RSA key before being able to deploy on your device. More information concerning this update there.
If you have made this update on your device, you won’t be able to deploy your application, even if you’ve accepted the RSA key. Your IDE won’t success to connect to the device and push on it.
When I tried to resolve the problem, it quickly appears it wasn’t related to the IDE but directly from the AIR SDK. Then I tried to deploy on my tablet with a Unity project: same problem. Using Unity, you work directly with the Android SDK. I just needed to download its latest version and replace the previous one, and it works!
Concerning AIR, we also use the Android SDK but not with its original structure. You need to replace some files on this folder: YourSDK/lib/android/bin. Replace those files: aapt, adb and dx.jar with files coming from the latest Android SDK: sdk/platform-tools aapt, adb and its lib folder with the dx.jar file. Using Windows you will also need to replace the dll files.
That was easy! I hope Adobe will quickly deploy an AIR SDK update for everyone.
Take photo with AIR on iOS
3Hey guys,
Recently, I’ve worked on a mobile project and camera use drives me crazy about performances. On iOS, after taking a photo and validate it, it freezes the application during one minute to be able to encode it.
I’ve made lots of tests with several libraries, using Alchemy ones, and finally the fastest way (less than 5 seconds) is to use the new BitmapData encode method, available since Flash Player 11.3 and AIR 3.3.
Here is the code (lots of code come from this excellent post) :
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 | if (CameraUI.isSupported) { viewsAC.addItem({label:"Take Photo", icon:cameraIcon}); myCam = new CameraUI(); myCam.addEventListener(MediaEvent.COMPLETE, onCameraComplete); } protected function onCameraComplete(evt:MediaEvent):void { sqlStatement = new SQLStatement(); sqlStatement.sqlConnection = model2.connection; sqlStatement.text = "INSERT INTO albumItems (album, photoFile, dateAdded) " + "VALUES (:album, :photoFile, :dateAdded)"; sqlStatement.parameters[":album"] = model2.selectedAlbum; var mediaPromise:MediaPromise = evt.data; if (mediaPromise.file == null) { // For iOS we need to load with a Loader first _loader = new Loader(); _loader.contentLoaderInfo.addEventListener(Event.COMPLETE, imageloader_complete, false, 0, true); _loader.loadFilePromise(mediaPromise); return; } else { // Android & BlackBerry registerPhoto(mediaPromise.file.url) } } private function imageloader_complete(event:Event):void { _loader.contentLoaderInfo.removeEventListener(Event.COMPLETE, imageloader_complete); var loaderInfo:LoaderInfo = event.target as LoaderInfo; if (CameraRoll.supportsAddBitmapData) { var bitmapData:BitmapData = new BitmapData(loaderInfo.width, loaderInfo.height); bitmapData.draw(loaderInfo.loader); var file:File = File.applicationStorageDirectory.resolvePath("image" + new Date().time + ".jpg"); var stream:FileStream = new FileStream() stream.open(file, FileMode.WRITE); var bytes:ByteArray = bitmapData.encode(bitmapData.rect, new flash.display.JPEGEncoderOptions()); stream.writeBytes(bytes, 0, bytes.bytesAvailable); stream.close(); } registerPhoto(file.url) } private function registerPhoto(url:String):void { sqlStatement.parameters[":photoFile"] = url; sqlStatement.parameters[":dateAdded"] = new Date(); sqlStatement.execute(); viewsList.selectedIndex = -1; } |
Citrus Engine V3 and a new website!
2Hey there,
I’m glad to announce the V3 release and a new website for the Citrus Engine! It has been a long and hard work. Most of the new tutorials and news will be displayed on this new website. Don’t know how I will manage this blog on Citrus Engine subject due to the new website, I will certainly replicate some contents.
Anyway I let you explore the new website
Don’t hesitate to give some feedback.
Playing with Cadet Editor 3D and AwayPhysics
6The 3D part is a bit in stand-by this last weeks on the Citrus Engine, because I’m polishing the engine for the V3 which should be out this week!
I’ve never clearly introduced AwayPhysics in the Citrus Engine, so it’s the day! The idea is to have a similar pre-built platformer objects that we already have with Box2D and Nape but with AwayPhysics this time for 3D stuff! This work and Away3D support will evolve during the V3.
Now that 3D views and physics are supported, it’s time to give a look on which tool we can use as a 3D game Level Editor. At first, I thought to Prefab. This is the best tool to create a scene with Away3D, importing assets, add lights… But too complex for a simple level design, I mean : hey the Citrus Engine is not a concurrent to Unity3D (which I started to learn thanks to this awesome tutorial)
Its 3D physics part is just here to create basic 3D game / puzzle. Also the level editor has to support object creation (physics object), this isn’t obvious with Prefab.
Then I gave a look to Cadet Editor, the 3D editor is very easy to handle. You can create quickly sphere, cube and plane objects, add lights… That’s the right tool to see what can be done!
I really enjoy the design of Cadet Editor, however it should manage right click with camera rotation instead of displaying a simple menu with a link to Away3D.
In Cadet3D project are saved into a format close to MXML. See our examples and its code :

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 | <cadet:CadetScene x:id="0" framerate="30" name="Component" timeScale="1" xmlns:cadet="www.unwrong.com/cadet/1.0.0" xmlns:x="org.bonesframework.core.serialization.Serializer" xmlns:bones="www.bonesframwork.org/bones/1.0.0" xmlns:cadetAway3D4="www.unwrong.com/cadetAway3D4/1.0.0" xmlns:ns0="cadetAway3D4.components.cameras" xmlns:ns1="cadetAway3D4.components.materials" xmlns:ns2="cadetAway3D4.components.geom" xmlns:ns3="cadetAway3D4.components.lights"> <bones:ArrayCollection x:name="children" x:id="1"> <cadetAway3D4:RendererAway3D x:name="0" x:id="2" name="Away3D 4 Renderer"> <ns0:CameraComponent x:name="cameraComponent" x:id="3" transform="0.9833181500434875,-8.754431490842762e-8,-0.18189382553100586,0,0.12057413905858994,0.7487242221832275,0.6518235206604004,0,0.13618825376033783,-0.66288161277771,0.7362341284751892,0,-191.17271423339844,590.8668212890625,-854.3065185546875,1" name="Camera"> <bones:ArrayCollection x:name="children" x:id="4"/> </ns0:CameraComponent> </cadetAway3D4:RendererAway3D> <cadetAway3D4:MeshComponent x:name="1" x:id="5" transform="1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1" name="Plane"> <ns1:ColorMaterialComponent x:name="materialComponent" x:id="6" gloss="50" specularStrength="1" color="333722" name="Blue Material" depthCompareMode="less"/> <bones:ArrayCollection x:name="children" x:id="7"> <ns2:PlaneGeometryComponent x:name="0" x:id="8" height="1000" segmentsH="1" segmentsW="1" name="a platform" width="1000"/> </bones:ArrayCollection> <x:Ref x:name="geometryComponent" x:id="8"/> </cadetAway3D4:MeshComponent> <ns3:DirectionalLightComponent x:name="2" x:id="9" color="16777215" specular="1" ambient="0.1" transform="0.9063077569007874,0,-0.4226183295249939,0,0.3830223083496094,0.42261824011802673,0.8213937878608704,0,0.17860621213912964,-0.9063078165054321,0.3830221891403198,0,0,0,0,1" ambientColor="16777215" diffuse="1" name="Directional Light"> <bones:ArrayCollection x:name="children" x:id="10"/> </ns3:DirectionalLightComponent> <x:Ref x:name="3" x:id="3"/> <cadetAway3D4:MeshComponent x:name="4" x:id="11" transform="1,0,0,0,0,1,0,0,0,0,1,0,-58.91594696044922,146.62379455566406,-32.241188049316406,1" name="Cube"> <ns1:ColorMaterialComponent x:name="materialComponent" x:id="12" gloss="50" specularStrength="1" color="489736" name="Green Material" depthCompareMode="less"/> <bones:ArrayCollection x:name="children" x:id="13"> <ns2:CubeGeometryComponent x:name="0" x:id="14" height="180.85" segmentsH="1" tile6="1" depth="180.85" segmentsW="1" segmentsD="1" name="a cube" width="180.85"/> </bones:ArrayCollection> <x:Ref x:name="geometryComponent" x:id="14"/> </cadetAway3D4:MeshComponent> <cadetAway3D4:MeshComponent x:name="5" x:id="15" transform="1,0,0,0,0,1,0,0,0,0,1,0,-140.8660888671875,340.4476318359375,-54.04615783691406,1" name="Sphere"> <ns1:ColorMaterialComponent x:name="materialComponent" x:id="16" gloss="50" specularStrength="1" color="12320768" name="Red Material" depthCompareMode="less"/> <bones:ArrayCollection x:name="children" x:id="17"> <ns2:SphereGeometryComponent x:name="0" x:id="18" segmentsH="12" segmentsW="16" radius="84.13898408877864" name="a sphere"/> </bones:ArrayCollection> <x:Ref x:name="geometryComponent" x:id="18"/> </cadetAway3D4:MeshComponent> <cadetAway3D4:MeshComponent x:name="6" x:id="19" transform="1,0,0,0,0,1,0,0,0,0,1,0,151.02638244628906,146.50900268554688,127.5979995727539,1" name="Sphere"> <x:Ref x:name="materialComponent" x:id="16"/> <bones:ArrayCollection x:name="children" x:id="20"> <ns2:SphereGeometryComponent x:name="0" x:id="21" segmentsH="12" segmentsW="16" radius="52.35" name="Sphere Geometry"/> </bones:ArrayCollection> <x:Ref x:name="geometryComponent" x:id="21"/> </cadetAway3D4:MeshComponent> <cadetAway3D4:MeshComponent x:name="7" x:id="22" transform="1,0,0,0,0,1,0,0,0,0,1,0,164.13088989257813,348.98486328125,102.31562805175781,1" name="Cube"> <x:Ref x:name="materialComponent" x:id="12"/> <bones:ArrayCollection x:name="children" x:id="23"> <ns2:CubeGeometryComponent x:name="0" x:id="24" height="76.87" segmentsH="1" tile6="1" depth="76.87" segmentsW="1" segmentsD="1" name="Cube Geometry" width="76.87064711138314"/> </bones:ArrayCollection> <x:Ref x:name="geometryComponent" x:id="24"/> </cadetAway3D4:MeshComponent> <cadetAway3D4:MeshComponent x:name="8" x:id="25" transform="1,0,0,0,0,1,0,0,0,0,1,0,-161.5980987548828,37.646751403808594,291.46575927734375,1" name="Cube"> <x:Ref x:name="materialComponent" x:id="12"/> <bones:ArrayCollection x:name="children" x:id="26"> <ns2:CubeGeometryComponent x:name="0" x:id="27" height="75.29" segmentsH="1" tile6="1" depth="75.29" segmentsW="1" segmentsD="1" name="Cube Geometry" width="75.29350445797198"/> </bones:ArrayCollection> <x:Ref x:name="geometryComponent" x:id="27"/> </cadetAway3D4:MeshComponent> <cadetAway3D4:MeshComponent x:name="9" x:id="28" transform="1,0,0,0,0,1,0,0,0,0,1,0,-10.505144119262695,181.75933837890625,69.79844665527344,1" name="Cube"> <x:Ref x:name="materialComponent" x:id="12"/> <bones:ArrayCollection x:name="children" x:id="29"> <ns2:CubeGeometryComponent x:name="0" x:id="30" height="66.29" segmentsH="1" tile6="1" depth="66.29" segmentsW="1" segmentsD="1" name="Cube Geometry" width="66.29740645095558"/> </bones:ArrayCollection> <x:Ref x:name="geometryComponent" x:id="30"/> </cadetAway3D4:MeshComponent> <cadetAway3D4:MeshComponent x:name="10" x:id="31" transform="1,0,0,0,0,1,0,0,0,0,1,0,-393.7727966308594,113.91818237304688,-41.75886917114258,1" name="Cube"> <x:Ref x:name="materialComponent" x:id="12"/> <bones:ArrayCollection x:name="children" x:id="32"> <ns2:CubeGeometryComponent x:name="0" x:id="33" height="84.85" segmentsH="1" tile6="1" depth="84.85" segmentsW="1" segmentsD="1" name="Cube Geometry" width="84.85060333420417"/> </bones:ArrayCollection> <x:Ref x:name="geometryComponent" x:id="33"/> </cadetAway3D4:MeshComponent> <cadetAway3D4:MeshComponent x:name="11" x:id="34" transform="1,0,0,0,0,1,0,0,0,0,1,0,110.92161560058594,25.127405166625977,230.1426239013672,1" name="Cube"> <x:Ref x:name="materialComponent" x:id="12"/> <bones:ArrayCollection x:name="children" x:id="35"> <ns2:CubeGeometryComponent x:name="0" x:id="36" height="50.25" segmentsH="1" tile6="1" depth="50.25" segmentsW="1" segmentsD="1" name="Cube Geometry" width="50.25481013821924"/> </bones:ArrayCollection> <x:Ref x:name="geometryComponent" x:id="36"/> </cadetAway3D4:MeshComponent> <cadetAway3D4:MeshComponent x:name="12" x:id="37" transform="1,0,0,0,0,1,0,0,0,0,1,0,156.6885223388672,99.52238464355469,51.9654655456543,1" name="Cube"> <x:Ref x:name="materialComponent" x:id="12"/> <bones:ArrayCollection x:name="children" x:id="38"> <ns2:CubeGeometryComponent x:name="0" x:id="39" height="49.76" segmentsH="1" tile6="1" depth="49.76" segmentsW="1" segmentsD="1" name="Cube Geometry" width="49.76054139091827"/> </bones:ArrayCollection> <x:Ref x:name="geometryComponent" x:id="39"/> </cadetAway3D4:MeshComponent> <cadetAway3D4:MeshComponent x:name="13" x:id="40" transform="1,0,0,0,0,1,0,0,0,0,1,0,-241.41665649414063,106.39049530029297,-15.305731773376465,1" name="Sphere"> <x:Ref x:name="materialComponent" x:id="16"/> <bones:ArrayCollection x:name="children" x:id="41"> <ns2:SphereGeometryComponent x:name="0" x:id="42" segmentsH="12" segmentsW="16" radius="81.08778158664182" name="Sphere Geometry"/> </bones:ArrayCollection> <x:Ref x:name="geometryComponent" x:id="42"/> </cadetAway3D4:MeshComponent> <cadetAway3D4:MeshComponent x:name="14" x:id="43" transform="1,0,0,0,0,1,0,0,0,0,1,0,240.3887176513672,66.32697296142578,28.3767147064209,1" name="Sphere"> <x:Ref x:name="materialComponent" x:id="16"/> <bones:ArrayCollection x:name="children" x:id="44"> <ns2:SphereGeometryComponent x:name="0" x:id="45" segmentsH="12" segmentsW="16" radius="42.73389288130251" name="Sphere Geometry"/> </bones:ArrayCollection> <x:Ref x:name="geometryComponent" x:id="45"/> </cadetAway3D4:MeshComponent> <cadetAway3D4:MeshComponent x:name="15" x:id="46" transform="1,0,0,0,0,1,0,0,0,0,1,0,77.10282897949219,82.25804901123047,-173.88815307617188,1" name="Sphere"> <x:Ref x:name="materialComponent" x:id="16"/> <bones:ArrayCollection x:name="children" x:id="47"> <ns2:SphereGeometryComponent x:name="0" x:id="48" segmentsH="12" segmentsW="16" radius="44.53863475379832" name="Sphere Geometry"/> </bones:ArrayCollection> <x:Ref x:name="geometryComponent" x:id="48"/> </cadetAway3D4:MeshComponent> <cadetAway3D4:MeshComponent x:name="16" x:id="49" transform="1,0,0,0,0,1,0,0,0,0,1,0,-8.887575149536133,77.42305755615234,325.1551513671875,1" name="Sphere"> <x:Ref x:name="materialComponent" x:id="16"/> <bones:ArrayCollection x:name="children" x:id="50"> <ns2:SphereGeometryComponent x:name="0" x:id="51" segmentsH="12" segmentsW="16" radius="26.87288819730761" name="Sphere Geometry"/> </bones:ArrayCollection> <x:Ref x:name="geometryComponent" x:id="51"/> </cadetAway3D4:MeshComponent> <cadetAway3D4:MeshComponent x:name="17" x:id="52" transform="1,0,0,0,0,1,0,0,0,0,1,0,-111.85294342041016,150.3166961669922,314.55419921875,1" name="Sphere"> <x:Ref x:name="materialComponent" x:id="16"/> <bones:ArrayCollection x:name="children" x:id="53"> <ns2:SphereGeometryComponent x:name="0" x:id="54" segmentsH="12" segmentsW="16" radius="36.59617847826625" name="Sphere Geometry"/> </bones:ArrayCollection> <x:Ref x:name="geometryComponent" x:id="54"/> </cadetAway3D4:MeshComponent> <cadetAway3D4:MeshComponent x:name="18" x:id="55" transform="1,0,0,0,0,1,0,0,0,0,1,0,194.72483825683594,66.20823669433594,249.91229248046875,1" name="Sphere"> <x:Ref x:name="materialComponent" x:id="16"/> <bones:ArrayCollection x:name="children" x:id="56"> <ns2:SphereGeometryComponent x:name="0" x:id="57" segmentsH="12" segmentsW="16" radius="34.06102262376309" name="Sphere Geometry"/> </bones:ArrayCollection> <x:Ref x:name="geometryComponent" x:id="57"/> </cadetAway3D4:MeshComponent> <cadetAway3D4:MeshComponent x:name="19" x:id="58" transform="1,0,0,0,0,1,0,0,0,0,1,0,153.58782958984375,59.29662322998047,139.8775634765625,1" name="Sphere"> <x:Ref x:name="materialComponent" x:id="16"/> <bones:ArrayCollection x:name="children" x:id="59"> <ns2:SphereGeometryComponent x:name="0" x:id="60" segmentsH="12" segmentsW="16" radius="27.704758141615848" name="Sphere Geometry"/> </bones:ArrayCollection> <x:Ref x:name="geometryComponent" x:id="60"/> </cadetAway3D4:MeshComponent> <x:Ref x:name="20" x:id="16"/> <x:Ref x:name="21" x:id="6"/> <x:Ref x:name="22" x:id="12"/> </bones:ArrayCollection> <Object x:name="userData" x:id="61"/> <bones:DependencyManager x:name="dependencyManager" x:id="62"> <bones:ArrayCollection x:name="dependencyNodes" x:id="63"/> </bones:DependencyManager> </cadet:CadetScene> |
This format isn’t easy to handle! Cadet should really export to XML or JSON format.
Anyway this what I’ve done : demo. You can remove the AwayPhysics debug view via the console :
set awayPhysics visible false
As usual all the source code is available on the GitHub.
This is the GameState :
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 | package awayphysics.cadet3d { import away3d.controllers.HoverController; import away3d.debug.AwayStats; import com.citrusengine.core.State; import com.citrusengine.physics.awayphysics.AwayPhysics; import com.citrusengine.utils.ObjectMaker3D; import com.citrusengine.view.CitrusView; import com.citrusengine.view.away3dview.Away3DView; import flash.events.Event; import flash.events.MouseEvent; import flash.geom.Vector3D; /** * @author Aymeric */ public class Cadet3DGameState extends State { [Embed(source="/../embed/3D/simpletest.away3d4", mimeType="application/octet-stream")] private const _CADET_LEVEL:Class; // navigation variables private var _cameraController:HoverController; private var _move:Boolean = false; private var _lastPanAngle:Number; private var _lastTiltAngle:Number; private var _lastMouseX:Number; private var _lastMouseY:Number; private var _lookAtPosition:Vector3D = new Vector3D(); public function Cadet3DGameState() { super(); } override public function initialize():void { super.initialize(); addChild(new AwayStats((view as Away3DView).viewRoot)); var awayPhysics:AwayPhysics = new AwayPhysics("awayPhysics"); awayPhysics.visible = true; add(awayPhysics); ObjectMaker3D.FromCadetEditor3D(XML(new _CADET_LEVEL())); _cameraController = new HoverController((view as Away3DView).viewRoot.camera, null, 175, 20, 1000); stage.addEventListener(MouseEvent.MOUSE_DOWN, _onMouseDown); stage.addEventListener(MouseEvent.MOUSE_UP, _onMouseUp); stage.addEventListener(Event.MOUSE_LEAVE, _onMouseUp); stage.addEventListener(MouseEvent.MOUSE_WHEEL, _onMouseWheel); } // Make sure and call this override to specify Away3D view. override protected function createView():CitrusView { return new Away3DView(this); } override public function destroy():void { stage.removeEventListener(MouseEvent.MOUSE_DOWN, _onMouseDown); stage.removeEventListener(MouseEvent.MOUSE_UP, _onMouseUp); stage.removeEventListener(Event.MOUSE_LEAVE, _onMouseUp); stage.removeEventListener(MouseEvent.MOUSE_WHEEL, _onMouseWheel); super.destroy(); } override public function update(timeDelta:Number):void { super.update(timeDelta); if (_move) { _cameraController.panAngle = 0.3 * (stage.mouseX - _lastMouseX) + _lastPanAngle; _cameraController.tiltAngle = 0.3 * (stage.mouseY - _lastMouseY) + _lastTiltAngle; } _cameraController.lookAtPosition = _lookAtPosition; } private function _onMouseDown(mEvt:MouseEvent):void { _lastPanAngle = _cameraController.panAngle; _lastTiltAngle = _cameraController.tiltAngle; _lastMouseX = stage.mouseX; _lastMouseY = stage.mouseY; _move = true; } private function _onMouseUp(evt:Event):void { _move = false; } private function _onMouseWheel(mEvt:MouseEvent):void { _cameraController.distance -= mEvt.delta * 5; if (_cameraController.distance < 100) _cameraController.distance = 100; else if (_cameraController.distance > 2000) _cameraController.distance = 2000; } } } |
Like Flash as Level Editor or Tiled Map Editor, we use the ObjectMaker class. This is the template for Cadet Editor 3D :
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 | public static function FromCadetEditor3D(levelData:XML, addToCurrentState:Boolean = true):Array { var ce:CitrusEngine = CitrusEngine.getInstance(); var params:Object; var objects:Array = []; var type:String; var radius:Number; var object:AwayPhysicsObject; for each (var root:XML in levelData.children()) { for each (var parent:XML in root.children()) { type = parent.@name; if (type == "Cube" || type == "Plane" || type == "Sphere") { var transform:Array = parent.@transform.split(","); params = {}; params.x = transform[12]; params.y = transform[13]; params.z = transform[14]; for each (var child:XML in parent.children()) { for each (var finalElement:XML in child.children()) { params.width = finalElement.@width; params.height = finalElement.@height; params.depth = finalElement.@depth; radius = finalElement.@radius; if (radius) params.radius = finalElement.@radius; if (type == "Plane") { // the plane seems to use the height as the depth params.depth = params.height; params.height = 0; params.view = new Mesh(new CubeGeometry(params.width, params.height, params.depth), new ColorMaterial(0xFF0000)); object = new Platform("plane", params); } else { if (params.radius) { params.view = new Mesh(new SphereGeometry(params.radius), new ColorMaterial(0x00FF00)); object = new AwayPhysicsObject("sphere", params); } else { params.view = new Mesh(new CubeGeometry(params.width, params.height, params.depth), new ColorMaterial(0x0000FF)); object = new AwayPhysicsObject("cube", params); } } objects.push(object); } } } } } if (addToCurrentState) for each (object in objects) ce.state.add(object); return objects; } |
As you can notice, the object maker uses CE prebuilt objects for AwayPhysics but not in a dynamic way.
This is some points that Cadet Editor should allow to be an excellent level editor :
- quickly most of the points are in Tiled Map Editor, checkout Citrus Engine support there.
- an easy to parse exporter format (xml or json).
- adding personal properties so we can precize type, gravity, view for example directly in the level editor. A must have!
I will continue my experiments and improve Cadet 3D support. I really hope that some of my requests will be added to Cadet Editor and then it will be an awesome 3D level editor for the CE!
Create combination of physics objects in Citrus Engine
0Some days ago, I said that November will be an awesome month for the Citrus Engine. So what is coming?
We’re the 2nd, November is already here and something huge is already happened to the CE : a tutorial on gotoandlearn made by Lee Brimelow, an Adobe Game Developer Evangelist! Definitely, an excellent tutorial to get start with the Citrus Engine. There’ll be other tutorials later, a new website and the forum’ll move on Starling website. Most of the future game will use Starling so that’s a good move! We’re not forgetting the 3D part, don’t worry
Everything should be ready for the next Adobe Game Jam in Chicago. So that’s the plan. Now let’s go for a small tutorial.
Someone asked me what is the best way to create a combination of physics objects in CE, like this one. You have to create the Box2D physics bodies and associate arts. Firstly, don’t forget that view can lies, but never physics. For example, on the CE’s demo on the actual website the mill’s arms are one Box2D object (with two bodies) and a single picture.
Example :

The Box2D Alchemy code :
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 | package { import Box2DAS.Collision.Shapes.b2PolygonShape; import Box2DAS.Common.V2; import Box2DAS.Dynamics.Joints.b2RevoluteJoint; import Box2DAS.Dynamics.Joints.b2RevoluteJointDef; import Box2DAS.Dynamics.b2Body; import Box2DAS.Dynamics.b2BodyDef; import Box2DAS.Dynamics.b2Fixture; import Box2DAS.Dynamics.b2FixtureDef; import com.citrusengine.objects.PhysicsObject; public class WindmillArms extends PhysicsObject { private var _hingeBodyDef:b2BodyDef; private var _hingeBody:b2Body; private var _shape2:b2PolygonShape; private var _fixtureDef2:b2FixtureDef; private var _fixture2:b2Fixture; private var _jointDef:b2RevoluteJointDef; private var _joint:b2RevoluteJoint; public function WindmillArms(name:String, params:Object=null) { super(name, params); } override public function destroy():void { _hingeBody.destroy(); _hingeBodyDef.destroy(); _fixtureDef2.destroy(); super.destroy(); } override public function set x(value:Number):void { super.x = value; if (_hingeBody) { var pos:V2 = _hingeBody.GetPosition(); pos.x = _x; _hingeBody.SetTransform(pos, _hingeBody.GetAngle()); } } override public function set y(value:Number):void { super.y = value; if (_hingeBody) { var pos:V2 = _hingeBody.GetPosition(); pos.y = _y; _hingeBody.SetTransform(pos, _hingeBody.GetAngle()); } } override protected function defineBody():void { super.defineBody(); _hingeBodyDef = new b2BodyDef(); _hingeBodyDef.type = b2Body.b2_staticBody; _hingeBodyDef.position.v2 = new V2(_x, _y); } override protected function createBody():void { super.createBody(); _hingeBody = _box2D.world.CreateBody(_hingeBodyDef); _hingeBody.SetUserData(this); } override protected function createShape():void { super.createShape(); _shape2 = new b2PolygonShape(); _shape2.SetAsBox(_width / 2, _height / 2, null, _rotation + (Math.PI / 2)); } override protected function defineFixture():void { super.defineFixture(); _fixtureDef2 = new b2FixtureDef(); _fixtureDef2.shape = _shape2; _fixtureDef2.density = _fixtureDef.density; _fixtureDef2.friction = _fixtureDef.friction; _fixtureDef2.restitution = _fixtureDef.restitution; } override protected function createFixture():void { super.createFixture(); _fixture2 = _body.CreateFixture(_fixtureDef2); } override protected function defineJoint():void { super.defineJoint(); _jointDef = new b2RevoluteJointDef(); _jointDef.bodyA = _hingeBody; _jointDef.bodyB = _body; _jointDef.enableMotor = true; _jointDef.maxMotorTorque = 6 _jointDef.motorSpeed = 0; _jointDef.localAnchorA.v2 = new V2(); _jointDef.localAnchorB.v2 = new V2(); } override protected function createJoint():void { super.createJoint(); _joint = _box2D.world.CreateJoint(_jointDef) as b2RevoluteJoint; _joint.SetUserData(this); } } } |
Easier than it sounds, right? The key is to override defined methods and add your new boddie.
Let’s go back to the first example on Emanuele’s website.
Click here for the web demo.
As usal, all the source code is available on the CE’s GitHub dedicated to examples.
Actually, you see the Box2D debug view, to see arts use the console :
Press tab key, write :
set box2D visible false
And press enter.
There are two ways to achieve it, one is not better than the other, it’s just different. You will certainly prefer one depending what you’re trying to achieve.
Create lots of CE’s object
This is the game state :
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 | package complexbox2dobject{ import com.citrusengine.core.State; import com.citrusengine.objects.platformer.box2d.Platform; import com.citrusengine.physics.box2d.Box2D; /** * @author Aymeric */ public class ComplexBox2DObjectGameState extends State { public function ComplexBox2DObjectGameState() { super(); } override public function initialize():void { super.initialize(); var box2D:Box2D = new Box2D("box2D"); box2D.visible = true; add(box2D); var platTop:Platform = new Platform("platTop", {x:stage.stageWidth / 2, width:stage.stageWidth, y:20}); add(platTop); var chain:Chain; var vecChain:Vector.<Chain> = new Vector.<Chain>(); for (var i:uint = 0; i < 4; ++i) { if (i == 0) chain = new Chain("chain" + i, {x:stage.stageWidth / 2, y:70 + i * 40, width:15, hangTo:platTop, distance:i, view:new ChainGraphic()}); else chain = new Chain("chain" + i, {x:stage.stageWidth / 2, y:70 + i * 40, width:15, hangTo:vecChain[i - 1], distance:i, view:new ChainGraphic()}); vecChain.push(chain); add(chain); } chain = new Chain("chain" + i, {x:2, y:70 + (i + 1) * 40, radius:15, hangTo:vecChain[i - 1], distance:i + 1, last:true, registration:"topLeft", view:new ChainGraphic(15)}); add(chain); vecChain.push(chain); } } } |
Here we create many CE objects, with lots of similar informations. The good thing is all the arts are managed by SpriteView and SpriteArt classes.
The Box2D Chain 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 | package complexbox2dobject{ import Box2D.Common.Math.b2Vec2; import Box2D.Dynamics.Joints.b2RevoluteJointDef; import com.citrusengine.objects.Box2DPhysicsObject; /** * @author Aymeric */ public class Chain extends Box2DPhysicsObject { public var hangTo:Box2DPhysicsObject; public var distance:uint; public var last:Boolean = false; private var _revoluteJointDef:b2RevoluteJointDef; public function Chain(name:String, params:Object) { super(name, params); } override protected function defineJoint():void { _revoluteJointDef = new b2RevoluteJointDef(); if (last) { _revoluteJointDef.localAnchorA.Set(0, (distance + 75) / _box2D.scale); _revoluteJointDef.localAnchorB.Set(0, 0); _revoluteJointDef.bodyA = hangTo.body; _revoluteJointDef.bodyB = _body; } else _revoluteJointDef.Initialize(hangTo.body, _body, new b2Vec2(275 / _box2D.scale, (70 + distance * 40) / _box2D.scale)); } override protected function createJoint():void { _box2D.world.CreateJoint(_revoluteJointDef); } } } |
Nothing too complex, we just create a joint in the right method.
And finally the ChainGraphic class :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | package complexbox2dobject{ import flash.display.Sprite; /** * @author Aymeric */ public class ChainGraphic extends Sprite { public function ChainGraphic(radius:uint = 0) { this.graphics.beginFill(Math.random() * 0xFFFFFF); if (radius != 0) this.graphics.drawCircle(0, 0, radius); else this.graphics.drawRect(0, 0, 15, 30); this.graphics.endFill(); } } } |
That’s it for the first method, the most obvious.
Only one complex Box2D and art classes
For the second method, like the mill’s arms we create several Box2D bodies in one Box2DPhysicsObject class.
The GameState :
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 | package complexbox2dobject{ import com.citrusengine.core.State; import com.citrusengine.objects.platformer.box2d.Platform; import com.citrusengine.physics.box2d.Box2D; /** * @author Aymeric */ public class ComplexBox2DObjectGameState extends State { public function ComplexBox2DObjectGameState() { super(); } override public function initialize():void { super.initialize(); var box2D:Box2D = new Box2D("box2D"); box2D.visible = true; add(box2D); var platTop:Platform = new Platform("platTop", {x:stage.stageWidth / 2, width:stage.stageWidth, y:20}); add(platTop); var ropeChain:RopeChain = new RopeChain("ropeChain", {hangTo:platTop, radius:15, y:150, x:30, view:new RopeChainGraphics(), registration:"topLeft"}); add(ropeChain); } } } |
The object created there is basically a circle hang to the platform top with several chains.
The RopeChain 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 | package complexbox2dobject{ import Box2D.Collision.Shapes.b2PolygonShape; import Box2D.Collision.Shapes.b2Shape; import Box2D.Common.Math.b2Vec2; import Box2D.Dynamics.Joints.b2RevoluteJointDef; import Box2D.Dynamics.b2Body; import Box2D.Dynamics.b2BodyDef; import Box2D.Dynamics.b2FixtureDef; import com.citrusengine.objects.Box2DPhysicsObject; import com.citrusengine.physics.PhysicsCollisionCategories; import flash.geom.Point; /** * @author Aymeric */ public class RopeChain extends Box2DPhysicsObject { public var hangTo:Box2DPhysicsObject; public var numChain:uint = 5; public var widthChain:uint = 15; public var heightChain:uint = 30; public var attach:Point = new Point(275, 60); public var distance:uint = 32; private var _vecBodyDefChain:Vector.<b2BodyDef>; private var _vecBodyChain:Vector.<b2Body>; private var _vecFixtureDefChain:Vector.<b2FixtureDef>; private var _vecRevoluteJointDef:Vector.<b2RevoluteJointDef>; private var _shapeChain:b2Shape; public function RopeChain(name:String, params:Object = null) { super(name, params); } override public function initialize(poolObjectParams:Object = null):void { super.initialize(poolObjectParams); if (view) (view as RopeChainGraphics).init(numChain, widthChain, heightChain); } override public function update(timeDelta:Number):void { super.update(timeDelta); if (view) (view as RopeChainGraphics).update(_vecBodyChain, _box2D.scale); } override protected function defineBody():void { super.defineBody(); _vecBodyDefChain = new Vector.<b2BodyDef>(); var bodyDefChain:b2BodyDef; for (var i:uint = 0; i < numChain; ++i) { bodyDefChain = new b2BodyDef(); bodyDefChain = new b2BodyDef(); bodyDefChain.type = b2Body.b2_dynamicBody; bodyDefChain.position = new b2Vec2(attach.x / _box2D.scale, (attach.y + i * distance) / _box2D.scale); bodyDefChain.angle = _rotation; _vecBodyDefChain.push(bodyDefChain); } } override protected function createBody():void { super.createBody(); _vecBodyChain = new Vector.<b2Body>(); var bodyChain:b2Body; for each (var bodyDefChain:b2BodyDef in _vecBodyDefChain) { bodyChain = _box2D.world.CreateBody(bodyDefChain); bodyChain.SetUserData(this); _vecBodyChain.push(bodyChain); } } override protected function createShape():void { super.createShape(); _shapeChain = new b2PolygonShape(); b2PolygonShape(_shapeChain).SetAsBox(widthChain / 2 / _box2D.scale, heightChain / 2 / _box2D.scale); } override protected function defineFixture():void { super.defineFixture(); _vecFixtureDefChain = new Vector.<b2FixtureDef>(); var fixtureDefChain:b2FixtureDef; for (var i:uint = 0; i < numChain; ++i) { fixtureDefChain = new b2FixtureDef(); fixtureDefChain.shape = _shapeChain; fixtureDefChain.density = 1; fixtureDefChain.friction = 0.6; fixtureDefChain.restitution = 0.3; fixtureDefChain.filter.categoryBits = PhysicsCollisionCategories.Get("Level"); fixtureDefChain.filter.maskBits = PhysicsCollisionCategories.GetAll(); _vecFixtureDefChain.push(fixtureDefChain); } } override protected function createFixture():void { super.createFixture(); var i:uint = 0; for each (var fixtureDefChain:b2FixtureDef in _vecFixtureDefChain) { _vecBodyChain[i].CreateFixture(fixtureDefChain); ++i; } } override protected function defineJoint():void { _vecRevoluteJointDef = new Vector.<b2RevoluteJointDef>(); var revoluteJointDef:b2RevoluteJointDef; for (var i:uint = 0; i < numChain; ++i) { revoluteJointDef = new b2RevoluteJointDef(); if (i == 0) revoluteJointDef.Initialize(hangTo.body, _vecBodyChain[i], new b2Vec2(attach.x / _box2D.scale, (attach.y + i * distance) / _box2D.scale)); else revoluteJointDef.Initialize(_vecBodyChain[i - 1], _vecBodyChain[i], new b2Vec2(attach.x / _box2D.scale, (attach.y + i * distance) / _box2D.scale)); _vecRevoluteJointDef.push(revoluteJointDef); } revoluteJointDef = new b2RevoluteJointDef(); revoluteJointDef.localAnchorA.Set(0, distance / _box2D.scale); revoluteJointDef.localAnchorB.Set(0, 0); revoluteJointDef.bodyA = _vecBodyChain[numChain - 1]; revoluteJointDef.bodyB = _body; _vecRevoluteJointDef.push(revoluteJointDef); } override protected function createJoint():void { for each (var revoluteJointDef:b2RevoluteJointDef in _vecRevoluteJointDef) { _box2D.world.CreateJoint(revoluteJointDef); } } } } |
We create Vectors with all our new chains’ bodies.
And finally the most complex part on this second way, the graphic 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 | package complexbox2dobject{ import Box2D.Dynamics.b2Body; import com.citrusengine.math.MathUtils; import flash.display.Sprite; import flash.geom.Point; /** * @author Aymeric */ public class RopeChainGraphics extends Sprite { private var _numChain:uint; private var _vecSprites:Vector.<Sprite>; private var _width:uint; private var _height:uint; public function RopeChainGraphics() { this.graphics.beginFill(0xFF0000); this.graphics.drawCircle(0, 0, 15); this.graphics.endFill(); } public function init(numChain:uint, width:uint, height:uint):void { _numChain = numChain; _width = width; _height = height; _vecSprites = new Vector.<Sprite>(); var sprite:Sprite; for (var i:uint = 0; i < _numChain; ++i) { sprite = new Sprite(); sprite.graphics.beginFill(Math.random() * 0xFFFFFF); sprite.graphics.drawRect(0, 0, _width, _height); sprite.graphics.endFill(); addChild(sprite); _vecSprites.push(sprite); } } public function update(vecBodyChain:Vector.<b2Body>, box2DScale:Number):void { var i:uint = 0; for each (var body:b2Body in vecBodyChain) { _vecSprites[i].x = body.GetPosition().x * box2DScale - this.parent.x - _width * 0.5; _vecSprites[i].y = body.GetPosition().y * box2DScale - this.parent.y - _height * 0.5; MathUtils.RotateAroundExternalPoint(_vecSprites[i], new Point(_width * 0.5 + _vecSprites[i].x, _height * 0.5 + _vecSprites[i].y), body.GetAngle() * 180 / Math.PI - _vecSprites[i].rotation - this.parent.rotation); ++i; } } } } |
Since chains’ graphics aren’t managed by SpriteView and the SpriteArt (only the circle), we have to update them manually! Note that there is a little bug on the rotation, I’m missing something here but can’t find the issue… Damn math knowledge
Anyway it shouldn’t prevent you to use this method!
Create a game like Tiny Wings
11Tiny Wings is really a cute game and very fun to play thanks to physics content! It uses Box2D. If you’ve never played the game, give it a quick look :
Generate those type of hills with a physics engine is complicated for a novice. You’ll find cool tutorials on Emanuele Feronato’s website using Box2D and an other on Lorenzo Nuvoletta’s website using Nape.
Someone asks me if it was possible to create this type of game with the Citrus Engine, absolutely! So let’s go for a quick tutorial.
Firstly, all hails to Lorenzo! The physics hills and the sprites creation come from his algorithm.
This is what you will get : demo.
To see the physics debug view, open the console with the key tab, then write :
set nape visible true
And press enter. You will see Nape’s debug view and the hero. You can jump with a click.
All the source code is accessible on the new CE’s GitHub for examples.
I won’t explain the algorithm. If you have any questions on it, ask Lorenzo. He will certainly answer better than me. However I’ll explain how it has been added to the Citrus Engine.
This the Main class :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | package games.tinywings { import com.citrusengine.core.StarlingCitrusEngine; /** * @author Aymeric */ public class Main extends StarlingCitrusEngine { public function Main() { setUpStarling(true); state = new TinyWingsGameState(); } } } |
The GameState. Here you init your physics world, and create the hero and the hills :
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 | package games.tinywings { import com.citrusengine.core.StarlingState; import com.citrusengine.math.MathVector; import com.citrusengine.physics.nape.Nape; import flash.geom.Rectangle; /** * @author Aymeric */ public class TinyWingsGameState extends StarlingState { private var _nape:Nape; private var _hero:BirdHero; private var _hillsTexture:HillsTexture; public function TinyWingsGameState() { super(); } override public function initialize():void { super.initialize(); _nape = new Nape("nape"); //_nape.visible = true; add(_nape); _hero = new BirdHero("hero", {radius:20}); add(_hero); _hillsTexture = new HillsTexture(); var hills:HillsManagingGraphics = new HillsManagingGraphics("hills", {sliceHeight:200, sliceWidth:70, currentYPoint:350, registration:"topLeft", view:_hillsTexture}); add(hills); view.setupCamera(_hero, new MathVector(stage.stageWidth /2, stage.stageHeight / 2), new Rectangle(0, 0, int.MAX_VALUE, int.MAX_VALUE), new MathVector(.25, .05)); } override public function update(timeDelta:Number):void { super.update(timeDelta); // update the hills here to remove the displacement made by StarlingArt. Called after all operations done. _hillsTexture.update(); } } } |
Now let’s focus on the Hills creation. Hills is a new Nape’s object added to the Core of the Citrus Engine. It extends NapePhysicsObject. Since the Citrus Engine separates logic/physics from view/art, we don’t want to manage graphics in this core class. So we remove all the content related to graphics :
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 | package com.citrusengine.objects.platformer.nape { import nape.geom.Vec2; import nape.phys.Body; import nape.phys.BodyType; import nape.shape.Polygon; import com.citrusengine.objects.NapePhysicsObject; /** * This class creates perpetual hills like the games Tiny Wings, Ski Safari... * Write a class to manage graphics, and extends this one to call graphics function. * For more information, check out CE's Tiny Wings example. * Thanks to <a href="http://www.lorenzonuvoletta.com/create-an-infinite-scrolling-world-with-starling-and-nape/">Lorenzo Nuvoletta</a>. */ public class Hills extends NapePhysicsObject { /** * This is the height of a slice. */ public var sliceHeight:uint = 600; /** * This is the width of a slice. */ public var sliceWidth:uint = 30; /** * This is the height of the first point. */ public var currentYPoint:Number = 200; /** * This is the width of the hills visible. Most of the time your stage width. */ public var widthHills:Number = 550; /** * This is the physics object from which the Hills read its position and create/delete hills. */ public var rider:NapePhysicsObject; protected var _slicesCreated:uint; protected var _currentAmplitude:Number; protected var _nextYPoint:Number; protected var _slicesInCurrentHill:uint; protected var _indexSliceInCurrentHill:uint; protected var _slices:Vector.<Body>; protected var _sliceVectorConstructor:Vector.<Vec2>; public function Hills(name:String, params:Object = null) { super(name, params); } override public function initialize(poolObjectParams:Object = null):void { super.initialize(poolObjectParams); _prepareSlices(); } protected function _prepareSlices():void { _slices = new Vector.<Body>(); // Generate a rectangle made of Vec2 _sliceVectorConstructor = new Vector.<Vec2>(); _sliceVectorConstructor.push(new Vec2(0, sliceHeight)); _sliceVectorConstructor.push(new Vec2(0, 0)); _sliceVectorConstructor.push(new Vec2(sliceWidth, 0)); _sliceVectorConstructor.push(new Vec2(sliceWidth, sliceHeight)); // fill the stage with slices of hills for (var i:uint = 0; i < widthHills / sliceWidth * 1.2; ++i) { _createSlice(); } } protected function _createSlice():void { // Every time a new hill has to be created this algorithm predicts where the slices will be positioned if (_indexSliceInCurrentHill >= _slicesInCurrentHill) { _slicesInCurrentHill = Math.random() * 40 + 10; _currentAmplitude = Math.random() * 60 - 20; _indexSliceInCurrentHill = 0; } // Calculate the position of the next slice _nextYPoint = currentYPoint + (Math.sin(((Math.PI / _slicesInCurrentHill) * _indexSliceInCurrentHill)) * _currentAmplitude); _sliceVectorConstructor[2].y = _nextYPoint - currentYPoint; var slicePolygon:Polygon = new Polygon(_sliceVectorConstructor); _body = new Body(BodyType.STATIC); _body.shapes.add(slicePolygon); _body.position.x = _slicesCreated * sliceWidth; _body.position.y = currentYPoint; _body.space = _nape.space; _pushHill(); } protected function _pushHill():void { _slicesCreated++; _indexSliceInCurrentHill++; currentYPoint = _nextYPoint; _slices.push(_body); } protected function _checkHills():void { if (!rider) rider = _ce.state.getFirstObjectByType(Hero) as Hero; var length:uint = _slices.length; for (var i:uint = 0; i < length; ++i) { if (rider.body.position.x - _slices[i].position.x > widthHills * 0.5 + 100) { _deleteHill(i); --i; _createSlice(); } else break; } } protected function _deleteHill(index:uint):void { _nape.space.bodies.remove(_slices[index]); _slices.splice(index, 1); } override public function update(timeDelta:Number):void { super.update(timeDelta); _checkHills(); } /** * Bodies are generated automatically, those functions aren't needed. */ override protected function defineBody():void { } override protected function createBody():void { } override protected function createMaterial():void { } override protected function createShape():void { } override protected function createFilter():void { } override protected function createConstraint():void { } } } |
We’ve cutted the algorithm in several methods to be able to manage graphics from a subclass.
This is the class which manage graphics :
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 | package games.tinywings { import com.citrusengine.objects.platformer.nape.Hills; /** * @author Aymeric */ public class HillsManagingGraphics extends Hills { public function HillsManagingGraphics(name:String, params:Object = null) { super(name, params); } override protected function _prepareSlices():void { if (view) (view as HillsTexture).init(sliceWidth, sliceHeight); super._prepareSlices(); } override protected function _pushHill():void { if (view) (view as HillsTexture).createSlice(_body, _nextYPoint, currentYPoint); super._pushHill(); } override protected function _deleteHill(index:uint):void { (view as HillsTexture).deleteHill(index); super._deleteHill(index); } } } |
We just make some calls to view’s functions. And so finally, the view 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 | package games.tinywings { import nape.phys.Body; import starling.display.Image; import starling.display.Sprite; import starling.events.Event; import starling.textures.Texture; import flash.display.BitmapData; import flash.geom.Matrix; /** * @author Aymeric */ public class HillsTexture extends Sprite { private var _groundTexture:Texture; private var _sliceWidth:uint; private var _sliceHeight:uint; private var _images:Vector.<Image>; private var _flagAdded:Boolean = false; public function HillsTexture() { } public function init(sliceWidth:uint, sliceHeight:uint):void { _sliceWidth = sliceWidth; _sliceHeight = sliceHeight; _groundTexture = Texture.fromBitmapData(new BitmapData(_sliceWidth, _sliceHeight, false, 0xffaa33)); _images = new Vector.<Image>(); addEventListener(Event.ADDED, _added); } private function _added(evt:Event):void { _flagAdded = true; removeEventListener(Event.ADDED_TO_STAGE, _added); } public function update():void { // we don't want to move the parent like StarlingArt does! if (_flagAdded) this.parent.x = this.parent.y = 0; } public function createSlice(rider:Body, nextYPoint:uint, currentYPoint:uint):void { var image:Image = new Image(_groundTexture); addChild(image); _images.push(image); var matrix:Matrix = image.transformationMatrix; matrix.translate(rider.position.x, rider.position.y); matrix.a = 1.04; matrix.b = (nextYPoint - currentYPoint) / _sliceWidth; image.transformationMatrix.copyFrom(matrix); } public function deleteHill(index:uint):void { removeChild(_images[index], true); _images.slice(index, 1); } } } |
And that’s it! Our scrollable world with hills is working!
To have exactly the same feeling that Tiny Wings there is always lots of work to achieve :
- make the terrain more rolling.
- don’t apply a linear velocity to your hero, just a basic force and then change its mass between screen touched or not.
- create nice graphics using procedural palettes algorithms.
When I’ve some time, I will work on the Box2D version thanks to Emanuele’s tutorial and polish this version.
Moved from Box2D Alchemy to Box2D AS3
2In the Citrus Engine, people knows that I’m very attached to the Box2D Alchemy version. Thanks to Alchemy we should have better performance than the pure AS3 version. Also this Alchemy version had some cool features mostly on the way you could manage collision : you don’t use a gobal contact listener like in any Box2D version, you use something very closer to AS3 event management. Example :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | override protected function createFixture():void { super.createFixture(); _fixture.m_reportBeginContact = true; _fixture.addEventListener(ContactEvent.BEGIN_CONTACT, handleBeginContact); } protected function handleBeginContact(e:ContactEvent):void { _contact = e.other.GetBody().GetUserData() as Box2DPhysicsObject; if (!e.other.IsSensor()) explode(); } |
I love this way. Anyway, many users ask me to change it… let’s go!
Box2D AS3
This is the contact listener class :
package com.citrusengine.physics.box2d { import Box2D.Dynamics.b2ContactImpulse; import Box2D.Collision.b2Manifold; import Box2D.Dynamics.Contacts.b2Contact; import Box2D.Dynamics.b2ContactListener; /** * Used to report the contact's interaction between object. */ public class Box2DContactListener extends b2ContactListener { public function Box2DContactListener() { } override public function BeginContact(contact:b2Contact):void { contact.GetFixtureA().GetBody().GetUserData().handleBeginContact(contact); contact.GetFixtureB().GetBody().GetUserData().handleBeginContact(contact); } override public function EndContact(contact:b2Contact):void { contact.GetFixtureA().GetBody().GetUserData().handleEndContact(contact); contact.GetFixtureB().GetBody().GetUserData().handleEndContact(contact); } override public function PreSolve(contact:b2Contact, oldManifold:b2Manifold):void { contact.GetFixtureA().GetBody().GetUserData().handlePreSolve(contact, oldManifold); contact.GetFixtureB().GetBody().GetUserData().handlePreSolve(contact, oldManifold); } override public function PostSolve(contact:b2Contact, impulse:b2ContactImpulse):void { contact.GetFixtureA().GetBody().GetUserData().handlePostSolve(contact, impulse); contact.GetFixtureB().GetBody().GetUserData().handlePostSolve(contact, impulse); } } } |
And then I override this function handleBeginContact, handleEndContact, handlePreSolve, handlePostSolve with what I need. And I use two very useful function to work the same way that I used to do with the Alchemy version :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | /** * In Box2D we are blind concerning the collision, we are never sure which body is the collider. This function should help. * Call this function to obtain the colliding physics object. * @param self : in CE's code, we give this. In your code it will be your hero, a sensor, ... * @param the contact * @return the collider */ static public function CollisionGetOther(self:Box2DPhysicsObject, contact:b2Contact):Box2DPhysicsObject { return self == contact.GetFixtureA().GetBody().GetUserData() ? contact.GetFixtureB().GetBody().GetUserData() : contact.GetFixtureA().GetBody().GetUserData(); } /** * In Box2D we are blind concerning the collision, we are never sure which body is the collider. This function should help. * Call this function to obtain the collided physics object. * @param self : in CE's code, we give this. In your code it will be your hero, a sensor, ... * @param the contact * @return the collided */ static public function CollisionGetSelf(self:Box2DPhysicsObject, contact:b2Contact):Box2DPhysicsObject { return self == contact.GetFixtureA().GetBody().GetUserData() ? contact.GetFixtureA().GetBody().GetUserData() : contact.GetFixtureB().GetBody().GetUserData(); } |
And finally an example how it is used :
1 2 3 4 5 6 7 8 9 10 11 12 13 | override public function handlePreSolve(contact:b2Contact, oldManifold:b2Manifold):void { if (!_ducking) return; var other:Box2DPhysicsObject = Box2DPhysicsObject.CollisionGetOther(this, contact); var heroTop:Number = y; var objectBottom:Number = other.y + (other.height / 2); if (objectBottom < heroTop) contact.SetEnabled(false); } |
Pretty cool, isn’t it? I know it will be a problem to upgrade on this last version, but it will not be as hard as it sounds since 99% of the code are similar. Just take a look on this new way to handle events.
Performance
When I made the transition, I told myself : “We shouldn’t see a big difference on a browser version, and if it sucks more than Alchemy on mobile we don’t care since there is Nape”! So I made a test with my quick demo : Live4Sales game. And this is the result on a iPad3 :
Compare now to the Box2D Alchemy & Nape versions : in this previous stress test. Holy s***! Box2D AS3 is very quicker than Alchemy version, really close to Nape in fact. So much close that we should make other tests with different mobiles and other type of games. Nape is always faster, but the difference is small. No more tax, better performances : definitely a good move!
All the demos have been updated on the GitHub.
Version 3?
We’ve never been so close to the final release, it should be in November. I would like to emphasize two points :
- the 3D part (handled with Away3D & AwayPhysics) is a sandbox for me. I never pretend to offer the best 3D game engine like you can have with Unity3D, how could I compare with them?, but just a good engine where you could create quiclky some 3d physics with a character, sensors… It could be used for simple games or for interactive websites. This 3D part will evolve along the Citrus Engine V3.
- the entity/component system is not bad, but not as useful as I would. Since some time, it is in stand by. It is not the main features wished by members, so I will refocus on it after. At the moment, use it at your own risk!
Citrus Engine’s structure updated, camera movement example
3This is a quick blog post to share a small tutorial on how you can handle a camera in the Citrus Engine and having feedbacks for the new CE’s structure.
Project dependencies
Let’s start for a very interessant & complex subject : refactor & separate an engine which has many libraries dependencies. Now that the Citrus Engine handles 3D and 3D physics thanks to Away3D & AwayPhysics it becomes too heavy. There is no problem with performances but a simple hello world is far too heavy. At the moment, any project needs to have Starling, Box2D, Nape, Away3D & AwayPhysics included. Even if you just need, for example, Starling & Box2D. We have to find a solution. We can’t just remove libraries that we don’t need manually and follow compiler instructions to remove them from the engine too. We have to find the best solution for final users & contributors.
It would be great to have swc as well for each dependency, e.g. ce.swc, ce_starling.swc & ce_starling_box2d.swc or ce.swc, cd_away3d.swc & ce_away3d_nape.swc. It means lots of swcs but it would be really easy to pick up those needed.
That was the point. And I’m glad to say that we find the best solution for everyone! And it’s up on GitHub! Also I would like to thanks Johann Barbie who helped me a lot on this. Now everything in the engine is separated, it means simply that in the view package we never care about which physics engine we use. Concerning the physics debug view, we have a class for each physics engine and then we use some wrapper to handle them in Starling or Away3D.
What changed?
I try to do my best to not break backward compatibility. If you use a modern IDE (if not, you should!!), just update the package import. It should works fine.
If you use Starling, you Main class must extends StarlingCitrusEngine. And that’s it!
All examples have been updated, and the final SWF size is really smaller!
Ant
Now I’m working with Ant to provide a swc for each build which only contain CE’s code. My main problem is I don’t success to remove external libraries on the final build. I would like to include them to compile my swc, but not include them in the final swc. Any advice would be very much appreciated!
Box2D AS3
Some people request me to move on Box2D AS3 version instead of the Alchemy version. It is a good idea if you don’t want to depend of the new Adobe tax. However this AS3 version is harder to handle since there isn’t a simple event system to manage collision like the Alchemy one. If you are familiar with the AS3 version and would like to provide your help, please contact me.
Camera demo
The demo.
Use the mouse wheel to zoom/dezoom. Press R to rotate the content.
The source code is very simple :
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 | package cameramovement { import com.citrusengine.core.CitrusEngine; import com.citrusengine.core.State; import com.citrusengine.math.MathUtils; import com.citrusengine.math.MathVector; import com.citrusengine.objects.platformer.box2d.Hero; import com.citrusengine.objects.platformer.box2d.Platform; import com.citrusengine.physics.box2d.Box2D; import flash.events.MouseEvent; import flash.geom.Point; import flash.geom.Rectangle; import flash.ui.Keyboard; /** * @author Aymeric */ public class CameraMovement extends State { public function CameraMovement() { super(); } override public function initialize():void { super.initialize(); var box2d:Box2D = new Box2D("box2D", {visible:true}); box2d.group = 0; // box2d debug view will be behind graphics, default is 1 : can't work on Starling, since the debug view is on the display list add(box2d); var hero:Hero = new Hero("hero", {x:100, view:"PatchSpriteArt.swf", width:60, height:120, offsetY:15}); add(hero); add(new Platform("platBot", {x:stage.stageWidth / 2, y:stage.stageHeight, width:3000})); add(new Platform("cloud", {x:450, y:250, width:200, oneWay:true})); view.setupCamera(hero, new MathVector(stage.stageWidth / 2, stage.stageHeight / 2), new Rectangle(0, 0, 1550, 450), new MathVector(.25, .05)); stage.addEventListener(MouseEvent.MOUSE_WHEEL, _mouseWheel); } override public function destroy():void { stage.removeEventListener(MouseEvent.MOUSE_DOWN, _mouseWheel); super.destroy(); } override public function update(timeDelta:Number):void { super.update(timeDelta); if (CitrusEngine.getInstance().input.isDown(Keyboard.R)) { // if you use Starling, just move the pivot point! MathUtils.RotateAroundInternalPoint(this, new Point(stage.stageWidth / 2, stage.stageHeight / 2), 1); view.cameraOffset = new MathVector(stage.stageWidth / 2, stage.stageHeight / 2); view.cameraBounds = new Rectangle(0, 0, 1550, 450); } } private function _mouseWheel(mEvt:MouseEvent):void { scaleX = mEvt.delta > 0 ? scaleX + 0.03 : scaleX - 0.03; scaleY = scaleX; view.cameraOffset = new MathVector(stage.stageWidth / 2 / scaleX, stage.stageHeight / 2 / scaleY); view.cameraBounds = new Rectangle(0, 0, 1550 * scaleX, 450 * scaleY); } } } |
In a physics world, you will never rotate the world! You just have to rotate its view, then manage the gravity like you want
November should be an awesome month for the Citrus Engine, so stay tuned!
The Citrus Engine meets Away3D
16Some days ago, I was thinking that my next post would be the annoucement of the Citrus Engine V3 stable release. Since Starling and Nape are well integrated in the engine and the API very stable, I didn’t see any new big features coming. Oh in fact, I imagined one : 3D support. This one was crazy, and now it is real! Welcome to the first 2D & 3D Flash game engine!
I’m not familiar with 3D. Last year I had a quick look with ThreeJS and then ported it in Away3D. That was my only experience with 3D until now, so don’t hesitate to correct me and to offer some suggestions. That’s lots of new stuff to learn, and that’s pretty exciting!
Also if you have links to free arts/assets Away3D friendly, share please
For the impatients, here is the demo! Note the Away3D support needs lots of work yet. Click on the red square to add boxes (I was a bit lazy to make a correct button). Use the mouse to move the camera, and right/left/down/space keys for the Hero.
I use Box2D for the physics (it could be Nape), to see the Box2D debug view press tab to open the console and write :
set box2D visible true
All the source code is available on GitHub (with samples) and on GoogleCode (engine code only). The Away3D part will evolve a lot in the next month, the 2D stuff is very stable to use!
The case study : Trine & Rayman
Trine 2 is with Rayman Origins one of my favorite platformer game. Both have different styles & gameplay :
Even if Trine is a 2D platformer game, it uses 3D arts whereas Rayman doesn’t. The camera has nice movement which highlight the 3D assets even if it never flips on the other side. 3D arts with 2D physics / logics works like a charm, it was something to add to the Citrus Engine.
Why Away3D?
I’ve chosen Away3D since like Starling it is free, open source and strongly supported by Adobe! Note that it’s very easy to add an other 3D engine.
Handle 3D view in 2D physics world
Since Away3D doesn’t use the same coordinates (middle of the screen) than Flash (top left of the screen) it is not evident to put the view at the good object position. At first I was fighting with project and unproject method using an OrthographicLens camera but that was a nightmare. I needed some help.
I discovered Roger Engelbert’s blog some months ago. There are very good tutorials moving from one framework to an other one in an other language : Objective-C, Java, AS3. It’s definetly one blog to bookmark for developers (and artists now)! Roger has made several game prototypes using a 3D view with a 2D logic, like this one. So I asked him, what was the best way to handle a 3D view in a 2D games? He answered me with a new tutorial : 3D Coordinates from 2D Simulation. That was very kind and the article is very smart. No more project unproject method! Thank you Roger!
The demo
Please refer to the top of the article for the demo and instructions. The Perelith Knight asset come from this awesome example from Away3D’s hero Rob Bateman : Away3D multi-knight demo. I used Misfit Model 3D software to manage MD2 files changing animations name and moving a bit the jump animation.
The code
Main file
package away3dbox2d { import com.citrusengine.core.CitrusEngine; [SWF(frameRate="60")] /** * @author Aymeric */ public class Main extends CitrusEngine { public function Main() { state = new Away3DGameState(); } } } |
The Game State:
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 | package away3dbox2d { import away3d.controllers.HoverController; import away3d.debug.AwayStats; import away3d.entities.Mesh; import away3d.library.AssetLibrary; import away3d.loaders.parsers.MD2Parser; import away3d.materials.ColorMaterial; import away3d.primitives.CubeGeometry; import away3d.primitives.SphereGeometry; import com.citrusengine.core.State; import com.citrusengine.math.MathVector; import com.citrusengine.objects.Box2DPhysicsObject; import com.citrusengine.objects.CitrusSprite; import com.citrusengine.objects.platformer.box2d.Coin; import com.citrusengine.objects.platformer.box2d.Hero; import com.citrusengine.objects.platformer.box2d.Platform; import com.citrusengine.physics.Box2D; import com.citrusengine.view.CitrusView; import com.citrusengine.view.away3dview.AnimationSequence; import com.citrusengine.view.away3dview.Away3DArt; import com.citrusengine.view.away3dview.Away3DView; import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; import flash.geom.Rectangle; import flash.geom.Vector3D; /** * @author Aymeric */ public class Away3DGameState extends State { [Embed(source="/../embed/pknight/pknight3.png")] public static var PKnightTexture3:Class; [Embed(source="/../embed/pknight/pknight.md2", mimeType="application/octet-stream")] public static var PKnightModel:Class; // navigation variables private var _cameraController:HoverController; private var _move:Boolean = false; private var _lastPanAngle:Number; private var _lastTiltAngle:Number; private var _lastMouseX:Number; private var _lastMouseY:Number; private var _lookAtPosition:Vector3D = new Vector3D(); private var _heroArt:AnimationSequence; private var _hero:Hero; private var _clickMe:Sprite; public function Away3DGameState() { super(); } override public function initialize():void { super.initialize(); addChild(new AwayStats((view as Away3DView).viewRoot)); var box2D:Box2D = new Box2D("box2D"); // box2D.visible = true; add(box2D); AssetLibrary.enableParser(MD2Parser); _heroArt = new AnimationSequence(new PKnightModel(), new PKnightTexture3()); _heroArt.scale(2); var cube1:Mesh = new Mesh(new CubeGeometry(300, 300, 0), new ColorMaterial(0x0000FF)); var cube2:Mesh = new Mesh(new SphereGeometry(15), new ColorMaterial(0xFFFF00)); var cube3:Mesh = new Mesh(new CubeGeometry(2500, 10, 300), new ColorMaterial(0xFFFFFF)); var cloud:CitrusSprite = new CitrusSprite("cloud", {x:150, y:50, width:300, height:300, view:cube1, parallax:0.3}); add(cloud); (view.getArt(cloud) as Away3DArt).z = 300; // equivalent to -> cube1.z = 300; add(new Platform("platformBottom", {x:stage.stageWidth / 2, y:stage.stageHeight - 30, width:2500, height:10, view:cube3})); _hero = new Hero("hero", {x:150, y:50, width:80, height:90, view:_heroArt}); add(_hero); var coin:Coin = new Coin("coin", {x:300, y:200, width:30, height:30, view:cube2}); add(coin); view.setupCamera(_hero, new MathVector(320, 240), new Rectangle(0, 0, 1550, 450), new MathVector(.25, .05)); _cameraController = new HoverController((view as Away3DView).viewRoot.camera, null, 175, 20, 500); _clickMe = new Sprite(); _clickMe.y = stage.stageHeight - 50; addChild(_clickMe); _clickMe.graphics.beginFill(0xFF0000); _clickMe.graphics.drawRect(10, 0, 40, 40); _clickMe.graphics.endFill(); _clickMe.buttonMode = true; stage.addEventListener(MouseEvent.MOUSE_DOWN, _onMouseDown); stage.addEventListener(MouseEvent.MOUSE_UP, _onMouseUp); stage.addEventListener(Event.MOUSE_LEAVE, _onMouseUp); stage.addEventListener(MouseEvent.MOUSE_WHEEL, _onMouseWheel); _clickMe.addEventListener(MouseEvent.CLICK, _addBoxes); } // Make sure and call this override to specify Away3D view. override protected function createView():CitrusView { return new Away3DView(this, "2D"); } override public function destroy():void { stage.removeEventListener(MouseEvent.MOUSE_DOWN, _onMouseDown); stage.removeEventListener(MouseEvent.MOUSE_UP, _onMouseUp); stage.removeEventListener(Event.MOUSE_LEAVE, _onMouseUp); stage.removeEventListener(MouseEvent.MOUSE_WHEEL, _onMouseWheel); _clickMe.addEventListener(MouseEvent.CLICK, _addBoxes); removeChild(_clickMe); super.destroy(); } override public function update(timeDelta:Number):void { super.update(timeDelta); if (_move) { _cameraController.panAngle = 0.3 * (stage.mouseX - _lastMouseX) + _lastPanAngle; _cameraController.tiltAngle = 0.3 * (stage.mouseY - _lastMouseY) + _lastTiltAngle; } _cameraController.lookAtPosition = _lookAtPosition; } private function _addBoxes(mEvt:MouseEvent):void { add(new Box2DPhysicsObject("box" + new Date().time, {x:Math.random() * stage.stageWidth, view:new Mesh(new CubeGeometry(30, 30, 30), new ColorMaterial(Math.random() * 0xFFFFFF))})); } private function _onMouseDown(mEvt:MouseEvent):void { _lastPanAngle = _cameraController.panAngle; _lastTiltAngle = _cameraController.tiltAngle; _lastMouseX = stage.mouseX; _lastMouseY = stage.mouseY; _move = true; } private function _onMouseUp(evt:Event):void { _move = false; } private function _onMouseWheel(mEvt:MouseEvent):void { _cameraController.distance -= mEvt.delta * 5; if (_cameraController.distance < 100) _cameraController.distance = 100; else if (_cameraController.distance > 2000) _cameraController.distance = 2000; } } } |
The code of the demo is very easy to understand and doesn’t request strong Away3D knowledge!
Away3D is added in the Away3DView class. The Away3DArt handles the different assets. The AnimationSequence class provides animation for models.
Work is in progress on this 3 classes, there are lots of option to try and find the best!
Full 3D engine?
When you create the Away3D view, we give as an argument the mode : 2D or 3D. Certainly there will be an Away3DArt2D and an Away3DArt3D classes to facilitate work on coordinates. From this point, nothing can prevent to use the Citrus Engine as a 3D engine! On my to-do list, there is the support for a 3D physics engine library (I don’t know which one at the moment)! Nape was added easily in the engine, an other physics libraries will be too.
Too many libraries included?
- having lots of libraries included in the project which are never clearly used don’t have any impact on performance! It just has an impact on the final file size.
- from what I know, most developers never upgrade libraries used when they are working on a project unless there is a new feature needed or bug fixed. So it’s very easy & quick to remove libs you don’t use in the engine, just follow the compiler’s indications. It takes less than 2 minutes!
- giving people choices : thanks to the Citrus Engine’s architecture it is very easy to customize the framework adding new views or physics libraries. Want to use Starling + Nape for your mobile project? Want to use Box2D + Away3D for a browser game? The engine is already ready for that! It’s something important for me.
Future
From 29 September to 8 October, I will be in vacation. After I will focus on the Away3D support and offer a beta 3, the last one. If a 3D physics library is added, that will be for the V3.5 ![]()
Also I’m searching a software which can be used as a 3D Level Editor (like Flash Pro, Gleed or Tiled Map Editor used for 2D). Maybe Prefab will do the job!
That’s it for this blog post! Don’t hesitate to comment, give your opinions and advice. Feel free to contribute!
Cheers!
Tiled Map Editor supported in the Citrus Engine
5Some days ago, I received a mail from a guy working on a game with the Citrus Engine. It shows me the progress of his game… another blast!
Let me introduce CynicMusic’s game, one of the greatest platformer game I’ve ever seen made with Starling and the Citrus Engine! He hopes to finish it around Christmas, best wishes for Alex Smith. Play the game, graphics are pixel art, and the music is awesome! I really love the mood. I also want to highlight his work on the gameplay : water, reward box… managed by Box2D Alchemy. Take a look on the FPS, 60fps, yep guys I’m impressed!
I had some discussions with Alex about his workflow, he used the Tiled Map Editor to create his levels then export them as PNG files and finally import in Flash Pro. Using Starling, textures images aren’t bigger than 2048*2048. He has created several CitrusSprite with the different PNGs.
I knew the Tiled Map Editor before, but never gave it a serious look! Quickly it appears that it would be really nice to support it directly, but how can we handle the tmx format provided by the software for our map? I found an article on PixelPracht’s website, he wrote some useful classes to parse the map! He made a quick demo for Flixel game engine. It is also possible to create objects inside the software, great news!
So thanks to his work, all hail to him, I’ve been able to write a parser to handle the Tiled Map Editor’s map in the Citrus Engine! Click here to play the demo.
Graphics are free to use, they come from this website.
This is a screenshot of the level :

Since the Citrus Engine is not based on grid for collisions detection, I’ve added platform objects. Mixing grid and physics is quick and really a good thing I think. It offers lots of flexibility : you may use both and keep grid interactivity.
Here is the demo code :
Main class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | package tiledmap { import com.citrusengine.core.CitrusEngine; [SWF(frameRate="60")] /** * @author Aymeric */ public class Main extends CitrusEngine { [Embed(source="/../embed/tiledmap/map.tmx", mimeType="application/octet-stream")] private const _Map:Class; public function Main() { state = new TiledMapGameState(XML(new _Map())); } } } |
The TiledMapGameState 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 | package tiledmap { import com.citrusengine.core.State; import com.citrusengine.math.MathVector; import com.citrusengine.objects.platformer.box2d.Hero; import com.citrusengine.objects.platformer.box2d.Platform; import com.citrusengine.physics.Box2D; import com.citrusengine.utils.ObjectMaker; import com.citrusengine.view.spriteview.SpriteArt; import flash.geom.Rectangle; /** * @author Aymeric */ public class TiledMapGameState extends State { [Embed(source="/../embed/tiledmap/Genetica-tiles.png")] private const _ImgTiles:Class; private var _level:XML; public function TiledMapGameState(level:XML) { super(); _level = level; var objects:Array = [Hero, Platform]; } override public function initialize():void { super.initialize(); var box2D:Box2D = new Box2D("box2D"); //box2D.visible = true; add(box2D); ObjectMaker.FromTiledMap(_level, _ImgTiles); var hero:Hero = getObjectByName("hero") as Hero; view.setupCamera(hero, new MathVector(stage.stageWidth / 2, 240), new Rectangle(0, 0, 1280, 640), new MathVector(.25, .05)); (view.getArt(getObjectByName("foreground")) as SpriteArt).alpha = 0.3; } } } |
Here it works on the classic flash display list. It works also on Starling if level dimensions are not above 2048*2048. I’m thinking to use the tile system made by Nick Pinkham (Citrus Engine contributor member) to handle bigger levels! It has been added to the CE, but it’s not 100% stable at the moment.
Again, all the Citrus Engine works & demo are available on its GitHub. Take a look on the map and the different files. Play with it. Don’t hesitate to test this parser and tell me if some important options are missing.
I’ve also gave a look to the Level Architect’s code (official CE’s level editor, created by Eric Smith), for the first time, and can assure that it needs to much work to keep it supported with the new CE version. I prefer to support format from serious tools (Flash Pro, Tiled Map Editor, Gleed) and continue to work hard on the Citrus Engine.
At the moment I’m working on something very huge for the Citrus Engine and I hope to be able to come back soon with very good news. Stay tuned!