Alternativa 3D Series – Tutorial 5 – Creating Interactivity

by Matthew Casperson 3

If you have ever created a traditional forms based application you’ve probably taken it for granted that you can attach functions to mouse events. After all, what good is a button if your program can’t respond to a mouse click? In Flash and Flex applications you listen and respond to mouse events (and many more) by attaching functions via the addEventListener function. The good news is that you can respond to the same events on Alternativa 3D objects in exactly the same way.

Requirements

Pre-Requesites

You should have read the first tutorial in this series, as it explains a lot of the underlying code that we build on for this tutorial.

Before we go into the detail of how this works, check out the demo that this code produces. You’ll see a big grey cube surrounded by 6 smaller cubes of various colours. Try clicking on the grey cube. You’ll notice that as you move the mouse over the grey cube with the left mouse button held down you draw on the surface. Now click on one of the smaller coloured cubes and try it again. You’ve just changed the colour you paint with. Now click the < or > buttons to flip the grey cube around to expose another surface to paint on. It’s no Photoshop, but it is a good demonstration of how to respond to mouse events involving Alternativa 3D objects.

EngineManager.as

package
{
	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;

	import flash.display.StageAlign;
	import flash.display.StageScaleMode;
	import flash.events.Event;

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

	/**
	 * 	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;
		// this flag is set to true to indicate that the ApplicationManager has been initialised
		protected var applicationManagerStarted:Boolean = false;

		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();
			scene.splitAnalysis = false;

			// Adding camera and view
			camera = new Camera3D();
			camera.x = 0;
			camera.y = 0;
			camera.z = 175;
			scene.root.addChild(camera);

			view = new View();
			// have to make the view interactive
			view.interactive = true;
			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 = false;

			// 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();

			// load the resources
			ResourceManager.loadResources();
		}

		private function onResize(e:Event):void
		{
			view.width = stage.stageWidth;
			view.height = stage.stageHeight;
			Application.application.width = stage.stageWidth;
			Application.application.height = stage.stageHeight;
		}

		/**
		 * 	This function is called once per frame. It notfies the BaseObjects
		 *  that a frame is about to be rendered and then calculates the scene
		 */
		protected function onEnterFrame(event:Event):void
		{
			// first check to make sure that all the resources have been loaded
			if (ResourceManager.allResourcesLoaded)
			{
				// once the resources have been loaded we can initiliase the ApplicationManager
				if (!applicationManagerStarted)
				{
					// set the flag to prevent the ApplicationManager being initialised twice
					applicationManagerStarted = true;
					// start the application
					ApplicationManager.Instance.startupApplicationManager();
				}

				// 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);

				// Scene calculating
				scene.calculate();
			}
		}

		/**
		 * 	Adds a BaseObject to the temporary collection newBaseObjects, the
		 *  contents of which will be added to the main baseObjects collection
		 *  in the insertNewBaseObjects function.
		 *
		 *  This function should only be called by the BaseObject class.
		 *
		 *  @param baseObject The BaseObject to be added
		 */
		public function addBaseObject(baseObject:BaseObject):void
		{
			newBaseObjects.addItem(baseObject);
		}

		/**
		 * 	Adds a BaseObject to the temporary collection removedBaseObjects, the
		 *  contents of which will be removed from the main baseObjects collection
		 *  in the removeDeletedBaseObjects function.
		 *
		 *  This function should only be called by the BaseObject class.
		 *
		 * 	@param baseObject The BaseObject to be removed
		 */
		public function removeBaseObject(baseObject:BaseObject):void
		{
			removedBaseObjects.addItem(baseObject);
		}

		/**
		 * 	Shutdown all the BaseObjects
		 */
		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();
			}
		}

		/**
		 * 	Adds the BaseObjects added to newBaseObjects with the function addBaseObject
		 *  to the main collection
		 */
		protected function insertNewBaseObjects():void
		{
			for each (var baseObject:BaseObject in newBaseObjects)
				baseObjects.addItem(baseObject);

			newBaseObjects.removeAll();
		}

		/**
		 * 	Removes the BaseObjects added to removedBaseObjects with the function removeBaseObject
		 *  from the main collection
		 */
		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();
		}
	}
}

The only change we have to make with the EngineManager class is where we set view.interactive = true. Once the interactive property is set to true Alternativa will start dispatching mouse events.

ApplicationManager.as

package
{
	import alternativa.engine3d.events.MouseEvent3D;
	import alternativa.engine3d.materials.TextureMaterial;
	import alternativa.engine3d.primitives.Box;

	import caurina.transitions.Tweener;

	import flash.display.BitmapData;
	import flash.geom.Rectangle;

	import mx.core.Application;

	/**
	 * 	The ApplicationManager holds all program related logic.
	 */
	public class ApplicationManager extends BaseObject
	{
		protected static var instance:ApplicationManager = null;
		/// true if we are currently drawing on the cube, false otherwise
		protected var drawing:Boolean = false;
		/// the colour of the main cube
		protected var colour:uint = 0xFFFFFFFF;
		/// the desired rotation of the main cube
		protected var yRotation:Number = 0;
		/// the actual main cube MeshObject
		protected var cube:MeshObject = null;
		/// the red colour cube
		protected var redColourCube:MeshObject = null;
		/// the green colour cube
		protected var greenColourCube:MeshObject = null;
		/// the blue colour cube
		protected var blueColourCube:MeshObject = null;
		/// the cyan colour cube
		protected var cyanColourCube:MeshObject = null;
		/// the yellow colour cube
		protected var yellowColourCube:MeshObject = null;
		/// the purple colour cube
		protected var purpleColourCube:MeshObject = null;

		/**
		 *  returns the singelton instance of the ApplicationManager
		 */
		public static function get Instance():ApplicationManager
		{
			if (instance == null)
				instance = new ApplicationManager();
			return instance;
		}

		public function ApplicationManager()
		{
			super();
		}

		/**
		 *  Initialise the ApplicationManager
		 */
		public function startupApplicationManager():ApplicationManager
		{
			super.startupBaseObject();

			// we create the main "drawing" cube
			cube = new MeshObject().startupModelObject(new Box(100, 100, 100));
			var surafceArray:Array = cube.model.surfaces.toArray(true);
			for (var i:int = 0; i < 6; ++i)
				cube.model.setMaterialToSurface(ResourceManager.cubeTexture[i], surafceArray[i]);
			Application.application.engineManager.cameraController.lookAt(cube.model.coords);

			// attach event listeners
			cube.model.addEventListener(MouseEvent3D.MOUSE_DOWN, mouseDown);
			cube.model.addEventListener(MouseEvent3D.MOUSE_UP, mouseUp);
			// releasing the mouse button has the same effect has the mouse moving
			// off the cube, so we just attach the MOUSE_OUT event to the same
			// mouseUp function as the MOUSE_UP event
			cube.model.addEventListener(MouseEvent3D.MOUSE_OUT, mouseUp);
			cube.model.addEventListener(MouseEvent3D.MOUSE_MOVE, mouseMove);

			// these are the "colour" cubes that sit alonside the main "drawing" cube

			// create the cube
			redColourCube = new MeshObject().startupModelObject(new Box(20, 20, 20));
			// set the material of the cube
			redColourCube.model.cloneMaterialToAllSurfaces(ResourceManager.redTexture);
			// set the position of the cube
			redColourCube.model.x = -115;
			redColourCube.model.y = 45;

			// attach event listeners
			redColourCube.model.addEventListener(MouseEvent3D.CLICK, colourMouseClick);
			redColourCube.model.addEventListener(MouseEvent3D.MOUSE_OVER, colourMouseOver);
			redColourCube.model.addEventListener(MouseEvent3D.MOUSE_OUT, colourMouseOut);

			// ... and repeat for each of the 5 additional colour cubes
			greenColourCube = new MeshObject().startupModelObject(new Box(20, 20, 20));
			greenColourCube.model.cloneMaterialToAllSurfaces(ResourceManager.greenTexture);
			greenColourCube.model.x = -115;
			greenColourCube.model.y = 0;

			greenColourCube.model.addEventListener(MouseEvent3D.CLICK, colourMouseClick);
			greenColourCube.model.addEventListener(MouseEvent3D.MOUSE_OVER, colourMouseOver);
			greenColourCube.model.addEventListener(MouseEvent3D.MOUSE_OUT, colourMouseOut);

			blueColourCube = new MeshObject().startupModelObject(new Box(20, 20, 20));
			blueColourCube.model.cloneMaterialToAllSurfaces(ResourceManager.blueTexture);
			blueColourCube.model.x = -115;
			blueColourCube.model.y = -45;

			blueColourCube.model.addEventListener(MouseEvent3D.CLICK, colourMouseClick);
			blueColourCube.model.addEventListener(MouseEvent3D.MOUSE_OVER, colourMouseOver);
			blueColourCube.model.addEventListener(MouseEvent3D.MOUSE_OUT, colourMouseOut);

			cyanColourCube = new MeshObject().startupModelObject(new Box(20, 20, 20));
			cyanColourCube.model.cloneMaterialToAllSurfaces(ResourceManager.cyanTexture);
			cyanColourCube.model.x = 115;
			cyanColourCube.model.y = 45;

			cyanColourCube.model.addEventListener(MouseEvent3D.CLICK, colourMouseClick);
			cyanColourCube.model.addEventListener(MouseEvent3D.MOUSE_OVER, colourMouseOver);
			cyanColourCube.model.addEventListener(MouseEvent3D.MOUSE_OUT, colourMouseOut);

			yellowColourCube = new MeshObject().startupModelObject(new Box(20, 20, 20));
			yellowColourCube.model.cloneMaterialToAllSurfaces(ResourceManager.yellowTexture);
			yellowColourCube.model.x = 115;
			yellowColourCube.model.y = 0;

			yellowColourCube.model.addEventListener(MouseEvent3D.CLICK, colourMouseClick);
			yellowColourCube.model.addEventListener(MouseEvent3D.MOUSE_OVER, colourMouseOver);
			yellowColourCube.model.addEventListener(MouseEvent3D.MOUSE_OUT, colourMouseOut);

			purpleColourCube = new MeshObject().startupModelObject(new Box(20, 20, 20));
			purpleColourCube.model.cloneMaterialToAllSurfaces(ResourceManager.purpleTexture);
			purpleColourCube.model.x = 115;
			purpleColourCube.model.y = -45;

			purpleColourCube.model.addEventListener(MouseEvent3D.CLICK, colourMouseClick);
			purpleColourCube.model.addEventListener(MouseEvent3D.MOUSE_OVER, colourMouseOver);
			purpleColourCube.model.addEventListener(MouseEvent3D.MOUSE_OUT, colourMouseOut);

			return this;
		}

		public override function enterFrame(dt:Number):void
		{
			// let the Tweener class update the animations
			Tweener.updateTime();
		}

		public function rotateCubeLeft():void
		{
			// remove any existing tweening animations for the main cude
			Tweener.removeTweens(cube.model);
			// update the target rotation (in radians)
			yRotation += Math.PI/2;
			// tell the Tweener to rotate the main cube to the desired rotation
			Tweener.addTween(cube.model, {time:2, rotationY:yRotation});
		}

		public function rotateCubeRight():void
		{
			Tweener.removeTweens(cube.model);
			yRotation -= Math.PI/2;
			Tweener.addTween(cube.model, {time:2, rotationY:yRotation});
		}

		public function colourMouseClick(event:MouseEvent3D):void
		{
			// we set the colour property depending on the cube that we clicked on,
			// which we can tell by the event.target property
			if (event.target == redColourCube.model)
				colour = 0xFFFF0000;
			else if (event.target == greenColourCube.model)
				colour = 0xFF00FF00;
			else if (event.target == blueColourCube.model)
				colour = 0xFF0000FF;
			else if (event.target == cyanColourCube.model)
				colour = 0xFF00FFFF;
			else if (event.target == yellowColourCube.model)
				colour = 0xFFFFFF00;
			else if (event.target == purpleColourCube.model)
				colour = 0xFFFF00FF;
		}

		public function colourMouseOver(event:MouseEvent3D):void
		{
			// the cube that the mouse has passed over is set to scale up to indicate that
			// it is under the mouse pointer
			if (event.target == redColourCube.model)
			{
				Tweener.removeTweens(redColourCube.model);
				Tweener.addTween(redColourCube.model, {time:1, scaleX:1.5, scaleY:1.5, scaleZ:1.5});
			}
			else if (event.target == greenColourCube.model)
			{
				Tweener.removeTweens(greenColourCube.model);
				Tweener.addTween(greenColourCube.model, {time:1, scaleX:1.5, scaleY:1.5, scaleZ:1.5});
			}
			else if (event.target == blueColourCube.model)
			{
				Tweener.removeTweens(blueColourCube.model);
				Tweener.addTween(blueColourCube.model, {time:1, scaleX:1.5, scaleY:1.5, scaleZ:1.5});
			}
			else if (event.target == cyanColourCube.model)
			{
				Tweener.removeTweens(cyanColourCube.model);
				Tweener.addTween(cyanColourCube.model, {time:1, scaleX:1.5, scaleY:1.5, scaleZ:1.5});
			}
			else if (event.target == yellowColourCube.model)
			{
				Tweener.removeTweens(yellowColourCube.model);
				Tweener.addTween(yellowColourCube.model, {time:1, scaleX:1.5, scaleY:1.5, scaleZ:1.5});
			}
			else if (event.target == purpleColourCube.model)
			{
				Tweener.removeTweens(purpleColourCube.model);
				Tweener.addTween(purpleColourCube.model, {time:1, scaleX:1.5, scaleY:1.5, scaleZ:1.5});
			}
		}

		public function colourMouseOut(event:MouseEvent3D):void
		{
			// the cube that the mouse has passed over is set to scale back down to normal
			// to indicate that it is no longer under the mouse pointer
			if (event.target == redColourCube.model)
			{
				Tweener.removeTweens(redColourCube.model);
				Tweener.addTween(redColourCube.model, {time:1, scaleX:1, scaleY:1, scaleZ:1});
			}
			else if (event.target == greenColourCube.model)
			{
				Tweener.removeTweens(greenColourCube.model);
				Tweener.addTween(greenColourCube.model, {time:1, scaleX:1, scaleY:1, scaleZ:1});
			}
			else if (event.target == blueColourCube.model)
			{
				Tweener.removeTweens(blueColourCube.model);
				Tweener.addTween(blueColourCube.model, {time:1, scaleX:1, scaleY:1, scaleZ:1});
			}
			else if (event.target == cyanColourCube.model)
			{
				Tweener.removeTweens(cyanColourCube.model);
				Tweener.addTween(cyanColourCube.model, {time:1, scaleX:1, scaleY:1, scaleZ:1});
			}
			else if (event.target == yellowColourCube.model)
			{
				Tweener.removeTweens(yellowColourCube.model);
				Tweener.addTween(yellowColourCube.model, {time:1, scaleX:1, scaleY:1, scaleZ:1});
			}
			else if (event.target == purpleColourCube.model)
			{
				Tweener.removeTweens(purpleColourCube.model);
				Tweener.addTween(purpleColourCube.model, {time:1, scaleX:1, scaleY:1, scaleZ:1});
			}
		}

		public function mouseDown(event:MouseEvent3D):void
		{
			// when the mouse is pressed over the main cube we set the
			// drawing flag to true
			drawing = true;
		}

		public function mouseUp(event:MouseEvent3D):void
		{
			// when the mouse is release or the pointer moves off the main cube
			// we set the drawing flag to false
			drawing = false;
		}

		public function mouseMove(event:MouseEvent3D):void
		{
			// check to see if we are drawing on the cube
			if (drawing)
			{
				// grab the material that is under the mouse pointer
				var material:TextureMaterial = event.surface.material as TextureMaterial;
				if (material != null)
				{
					// get the underlying BitmapData from the texture
					var bitmap:BitmapData = material.texture.bitmapData;
					// using the u and v coords supplied by the event draw some pixels to the BitmapData
					bitmap.fillRect(new Rectangle(event.u * bitmap.width, bitmap.height - (event.v * bitmap.height), 2, 2), colour);
				}
			}
		}
	}
}

The code that creates the interactive cubes exists in the ApplicationManager class. Take a look at the startupApplicationManager function. It’s here that we create the cubes and attach functions to the mouse events. Let’s break down how the grey “drawing” cube is created.

cube = new MeshObject().startupModelObject(new Box(100, 100, 100));

This code creates a MeshObject that hosts a standard Box primitive.

var surafceArray:Array = cube.model.surfaces.toArray(true);
for (var i:int = 0; i  < 6; ++i)
	cube.model.setMaterialToSurface(ResourceManager.cubeTexture[i],  surafceArray[i]);

Here we take the surfaces of the cube and add them into a standard array. Once in an array we loop over each surface and assign each its own texture. Giving each surface it’s own seperate texture allows us to draw on each surface individually later on.

Application.application.engineManager.cameraController.lookAt(cube.model.coords);

Now we set the camera to look at the drawing cube.

With the cube now created and added to the scene it is time to add some event listeners to it.

cube.model.addEventListener(MouseEvent3D.MOUSE_DOWN, mouseDown);

Here we listen for any mouse clicks that occur when the mouse pointer is over the cube.

cube.model.addEventListener(MouseEvent3D.MOUSE_UP, mouseUp);

And here we do the opposite, listening for when a mouse button is released over the cube.

cube.model.addEventListener(MouseEvent3D.MOUSE_OUT, mouseUp);

Here we listen for when the mouse pointer moves off the cube. You’ll notice that we have attached the same function (mouseUp) to this event as with the MOUSE_UP event. This is because, in the context of this demo, both the mouse up and mouse out events have the same outcome of stopping the user from drawing on the cube, so it makes sense to attach the same function to both events.

cube.model.addEventListener(MouseEvent3D.MOUSE_MOVE, mouseMove);

Finally we listen for any mouse movement events that occur over the cube.

As you can see the process of listening for mouse events that involve Alternativa objects is exactly the same as any standard GUI component. Once the View has been set to be interactive there is no special work that the developer has to do to listen for events other than a call to the addEventListener function.

Now that we have seen how to listen for events, let’s take a look at what happens when these events are fired.

public function mouseDown(event:MouseEvent3D):void
{
	// when the mouse is pressed over the  main cube we set the
	// drawing flag to true
	drawing = true;
}

This function sets the drawing flag to true, to indicate that we are drawing on the cube.

public function mouseUp(event:MouseEvent3D):void
{
	// when the mouse is release or the  pointer moves off the main cube
	// we set the drawing flag to false
	drawing = false;
}

And the opposite is done in the mouseUp function (which if you recall is called both when the mouse button has been released, and when the mouse pointer moves off the cube) by setting the drawing flag to false.

public function mouseMove(event:MouseEvent3D):void
{
	// check to see if we are drawing on  the cube
	if (drawing)
	{
		// grab the material that is  under the mouse pointer
  		var material:TextureMaterial = event.surface.material as TextureMaterial;
		if (material != null)
		{
			// get the  underlying BitmapData from the texture
  			var bitmap:BitmapData = material.texture.bitmapData;
			// using the u  and v coords supplied by the event draw some pixels to the BitmapData
			bitmap.fillRect(new Rectangle(event.u * bitmap.width, bitmap.height - (event.v * bitmap.height), 2,  2), colour);
		}
	}
}

The mouseMove function makes use of some of the event properties to determine exactly where to draw on the cube. The MouseEvent3D class has a wealth of information, but for this demo we are interested in the u,v coordinates, which are values between 0 and 1 that tell us where the event occurred as a relative position on the surface material. The MouseEvent3D class also lets us pull out the surface material that the event occurred on with event.surface.material. Armed with a reference to the material and the position, we can then get the underlying material BitmapData and modify it with a call to the standard fillRect function.

The process of listening for mouse events that involve the smaller colour cubes is exactly the same. You create the Alternativa object, attach some functions using addEventListener, and process the events inside the attached functions.

As you can see Alternativa makes it very easy to respond to mouse events, which allows a developer to create rich, interactive 3D Flash applications almost as if they were creating a plain old 2D GUI. And with the additional information contained in the MouseEvent3D class you can go one step futher and implement some neat effects that just aren’t possible in 2D.

Check out the online demo here, and download the source 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>