Alternativa 3D Series – Tutorial 1 – Getting Started
by 22 February, 2009 11:44 pm11
Alternativa is a 3D Flash engine that allows you to embed 3D environments directly into your web pages. Given that the Flash player is installed on something like 90% of all internet connected devices, Alternativa gives you a 3D platform that requires no effort or installation from your end users. In this tutorial I will show you how to create a simple 3D application using Alternativa and Flex.
While the end result of this tutorial is very simple, this is not a “hello world” application. From the outset the focus of this tutorial will be to create a solid framework that can be easily extended in future articles. With that goal in mind one of the principals that I’ll be applying here is the separation of the logic relating to the management of the underlying 3D engine (i.e. pretty much any code to do with Alternativa), and the logic that defines the desired result of application itself.
Requirments
- Adobe Flex
- Alternativa 3D (need free registration)
- Source files
EngineManager.as
package { import flash.display.StageAlign; import flash.display.StageScaleMode; import flash.events.Event; import mx.collections.ArrayCollection; import mx.core.Application; import mx.core.UIComponent; import alternativa.engine3d.controllers.CameraController; import alternativa.engine3d.core.Camera3D; import alternativa.engine3d.core.Object3D; import alternativa.engine3d.core.Scene3D; import alternativa.engine3d.display.View; import alternativa.utils.FPS; /** * The EngineManager holds all of the code related to maintaining the Alternativa 3D engine. */ public class EngineManager extends UIComponent { public var scene:Scene3D; public var view:View; public var camera:Camera3D; public var cameraController:CameraController; // a collection of the BaseObjects protected var baseObjects:ArrayCollection = new ArrayCollection(); // a collection where new BaseObjects are placed, to avoid adding items // to baseObjects while in the baseObjects collection while it is in a loop protected var newBaseObjects:ArrayCollection = new ArrayCollection(); // a collection where removed BaseObjects are placed, to avoid removing items // to baseObjects while in the baseObjects collection while it is in a loop protected var removedBaseObjects:ArrayCollection = new ArrayCollection(); // the last frame time protected var lastFrame:Date; public function EngineManager() { super(); addEventListener(Event.ADDED_TO_STAGE, init); } public function init(e:Event): void { stage.scaleMode = StageScaleMode.NO_SCALE; stage.align = StageAlign.TOP_LEFT; // Creating scene scene = new Scene3D(); scene.root = new Object3D(); // Adding camera and view camera = new Camera3D(); camera.x = 100; camera.y = -150; camera.z = 100; scene.root.addChild(camera); view = new View(); addChild(view); view.camera = camera; // Connecting camera controller cameraController = new CameraController(stage); cameraController.camera = camera; cameraController.setDefaultBindings(); cameraController.checkCollisions = true; cameraController.collisionRadius = 20; cameraController.controlsEnabled = true; // FPS display launch FPS.init(stage); stage.addEventListener(Event.RESIZE, onResize); stage.addEventListener(Event.ENTER_FRAME, onEnterFrame); onResize(null); // set the initial frame time lastFrame = new Date(); // start the application ApplicationManager.Instance.startupApplicationManager(); } private function onResize(e:Event):void { view.width = stage.stageWidth; view.height = stage.stageHeight; Application.application.width = stage.stageWidth; Application.application.height = stage.stageHeight; } protected function onEnterFrame(event:Event):void { // Calculate the time since the last frame var thisFrame:Date = new Date(); var seconds:Number = (thisFrame.getTime() - lastFrame.getTime())/1000.0; lastFrame = thisFrame; // sync the baseObjects collection with any BaseObjects created or removed during the // render loop removeDeletedBaseObjects(); insertNewBaseObjects(); // allow each BaseObject to update itself for each (var baseObject:BaseObject in baseObjects) baseObject.enterFrame(seconds); // User input processing cameraController.processInput(); // Scene calculating scene.calculate(); } public function addBaseObject(baseObject:BaseObject):void { newBaseObjects.addItem(baseObject); } public function removeBaseObject(baseObject:BaseObject):void { removedBaseObjects.addItem(baseObject); } protected function shutdownAll():void { // don't dispose objects twice for each (var baseObject:BaseObject in baseObjects) { var found:Boolean = false; for each (var removedObject:BaseObject in removedBaseObjects) { if (removedObject == baseObject) { found = true; break; } } if (!found) baseObject.shutdown(); } } protected function insertNewBaseObjects():void { for each (var baseObject:BaseObject in newBaseObjects) baseObjects.addItem(baseObject); newBaseObjects.removeAll(); } protected function removeDeletedBaseObjects():void { for each (var removedObject:BaseObject in removedBaseObjects) { var i:int = 0; for (i = 0; i < baseObjects.length; ++i) { if (baseObjects.getItemAt(i) == removedObject) { baseObjects.removeItemAt(i); break; } } } removedBaseObjects.removeAll(); } } }
We’ll start with the EngineManager class. This class will hold all of the code necessary to initialize and run the Alternativa 3D engine. The EngineManager class extends UIComponent, which means we can add it to the root Flex Application class like any other GUI control. We’ll see this in action when we take a look at the Alternativa1.mxml file later.
In the EngineManager constructor we add an event listener which will call the init function once the EngineManager has been added to the on stage display list. If added to the on stage display list doesn’t mean anything to you, don’t worry – all you need to know is that once this event has occurred the stage property is no longer null.
The init function holds the majority of the Alternativa initialization code. We create 4 important Alternativa objects: a Scene3D, a Camera3D, a CameraController and a View. Scene3D is essentially a container in
which our scene objects are placed. The Camera3D is, as the name suggests, the viewpoint into the world. The CameraController is a convenient class that allows us to move the camera with the keyboard and mouse, and also to detect collisions with scene objects. CameraController gives you this ability to move in and
interact with the world with all of 6 lines of code, which is very cool. Finally there is the View object, which basically takes the 3D world that the camera see’s and translates it into a 2D picture to be displayed on your monitor.
You’ll also notice that we call FPS.init(stage). This places a stat counter on the screen which gives you a readout of the memory usage frames per second. It’s useful for monitoring your application, but you’d
want to comment out that line when deploying your final build.
Once the Alternativa engine has been initialized we then add two more event listeners, one to respond to window resizing events (stage.addEventListener(Event.RESIZE, onResize) ), and one to respond to frame drawing events ( stage.addEventListener(Event.ENTER_FRAME, onEnterFrame) ). We catch the window resizing events so we can update the size of the View to match. The frame drawing event will form the basis of our render loop.
Render loop is a general term that refers to the loop that defines how the application runs. The loop consists of two parts. The first is where the application updates itself. Any movement or logic in your game is done in this first part of the render loop, say by moving a rocket further through space. The second is where the 3D engine renders one frame to the screen, thus displaying the changes that were made to the 3D world.
Updating the application (the first part of the render loop) is done via the BaseObject class. You will notice in the onEnterFrame function that we loop through a collection of BaseObject’s calling their enterFrame function. The enterFrame function is the sole purpose for the BaseObject class – it allows any class that extends BaseObject to easily update itself inside the render loop. We’ll see how this works a little later with the MeshObject and RotatingBox classes.
Finally in the init function we call ApplicationManager.Instance.startupApplicationManager(). I said before that we would be splitting the application logic from the engine logic. With the EngineManager class taking care of managing the Alternativa engine and the render loop it’s time to get our program to actually do something. For this we create the ApplicationManager class.
ApplicationManager.as
package { import mx.core.Application; /** * The ApplicationManager holds all program related logic. */ public class ApplicationManager { protected static var instance:ApplicationManager = null; public static function get Instance():ApplicationManager { if (instance == null) instance = new ApplicationManager(); return instance; } public function ApplicationManager() { } public function startupApplicationManager():ApplicationManager { var rotatingBox:RotatingBox = new RotatingBox().startupRotatingBox(); Application.application.engineManager.cameraController.lookAt(rotatingBox.model.coords); return this; } } }
The ApplicationManager is designed as a Singleton. Although ActionScript does not have the ability to have protected or private constructors (and thus easily implement singletons), and singletons do have some negative consequences, I still find the design pattern useful as a tool for self documentation – if a class has an Instance property then it is a singleton and you should not create objects with the new function.
The ApplicationManager in this example has only one function (apart from the singleton property): startupApplicationManager. It’s in this function that we write the code that relates to the application itself, as opposed to initialising the Alternativa engine. In our case we are creating a RotatingBox and pointing the camera at it. It may seem like overkill to define a class for 2 lines of code, but this separation of engine / application logic will be more useful in complicated applications.
BaseObject.as
package { import mx.core.Application; /** * The BaseObject class allows extending classes to update themselves during the render loop. */ public class BaseObject { public function BaseObject() { } /** * Must be called by all extending classes when being created. Adds this object to the list of BaseObjects maintained * by the EngineManager. */ public function startupBaseObject():void { Application.application.engineManager.addBaseObject(this); } /** * Must be called by all extending classes when being destroyed. Removes this object to the list of BaseObjects maintained * by the EngineManager. */ public function shutdown():void { Application.application.engineManager.removeBaseObject(this); } /** * This function is called once per frame before the scene is rendered. * * @param dt The time in seconds since the last frame was rendered. */ public function enterFrame(dt:Number):void { } } }
MeshObject.as
package { import alternativa.engine3d.core.Object3D; import alternativa.engine3d.materials.SurfaceMaterial; import mx.core.Application; public class MeshObject extends BaseObject { public var model:Object3D = null; public function MeshObject() { super(); } override public function shutdown():void { super.shutdown(); Application.application.engineManager.scene.root.removeChild(model); model = null; } public function startupModelObject(object:Object3D):void { model = object; Application.application.engineManager.scene.root.addChild(model); super.startupBaseObject(); } } }
The RotatingBox class is an example of how to extend the BaseObject class, although if you look closely we have actually created an intermediate class called MeshObject. MeshObject extends the BaseObject class and adds an Object3D property, which represents a 3D mesh on the screen. Again it may seem trivial to implement another class for this one property, but the truth is that while most of the elements in you application or game that need updating every frame will also have a 3D mesh representation, occasionally there will be the need for a class to update every frame while not having any 3D mesh associated with it. This is why we have the BaseObject and MeshObject as separate classes: extending BaseObject allows an object to update itself, and MeshObject will be a common base class for those objects that have 3D models – like the RotatingBox.
RotatingBox.as
package { import alternativa.engine3d.materials.WireMaterial; import alternativa.engine3d.primitives.Box; public class RotatingBox extends MeshObject { protected static const ROTATION_SPEED:Number = 1; public function RotatingBox() { super(); } public function startupRotatingBox():RotatingBox { var box:Box = new Box(100, 100, 100, 3, 3, 3); box.cloneMaterialToAllSurfaces(new WireMaterial(1, 0x000000)); super.startupModelObject(box); return this; } public override function enterFrame(dt:Number):void { model.rotationX += dt * ROTATION_SPEED; model.rotationY += dt * ROTATION_SPEED; model.rotationZ += dt * ROTATION_SPEED; } } }
So let’s take a look at the RotatingBox class. We have two important functions: startupRotatingBox and enterFrame. The startupRotatingBox function is responsible for creating and texturing the mesh that will be displayed on the screen. In this case we use the built in Box primitive, and texture it with a wireframe texture called WireMaterial. The enterFrame function has been overridden from BaseObject, and it is here that we rotate the box by a small amount each frame.
Alternativa1.mxml
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:ns1="*" width="640" height="480" color="#FFFFFF" backgroundGradientAlphas="[1.0, 1.0]" backgroundGradientColors="[#FFFFFF, #C0C0C0]"> <ns1:EngineManager id="engineManager" x="0" y="0" width="100%" height="100%"/> <mx:Image x="10" y="10" id="imgAlternativa" source="@Embed(source='../media/alternativa.jpg')"/> </mx:Application>
Putting it all together is the Alternativa1.mxml file. This is the entry point of the application. As I mentioned earlier the EngineManager class extends the UIComponent class, which allows it to be added as a child of the main Application class. You can see we have added the EngineManager as a child control, just like you would a button or text label.
So what have we created here? The EngineManager class holds the code required to initialize and manage the Alternativa engine. This code will change very little between examples. The ApplicationManager class holds the code that relates to the logic of the application itself. You will find that this code will almost always change between different examples. The BaseObject and MeshObject base classes allow us an easy way to create object that can update themselves in the render loop. These classes will feature heavily in future
tutorials. And finally we have the RotatingBox class that shows you how to extend the MeshObejct class to create a self updating object in the 3D world.
Check out the online demo here, and download the source code here.