Playing with Cadet Editor 3D and AwayPhysics

The 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!

6 thoughts on “Playing with Cadet Editor 3D and AwayPhysics

  1. Hi Aymeric,

    Thanks for the feedback and this great post! 🙂

    It’s exciting for us to see other developers showing an interest in CadetEditor and integrating it with their own systems. It’s a shame we can’t provide you with the AwayPhysics editor just yet, you may have already seen that this is in development.

    The serialized scenes are just XML, though they use namespaces like MXML. The reason for this is that the namespaces (e.g. xmlns:cadet=”www.unwrong.com/cadet/1.0.0″) help to map the serialized objects to their AS3 equivalents. If we were to try and simplify the xml, we would lose information that is critical to this process in the general case. We are however considering introducing custom exporters for specific builds, allowing games (for instance) to deal in a much simplified format that’s quicker for their implementation to load/parse, so this might help you down the line.

    We’re actually currently working on a *big* update which should make your life easier in a number of ways… Looking forward to getting it out there, but there’s still a reasonable amount of work to do..

    If you’d like to discuss anything in more detail, please feel free to ask any questions on the new CadetEditor Google Group! https://groups.google.com/forum/?fromgroups#!forum/cadeteditor

    cheers
    rob

  2. Hey Rob,

    I understand the use of the namespaces used by Cadet. It just should export in other format for developers, that’s in your plan so it’s great!
    So I’m waiting for the big update 😉

    Continue the good work!

  3. Indeed, I really love how you can design a Box2D scene in Cadet2D. However the save format file isn’t easy to parse. I hope that Cadet Editor will provide a simple xml or json formats. From what Rob said, that’s on the roadmap 😉

  4. I hadn’t thought of using ActionScript for 3D game development (due to the lack of developer friendly tools). But Citrus + Cadet are clearly proving that Flash will continue to evolve as a legitimate game development toolset.

    Great work Rob and Aymeric!

Leave a Reply

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