Alternativa 3D Series – Tutorial 4 – Working with Camera Controllers

by Matthew Casperson 4

The vast majority of 3D flash applications you see on the net today are quite simple. For the most part they will be advertisements flipping a bunch of pictures around, a 3D photo album or something similar. However the Flash runtime is improving all the time, allowing more detailed and interactive applications. In fact I don’t think it will be long before Flash is the platform of choice for the sort of 3D games that were all the rage maybe 10 years ago.

In order to take advantage of these new possibilities provided by Flash it is necessary not to just display a 3D world, but also to interact with and move around inside of it. Thankfully Alternativa already offers the developer a very easy way to move inside a 3D world with WalkController and FlyController (both of which extend the ObjectController class). With just a few lines of code you can move the camera, or any 3D object, around inside a 3D level with full collision detection.

Requirements

First of all we need a 3D world to move around in. For that we use the ResourceManager class.

ResourceManager.as

package
{
	import alternativa.engine3d.core.Mesh;
	import alternativa.engine3d.core.Object3D;
	import alternativa.engine3d.loaders.Loader3DS;
	import alternativa.engine3d.materials.TextureMaterialPrecision;
	import alternativa.utils.MeshUtils;

	import flash.events.Event;
	import flash.events.IOErrorEvent;
	import flash.events.SecurityErrorEvent;

	import mx.collections.ArrayCollection;
	import mx.core.Application;

	/**
	 * 	ResourceManager is where we embed and create the resources needed by our application
	 */
	public class ResourceManager
	{
		protected static const SERVER_URL:String = "http://alternativatut.sourceforge.net/media/";

		public static var mapElements:ArrayCollection = new ArrayCollection();
		protected static var mapLoader:Loader3DS = null;
		protected static var mapLoaded:Boolean = false;

		public static function get allResourcesLoaded():Boolean
		{
			return mapLoaded;
		}

		public static function loadResources():void
		{
			mapLoader = new Loader3DS();
			mapLoader.smooth = true;
			mapLoader.precision = TextureMaterialPrecision.HIGH;

			// watch out - case counts on some web servers, but not in windows
			mapLoader.load(SERVER_URL + "testmap.3ds");
			mapLoader.addEventListener(Event.COMPLETE, onLoadMapComplete);
			mapLoader.addEventListener(IOErrorEvent.IO_ERROR, ioError);
			mapLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityError);
		}

		protected static function ioError(e:Event):void
		{
			Application.application.lblLoading.text = "Error Loading";
		}

		protected static function securityError(e:Event):void
		{
			Application.application.lblLoading.text = "Error Loading";
		}

		protected static function weldVerticesAndFaces(object:Object3D):void
		{
			if (object != null)
			{
				if (object is Mesh)
				{
					MeshUtils.autoWeldVertices(Mesh(object), 0.01);
					MeshUtils.autoWeldFaces(Mesh(object), 0.01, 0.001);
				}

				// Launching procedure for object's children
				for (var key:* in object.children)
				{
					weldVerticesAndFaces(key);
				}
			}
		}

		protected static function onLoadMapComplete(e:Event):void
		{
			for (var o:* in mapLoader.content.children)
				mapElements.addItem(o);

			mapLoaded = true;
		}
	}
}

The vast majority of this code for the ResourceManager is copied straight from the last article. The only changes that were necessary were the addition of a ArrayCollection property called mapElements, and a slight modification of the onLoadMapComplete function (renamed from onFighterLoadComplete). This is because, unlike the last article where the 3DS model had only one child mesh, the 3DS file that is our 3D level may have more than one. So instead of retrieving the first mesh and then breaking from the loop we save each mesh into the mapElements array.

ApplicationManager.as

package
{
	import alternativa.engine3d.controllers.FlyController;
	import alternativa.engine3d.controllers.WalkController;
	import alternativa.engine3d.core.Camera3D;
	import alternativa.engine3d.core.Mesh;
	import alternativa.types.Point3D;

	import mx.collections.ArrayCollection;
	import mx.core.Application;

	/**
	 * 	The ApplicationManager holds all program related logic.
	 */
	public class ApplicationManager extends BaseObject
	{
		protected static var instance:ApplicationManager = null;
		protected var mapMeshes:ArrayCollection = new ArrayCollection;
		protected var walk:WalkController = null;
		protected var fly:FlyController = null;

		public static function get Instance():ApplicationManager
		{
			if (instance == null)
				instance = new ApplicationManager();
			return instance;
		}

		public function ApplicationManager()
		{
			super();
		}

		public function startupApplicationManager():ApplicationManager
		{
			for each (var mesh:Mesh in ResourceManager.mapElements)
				mapMeshes.addItem(new MeshObject().startupModelObject(mesh));

			super.startupBaseObject();

			var cam:Camera3D = Application.application.engineManager.camera;

			walk = new WalkController(Application.application.stage);
			walk.object = cam;
			walk.setDefaultBindings();
			walk.checkCollisions = true;
			walk.gravity = 100;

			fly = new FlyController(Application.application.stage);
			fly.object = cam;
			fly.setDefaultBindings();
			fly.checkCollisions = true;

			setController();

			return this;
		}

		public function setController():void
		{
			var cam:Camera3D = Application.application.engineManager.camera;
			cam.rotationZ = 0;
			cam.rotationY = 0;
			cam.rotationX = 0;

			fly.enabled = Application.application.rdoFly.selected;
			walk.enabled = Application.application.rdoWalk.selected;
		}

		public override function enterFrame(dt:Number):void
		{
			walk.processInput();
			fly.processInput();
		}
	}
}

The ApplicationManager class contains the code that makes use of the two ObjectControllers. You’ll notice that we create two properties, walkController and flyController; one for each of the Alternativa ObjectControllers. By creating both we can easily switch between the two at runtime.

The startupApplicationManager function is where we initialise each of these properties. You’ll notice that we loop through each of the Meshes in ResourceManager mapElements, and create a corresponding MeshObject which is added to the mapMeshes array. This essentially adds the 3DS model we loaded with the ResourceManager to the scene. We then create the two ObjectControllers. The code is quite straight forward: we create the object, set it to control the camera, initialise the default key bindings with the setDefaultBindings function, enable collisions, and for the walkController we set a value for gravity.

Finally we call setController, which enables (via the enabled property) one of the ObjectControllers or the other depending on which radio box in the main GUI is selected. The setController function also resets any rotation in the camera when switching from one controller to the next. This is to stop the orientation of the FlyController from translating to the WalkController. This is important because the FlyController modifies the roll of the camera, whereas the WalkController does not. For those not familiar with the term “roll” (along with “pitch” and “yaw”), imagine that pitch is like looking up and down, yaw is like looking left and right, and roll is like standing on your head.

The last function in ApplicationManager, enterFrame, simply calls the processInput function on our two controllers, which allows them to respond to any key presses or mouse movements the player has made during the frame. Even though both controllers are both processing input, only one is ever enabled at any given time, so only one will actually move the camera.

Alternativa4.mxml

<?xml version="1.0" encoding="utf-8"?>
<mx:Application
	xmlns:mx="http://www.adobe.com/2006/mxml"
	layout="absolute"
	xmlns:ns1="*"
	width="600"
	height="400"
	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.png')"/>
	<mx:Image x="10" y="340" source="@Embed(source='../media/thetechlabs.png')"/>
	<mx:Label x="247" y="172" text="Loading" color="#000000" fontSize="26" id="lblLoading"/>
	<mx:RadioButtonGroup id="controllers"/>
	<mx:RadioButton x="10" y="50" label="Fly" groupName="controllers" color="#FFFFFF" id="rdoFly" click="{ApplicationManager.Instance.setController();}"/>
	<mx:RadioButton x="10" y="76" label="Walk" groupName="controllers" color="#FFFFFF" selected="true" id="rdoWalk" click="{ApplicationManager.Instance.setController();}"/>
</mx:Application>

The MXML file includes a RadioButtonGroup, to which we add two radio buttons, which allow the player to select either the walk controller, or the fly controller. Both radio buttons call the ApplicationManager setController function which I mentioned above.

When you play the final result you will notice that the camera drops to the ground (because the walk controller is the default one), and as you move around you will slide along the edges of the 3D objects in the world. This movement is very common with FPS’s. When you enable the fly controller you can move around the level like you are playing a flight simulator.

While the ability to move around and collide with a level might not seem like much, it saves you from writing a whole lot of tedious code. In fact I don’t know of any other Flash 3D engine that allows you to move in and collide with a 3D world so easily.

Check out the online demo here, and download the source code here. The default keys for the fly controller are listed here, and the default keys for the walk controller are listed here.

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>