In this Tut, you will learn how to create a complete 3D Interface framework that you can easily modify and re-use. I promise, by the time you finish, you'll have a better understanding of Papervision3D, learn a handful of effects and apply them to PV3D.
You'll also have a good understanding of OOP and how classes work together in a medium scale project like this one. We'll start with the simplest class and progress to more complex ones. The project has a total of 17 classes working together. Also, the interface is set up so that you can easily plug-in any game that you've developed.
Also available in this series:
- Create a Stylish 3D Interface with Papervision3D: Structure
- Create a Stylish 3D Interface with Papervision3D: Effects and Submenus
Step 1: Create a New Project
Open FlashDevelop and click Project > New Project

Step 2: Set Up
Choose Actionscript 3 > AS3 Project.
For the name of the Project put in https://active.tutsplus.com/about/write-a-tutorial/
.
For the location, click and navigate to the folder you would like to save it into.
Leave the Create Directory For Project checkbox selected and click OK.

If you want to use Flash CS3+, create a new Flash file and set the width and height to 1000x700, set the background color to black, and set the frame rate to 40. Name it interface3D.fla
and save it anywhere you like.
Step 3: Preparing Assets for Use
For FlashDevelop, open the project directory and copy or drag soundAssets from the source download (linked at the top of the page) into the \bin\ folder. Inside the source download folder is another folder named actionscript. This folder contains all the Actionscript classes for the project. Except for the Millenia.as and the CustomLoader.as, the rest of the classes in this folder are the completed versions of what you will be writing.You can compare your work with them every time you finish a class you made from scratch following the tutorial. Drag the Millenia.as and the CustomLoader.as files into the \src\ folder where the Main.as class resides. Those 2 classes come as freebie. I'll discuss more about them later.
For Flash, copy or drag soundAssets folder from the source download into the same folder where you have interface3D.fla. Then drag the 2 classes mentioned above into the same folder.
Let's review the contents of the soundAssets folder. We have 10 sound files in MP3 format which will be used for sound effects.

Step 4: Install TweenLite & Papervision3D
We're going to use TweenLite for the tweening and Papervision for 3D. Both packages are included with the source download. For more info, you can visit their websites at Papervision and TweenLite.
For FlashDevelop, go ahead and copy or drag greensock.swc and the Papervision3D_2.1.932.swc from the Source download into the \lib\ folder for this project.
From FlashDevelop, click View > Project Manager

Step 5: External Libraries
Still in FlashDevelop, click the '+' sign to the left of the lib folder to expand it.
Select both swc files and right-click, choose Add to Library.

For Flash, copy or drag the \com\, \org\, and \nochump\ folders from the Source download into the same folder as your interface3D.fla file.
Step 6: Setting Up the Document Class
If you're using Flash CS3+, just follow along. Instructions for Flash CS3+ continue in step 8.
Now, in FlashDevelop, click view and choose Project Manager as you did in step 4. Once the Project Manager, go into the \src\ folder and double-click to open the Main.as class. This class is the document class for this project and is automatically generated for every new project. Below is the list of responsibilities it needs to accommodate for our application.
- load sound assets to use for effects using the CustomLoader class
- once all the sound assets are loaded, create and add the 3D menu into the stage
- listen for FullScreen events and inform UserInterface3D about it
- add some random sound effects to enhance the atmosphere
- listen for UserInterface3D.START_GAME to know when to load a sprite or a movieclip representing a game
- load the UserInterface3D back into the stage again when the game sprite has been removed
Note that for the 2nd responsibility of this class, we will first use a Tester class and then later on replace it with the UserInterface3D class. Tester will be used for creating and testing the 3D classes as you create them and UserInterface3D will put the them all together as one complete application.
Go inside the package declaration and replace all the imports with the list below:
import flash.display.Sprite; import flash.events.Event; import flash.events.FullScreenEvent; import flash.events.MouseEvent; import flash.events.TimerEvent; import flash.media.Sound; import flash.media.SoundTransform; import flash.system.Capabilities; import flash.text.TextField; import flash.text.TextFieldAutoSize; import flash.utils.Timer;
These imports will all be used by the time you complete this project. As for instance variables, let's start with 2, _timer which will be a Timer instance assigned with a random delay which is for playing sound effects to add atmosphere to our project, and _customLoader to hold the single instance of the CustomLoader class which will be used to load all the sound assets.
Add the following lines of code inside the Class declaration above the constructor method:
private var _customLoader:CustomLoader; private var _timer:Timer;
Step 7: Document Class Initialization
Still in FlashDevelop, right below where it says 'entry point' inside the init () method, add the following lines of code:
private function init (e:Event = null):void { removeEventListener(Event.ADDED_TO_STAGE, init); //entry point _customLoader = CustomLoader.getInstance (); _customLoader.addEventListener (CustomLoader.ALL_LOADS_COMPLETE, onLoad); _customLoader.addSound ('soundAssets/background2.mp3'); _customLoader.addSound ('soundAssets/staticNoise3.mp3'); _customLoader.addSound ('soundAssets/buttonShow.mp3'); _customLoader.addSound ('soundAssets/menuExpand.mp3'); _customLoader.addSound ('soundAssets/backgroundFlash.mp3'); _customLoader.addSound ('soundAssets/menuCollapse.mp3'); _customLoader.addSound ('soundAssets/buttonOver.mp3'); _customLoader.addSound ('soundAssets/buttonFlash.mp3'); _customLoader.addSound ('soundAssets/buttonClick.mp3'); _customLoader.addSound ('soundAssets/randomEcho.mp3'); _customLoader.startLoading (); }
For those of you not familiar with the CustomLoader class, It is mainly used to load visual and sound assets. It is based on the Singleton Pattern to make sure there is only 1 instance that will hold all the external assets for the project. My other tutorial for Creating Intros with PV3D linked here, has detailed information for building this class.
Its list of responsibilities are:
- load external assets (display and sound assets).
- give out a list of names of all the assets that were loaded.
- dispatch a 'CustomLoader.ALL_LOADS_COMPLETE' event when all the assets have loaded successfully.
- provide the asset when requested.
- if something goes wrong during loading, send out the error.
To start using the CustomLoader class, we assign _customLoader an instance returned by calling the CustomLoader.getInstance () static method. As soon as the _customLoader instance has been created, it's important to add a 'CustomLoader.ALL_LOADS_COMPLETE' listener to it. This will inform us when all the assets have loaded successfully. To add a visual asset like an image to the queue for loading, just call the addItem () method and pass in the url as parameter. For sound assets, call addSound () and also pass in the url as parameter.
Once all the assets you want to load have been added to the queue, call the startLoading () method. The names for each sound/image asset are automatically generated by getting its short name. For example: if you load 'soundAssets/menuCollapse.mp3', the name to access this sound asset will be 'menuCollapse'. To get the list of all the names of all the assets (images and sounds), just call _customLoader.names like trace (_customLoader.names) and all the names will print as an array of names. To access images, call the _customLoader.getItem () method and just pass in the short name in string format. For sounds, call _customLoader.getSound () and pass in the short name also in string format. We will only need to load sound assets for this project since all the graphics will be generated by Papervision3D.
Step 8: Testing the CustomLoader Instance
For a simple test, let's play a random sound asset by accessing all the names of the assets currently stored by the CustomLoader instance. Add the following lines of code below the init () method:
private function onLoad (e:Event):void { var soundArray:Array = new Array; var namesArray:Array = CustomLoader (e.target).names; for (var i:uint = 0; i < namesArray.length; i++) { var sound:Sound = CustomLoader (e.target).getSound (nameArray[i]) if (sound != null) soundArray.push (sound); } soundArray[int (Math.random() * soundArray.length)].play (); }
Now when you run the project, you should hear a random sound played. Run it a few more times and you're likely to hear a different sound each time.
For Flash, Create a new Class named 'Main' where you saved your interface3D.fla and use it as the Document class. The imports are usually generated by FlashDevelop automatically but since you're using Flash to create your classes, just add them all now and you'll see what they're used for later on. Copy the code below and replace all the code inside the new class:
package { import flash.display.Sprite; import flash.events.Event; import flash.events.FullScreenEvent; import flash.events.MouseEvent; import flash.events.TimerEvent; import flash.media.Sound; import flash.media.SoundTransform; import flash.system.Capabilities; import flash.text.TextField; import flash.text.TextFieldAutoSize; import flash.utils.Timer; public class Main extends Sprite { private var _customLoader:CustomLoader; private var _timer:Timer; public function Main ():void { if (stage) init (); addEventListener (Event.ADDED_TO_STAGE, init); } public function init (e:Event = null):void { removeEventListener (Event.ADDED_TO_STAGE, init); _customLoader = CustomLoader.getInstance(); _customLoader.addEventListener (CustomLoader.ALL_LOADS_COMPLETE, onLoad); _customLoader.addSound ('soundAssets/background2.mp3'); _customLoader.addSound ('soundAssets/staticNoise3.mp3'); _customLoader.addSound ('soundAssets/buttonShow.mp3'); _customLoader.addSound ('soundAssets/menuExpand.mp3'); _customLoader.addSound ('soundAssets/backgroundFlash.mp3'); _customLoader.addSound ('soundAssets/menuCollapse.mp3'); _customLoader.addSound ('soundAssets/buttonOver.mp3'); _customLoader.addSound ('soundAssets/buttonFlash.mp3'); _customLoader.addSound ('soundAssets/buttonClick.mp3'); _customLoader.addSound ('soundAssets/randomEcho.mp3'); _customLoader.startLoading (); } private function onLoad (e:Event):void { var soundArray:Array = new Array ; var namesArray:Array = CustomLoader(e.target).names; for (var i:uint = 0; i < namesArray.length; i++) { var sound:Sound = CustomLoader(e.target).getSound(namesArray[i]); if (sound != null) { soundArray.push (sound); } } soundArray[int (Math.random() * soundArray.length)].play (); } } }
Now run the application by hitting CTRL + ENTER.
Great! Now we know the CustomLoader works! Let's move on.
Step 9: Generating Sound Effects to Add Atmosphere
Replace the contents of the onLoad () Method with the code below:
var sound:Sound = _customLoader.getSound ('background2'); sound.play (0, 100); createRandomNoise ();
Here, we assign a local variable 'sound' an instance of the background2.mp3 and play it 100 times. Then the createRandomNoise () method is called. Add the following code below the onLoad () method:
private function createRandomNoise ():void { _timer = new Timer (getRandomInRange (5000,20000, false), 1); _timer.addEventListener (TimerEvent.TIMER_COMPLETE, onTimerComplete); _timer.start (); }
Add the onTimerComplete() Method below the createRandomNoise () Method.
private function onTimerComplete(e:TimerEvent):void { var sound:Sound = _customLoader.getSound ('randomEcho'); sound.play (0, 0, new SoundTransform (Math.random () * .7)); createRandomNoise (); }
To complete the class add the getRandomInRange () static method. This useful method is declared static to maximize its accessibility. It simply generates a random number between $min and $max, and returns it:
- rounded if the 3rd parameter is left as default,
- as a float if the 3rd parameter is set to false.
Add the following lines of code below the onTimerComplete () method:
public static function getRandomInRange ($min:Number, $max:Number, $rounded:Boolean = true):Number { if ($rounded) return Math.round (Math.random () * ($max - $min) + $min); else return Math.random () * ($max - $min) + $min; }
All the createRandomNoise () method does is assign a new Timer instance to _timer with a random interval between 5 and 20 seconds. An onTimerComplete listener is then triggered each time the Timer completes and this is where the 'randomEcho' sound is played and as you can see, the sound generated has variable volume between 0 and .7. It then calls the createRandomNoise () method again to start a new Timer. This creates some sort of a random sound loop with varying interval. Go ahead and run a test if things are working. If everything went well, you should hear the sound effects play in the background when you run the program.
Save your work and let's move on to creating 3D objects.
Step 10: Creating the Tester Class
If you're using Flash CS3+, again, just follow along. Instructions for Flash CS3+ will continue in Step 13.
As mentioned earlier, this class will be used for testing 3D objects. This class will inherit from BasicView which makes of a quick 3D template ready for use. In FlashDevelop, click View > Project Manager, right-click the \src\ folder and choose Add > New Class.

The New Actionscript Class window will pop-up. For the Name enter 'Tester', for the Base Class, click browse and enter 'org.papervision3d.view.BasicView'. Click "OK" to finish.

This is a simple class. Its responsibilities are to:
- Add simple 3D perspective.
- Create simple 3D object as prototype as starting point for creating custom DisplayObject3D classes.
- Test each custom DisplayObject3D created as we create them.
Add the following imports inside the package brackets before the class declaration:
import flash.events.Event; import org.papervision3d.core.math.Number2D; import org.papervision3d.events.InteractiveScene3DEvent; import org.papervision3d.materials.special.Letter3DMaterial; import org.papervision3d.materials.special.VectorShapeMaterial; import org.papervision3d.objects.DisplayObject3D; import org.papervision3d.objects.special.Graphics3D; import org.papervision3d.objects.special.VectorShape3D; import org.papervision3d.typography.Font3D; import org.papervision3d.typography.Text3D; import org.papervision3d.view.BasicView; import org.papervision3d.view.layer.ViewportLayer;
Step 11: Tester Instance Variables
Now add the following private instance variables inside the Class declaration:
private var _rotX:Number = .1; private var _rotY:Number = .1; private var _camPitch:Number = 90; private var _camYaw:Number = 270; private var _easOut:Number = .1;
These 5 variables will be used to move the camera around in an orbiting manner. The objects added to the scene will not move but will look so since the camera will be the one moving around them. We'll add other variables as we develop their corresponding DisplayObject3D classes. When we get to the UserInterface3D, we'll have the objects rotate and hold the camera at a stationary position instead.
Step 12: Tester's Constructor
The constructor will call 2 methods to start things, first, the init () method which will handle general initialization and second, startRendering () which will start Papervision's 3D rendering engine. Add the following lines of code inside the Constructor Method:
init (); startRendering ();
Step 13: Adding 3D Display Objects
This method will take care of initializing the 3D objects we're about to create . For now let's add a simple DisplayObject3D into to the scene as a warm up. Add the following lines of code below the Tester Constructor method:
private function init ():void { addSimpleDO3D (); }
The addSimpleDO3D () Method will add a 3D vector graphic along with 3D text into the scene. This will serve as the prototype for the custom classes we will make soon. Add the code below the init () method:
private function addSimpleDO3D ():void { //creating 3d vector shape var material:VectorShapeMaterial = new VectorShapeMaterial (); var vs3D:VectorShape3D = new VectorShape3D (material); var g:Graphics3D = vs3D.graphics; g.beginFill (0xFF000); g.lineStyle (1, 0x0000FF); g.moveTo ( -199, 106); g.lineTo (-199,-106); g.lineTo (199,-106); g.lineTo (199,106); g.lineTo (-199,106); g.endFill (); var vLayer:ViewportLayer = viewport.getChildLayer (vs3D); vLayer.alpha = .6; //creating 3d text var nameString:String = 'sample'; var letterMaterial:Letter3DMaterial = new Letter3DMaterial (0x00FF00); var font3D:Font3D = new Millennia; var text3D:Text3D = new Text3D (nameString, font3D, letterMaterial, nameString); text3D.x = -650; text3D.y = 10;//we use this y setting for the font Millenia text3D.z = -25; text3D.align = 'left'; var tLayer:ViewportLayer = viewport.getChildLayer (text3D); tLayer.alpha = .6; //using an empty DisplayObject3D to contain both 3d graphics and text var do3D:DisplayObject3D = new DisplayObject3D; do3D.addChild (vs3D); do3D.addChild (text3D); //adding the DisplayObject3D containing both vector graphic and text into the scene scene.addChild (do3D); }
First, we create a VectorShape3D, this Class is Papervision's 3D version of Flash's Vector graphic. To make this 3D graphic asset, we need an instance of the VectorShapeMaterial to use as its material. Once a VectorShape3D has been instantiated, we can then access its Graphics3D property for drawing.
Using Graphics3D is very similar to using Flash's native Graphics class. Here we assign a local variable named 'material' of type VectorShapeMaterial a new instance. Another local variable named 'vs3D' of type VectorShape3D is then instantiated assigning 'material' as its parameter. To start drawing, a local variable named 'g' is assigned the VectorShape3D's Graphics3D property. The color is set by calling Graphics3D's method beginFill () and passing in the color 0xFF0000 (red). Additionally, you can pass in a value between 0 and 1 as the second parameter to set the alpha property. We set the Graphics3D line property by calling its lineStyle () method and passing in 2 parameters to set its thickness and color properties respectively. Here we set the thickness to 1 and the color to 0x0000FF (blue). Drawing is then started by calling Graphics3D's moveTo method and passing in the starting x and y positions. Here we start at the left-top corner -199 units to the left of 'x' axis center and 106 units up the center y axis. A call to Graphics3D's lineTo () method is then repeated 4 times to draw a square VectorShape3D. The shape is drawn by creating a line from the starting point (-199,106) going down (-199,-106), then to the right (199, -106), and then up (199,106) and finally completing the square, back to (-199,106).
To apply alpha transparency of .6 to the VectorShape3D object, we assign a local property named vLayer of type ViewportLayer, the ViewportLayer of the VectorShape3D instance. We do this by calling Viewport.getChildLayer () method and passing in the instance of VectorShape3D assigned to vs3D, this parameter takes in DisplayObject3D objects. Once assigned, you can then apply effects for the 3D asset just like you would on a regular display object.
The next thing we do is add 3D text. This is where we will first use the imported Millenia.as class. This class is a 3D font created using Mathuie Badimon's 'Make a typography file' version 2.0 software. You can get more information about it here. To create 3D vector text, we'll need to instantiate 4 properties and pass them as parameters when the 3D text is instantiated:
- The 1st is the text it will write, this is in String fomat. Here we pass it the nameString variable.
- Next, letterMaterial of type LetterMaterial. When instantiating this class, here we pass in the color of 0x00FF00 (green). If left alone, the default parameters are pink for the color and 1 for the alpha (full opacity).
- The 3rd variable font3D of type Font3D is assigned an instance of the Millenia.as class.
- The 4th and optional parameter is the name parameter, used as the name for the Text3D instance. This too is in String format.
Here, we create a new Text3D and pass it nameString to use for its text, font3D for the its font, letterMaterial for its material, and nameString again for its last and optional parameter, its name property. All we need to do now is position it, set its align property, and add it into the scene. Since we are using Millenia.as font for this project, we will set Text3D's align property to 'left', its 'x' position is set to 650 to center it in the 'x' axis, the 'y' position is assigned 10 to center its 'y' axis, finally the 'z' position is set to -25 to make it appear in front of the VectorShape3D.
To apply alpha transparency, we will use the same method as we did for the VectorShape3D we created earlier. A local variable named tLayer of type ViewportLayer is assigned text3D's ViewportLayer. Again, this is done by calling Viewport's instance method 'getChildLayer' and passing in text3D as parameter. The alpha value of .6 is then set for tLayer. This is only way to apply filters and effects to anything that inherits from DisplayObject3D. As you will see later when we cover effects more extensively.
Here, we create a new Text3D and pass it nameString to use for its text, font3D for the its font, letterMaterial for its material, and nameString again for its last and optional parameter, its name property. All we need to do now is position it, set its align property, and add it into the scene. Since we are using Millenia.as font for this project, we will set Text3D's align property to 'left', its 'x' position is set to 650 to center it in the 'x' axis, the 'y' position is assigned 10 to center its 'y' axis, finally the 'z' position is set to -25 to make it appear in front of the VectorShape3D. To apply alpha transparency, we will use the same method as we did for the VectorShape3D we created earlier. A local variable named tLayer of type ViewportLayer is assigned text3D's ViewportLayer. Again, this is done by calling Viewport's instance method 'getChildLayer' and passing in text3D as parameter. The alpha value of .6 is then set for tLayer. This is only way to apply filters and effects to anything that inherits from DisplayObject3D. As you will see later when we cover effects more extensively.
To keep things organized, we'll use an empty DisplayObject3D to house both VectorShape3D and Text3D and then add that into the scene. A local variable named do3D of type DisplayObject3D is created, adds vs3D and text3D into it and then adds it to the scene.
Go back to your Main.as class and add the code below inside the onLoad () method after the createRandomNoise () method call:
var tester:Tester = new Tester; addChild (tester);
Now run it to view the product. It should look like the cut-out below with the sound effects playing in the background. If you're using Flash, just add the code above inside Main.as and follow along to the end of this step.

Let's add 1 last method before moving on to creating our first custom DisplayObject3D class. This will take care of moving the camera around and give us that 3D perspective. Add the following lines of code below the addSimpleDO3D () method:
override protected function onRenderTick (event:Event = null):void { var xDist:Number = mouseX - stage.stageWidth * .5; var yDist:Number = mouseY - stage.stageHeight * .5; _camPitch += ((yDist * _rotX) - _camPitch + 90) * _easOut; _camYaw += ((xDist * _rotY) - _camYaw + 270) * _easOut; camera.orbit (_camPitch, _camYaw); super.onRenderTick(event); }
This method is from BasicView's superclass AbstractView. It's triggered on every enterFrame event once the startRendering () method is called as we did inside the init () method.
Now, when you run a test, you'll see that the camera is now orbiting your DisplayObject3D. It might look like the object is moving but its actually the camera that's rotating around the center of the scene. In this method, the main function is the Camera.orbit () method. To use this method, xDist & yDist are assigned the distances of the mouseX & mouseY from the center of the scene basing it from the width and height of the stage. The instance variable _camPitch of type Number is used as the value to orbit the camera on the 'y' axis (moving left or right). This is calculated by first multiplying yDist with _rotX and then subtracting the current value of _camPitch plus 90. Adding 90 adjusts the camera so that when the mouse is on center, the camera is also looking straight at the center of the stage. _easOut is then multiplied to the overall value to apply easing to the camera's movement. The same concept applies with _camYaw which has the effect applied to the camera's 'x' axis (moving up or down). We add 270 to center the camera when the mouse is centered on the stage. The two variables are then passed into the Camera.orbit () method to move the camera. Lastly, super.onRenderTick () is called at the end of the method so that the super classes can render the 3D scene.
OK, run another test and see some 3D movement going on!
Whoa! Did you see that?! I'm not talking about the 3D perspective. When you move your mouse to the left of the stage, do you notice how the Text3D went to the back of the VectorShape3D? You've got a 'Z' sorting issue. Don't worry, we have a simple solution which we'll add to the UIComponent3D class we're creating next. Save your work before going to the next step.
If you're using Flash, apply the changes mentioned earlier for the Main document class and continue here. Create a new class named Tester.as, save it in the same folder where you have interface3D.fla file. Copy the code below into it, save it again and then run it.
package { import flash.events.Event; import org.papervision3d.core.math.Number2D; import org.papervision3d.events.InteractiveScene3DEvent; import org.papervision3d.materials.special.Letter3DMaterial; import org.papervision3d.materials.special.VectorShapeMaterial; import org.papervision3d.objects.DisplayObject3D; import org.papervision3d.objects.special.Graphics3D; import org.papervision3d.objects.special.VectorShape3D; import org.papervision3d.typography.Font3D; import org.papervision3d.typography.Text3D; import org.papervision3d.view.BasicView; import org.papervision3d.view.layer.util.ViewportLayerSortMode; import org.papervision3d.view.layer.ViewportLayer; public class Tester extends BasicView { private var _rotX:Number = .1; private var _rotY:Number = .1; private var _camPitch:Number = 90; private var _camYaw:Number = 270; private var _easOut:Number = .1; public function Tester () { init (); startRendering (); } private function init ():void { addSimpleDO3D (); } private function addSimpleDO3D ():void { //creating 3d vector shape var material:VectorShapeMaterial = new VectorShapeMaterial (); var vs3D:VectorShape3D = new VectorShape3D (material); var g:Graphics3D = vs3D.graphics; g.beginFill (0xFF0000); g.lineStyle (1, 0x0000FF); g.moveTo ( -199, 106); g.lineTo (-199,-106); g.lineTo (199,-106); g.lineTo (199,106); g.lineTo (-199,106); g.endFill (); var vLayer:ViewportLayer = viewport.getChildLayer (vs3D); vLayer.alpha = .6; //creating 3d text var nameString:String = 'sample'; var letterMaterial:Letter3DMaterial = new Letter3DMaterial (0x00FF00); var font3D:Font3D = new Millennia; var text3D:Text3D = new Text3D (nameString, font3D, letterMaterial, nameString); text3D.x = -650; text3D.y = 10;//we use this y setting for Millenia text3D.z = -25; text3D.align = 'left'; var tLayer:ViewportLayer = viewport.getChildLayer (text3D); tLayer.alpha = .6; //using an empty DisplayObject3D to contain both 3d graphics and text var do3D:DisplayObject3D = new DisplayObject3D; do3D.addChild (vs3D); do3D.addChild (text3D); //adding the DisplayObject3D containing both vector graphic and text into the scene scene.addChild (do3D); } override protected function onRenderTick (event:Event = null):void { var xDist:Number = mouseX - stage.stageWidth * .5; var yDist:Number = mouseY - stage.stageHeight * .5; _camPitch += ((yDist * _rotX) - _camPitch + 90) * _easOut; _camYaw += ((xDist * _rotY) - _camYaw + 270) * _easOut; camera.orbit (_camPitch, _camYaw); super.onRenderTick(event); } } }
If you run into problems, just compare your code with the completed classes included with the source download.
Step 14: Creating a Custom DisplayObject3D Base Class - UIComponent3D
What this class needs to do is simplify adding custom drawn VectorShape3D with optionally visible text3d into the scene. It will also be in charge of 'z' sorting. This will be part of the solution we will discuss later. It will add, remove, and dispatch InteractiveScen3DEvents, draw the 3D vectors and calculate their dimensions. This class will serve as the Super class for all the different visual assets (including buttons) used in this project. Here is the list of its responsibilities:
- provide width and height properties translated into PV3D's unit measurement system.
- draw custom vectorshape3d and access its ViewportLayer to apply effects.
- add text3D, also access its ViewportLayer to apply effects.
- organize z sorting.
- add listeners, remove listeners, and dispatch InteractiveScene3DEvents.
For FlashDevelop, create a new class, name it UIComponent3D and have it extend DisplayObject3D. See Step 10 for a refresher.
For Flash, create the class inside the folder you've been using for interface3D.fla.
Select all of the code inside the class and replace it with the lines below:
package { import org.papervision3d.core.math.Number2D; import org.papervision3d.events.InteractiveScene3DEvent; import org.papervision3d.materials.special.Letter3DMaterial; import org.papervision3d.materials.special.VectorShapeMaterial; import org.papervision3d.objects.DisplayObject3D; import org.papervision3d.objects.special.Graphics3D; import org.papervision3d.objects.special.VectorShape3D; import org.papervision3d.typography.Font3D; import org.papervision3d.typography.Text3D; import org.papervision3d.view.layer.ViewportLayer; import org.papervision3d.view.Viewport3D; public class UIComponent3D extends DisplayObject3D { protected var _coordinates:Array; //holds an array of Number2D used to draw the VectorShape3D protected var _fillColor:uint = 0x2D2D2D; //the fillColor to use for the VectorShape3D protected var _fillAlpha:Number = .6; //the fillAlpha to use for the VectorShape3D protected var _lineColor:uint = 0x007500; //the lineColor to use for the VectorShape3D protected var _lineAlpha:Number = 0; //the lineAlpha to use for the VectorShape3D protected var _lineThickness:Number = 2; //the lineThickness to use for the VectorShape3D protected var _layerIndex:Number = 1; //part of 'z' sorting protected var _viewport:Viewport3D; //part of accessing the ViewportLayer of a DisplayObject3D protected var _interfaceGraphic:VectorShape3D; //the drawing protected var _interfaceLayer:ViewportLayer; //the layer of the VectorShape3D to apply filters and effects protected var _text3D:Text3D; //the 3d text protected var _textLayer:ViewportLayer; //the layer of the Text3D to apply filters and effects protected var _width:Number; //used by Button3D subclass - you'll see more about this later protected var _height:Number; //same as above protected var _showText:Boolean; //if set to true during instantiation, Text3D will be added into the display list public function UIComponent3D ($name:String, $viewport:Viewport3D, $coordinates:Array, $showText:Boolean = false) { super ($name); _viewport = $viewport; _coordinates = $coordinates; _showText = $showText; } } }
After the package declaration comes all the required class imports. Then comes the class declaration. Below that, you see all the protected variables needed for its functionality. Comments are listed to the right of each variable for usage description. Then comes the constructor. The first 3 parameters are required followed by an optional 4th parameter. Its important to have a name for each DisplayObject3D for reference. The $viewport parameter is needed to access the ViewportLayer of the Text3D and VectorShape3D which will have effects applied to. The coordinates parameter is an array of Number2D used for drawing. The 4th parameter allows to show or hide the 3D text.
The constructor calls super () to assign the name for the DisplayObject3D and the rest are custom properties are stored for each UIComponent3D instance.
Step 15: UIComponent3D Visual Customization
Once an instance of UIComponent3D has been created, we can then customize the VectorShape3D's properties before drawing. Add the following lines of code after the constructor:
public function set fillColor($value:uint):void { _fillColor = $value; } public function set fillAlpha($value:Number):void { _fillAlpha = $value; } public function set lineColor($value:uint):void { _lineColor = $value; } public function set lineAlpha($value:Number):void { _lineAlpha = $value; } public function set lineThickness($value:Number):void { _lineThickness = $value; }
These are all implicit setter methods. You can read more about these methods, including their counterparts here. You'll see them in action later.
Step 16: UIComponent3D Initialization
Once we've set up the visual properties, we can then initialize the UIComponent3D instance externally. An example will show why we have this set as public methods. Here, setDimensions () is called to set its width and height. Afterwards the createInterface () method is called to draw the VectorShape3D and finally, createText () is called to create the Text3D. Add the following method above the getter methods:
public function init ():void { setDimensions (); createInterface (_coordinates); createText (); }
Step 17: Calculating UIComponent3D's Width and Height Properties
We need to get these properties to create an interactive plane later when we extend this for our Button3D class.As you know, Papervision DisplayObjects don't have width and height properties so we're adding them as custom instance properties inherited by all of the subclasses. For the width, we use simple math and add the farthest 'x' position to the left of center with the farthest 'x' position to the right. The same applies for the height. Add the code after the init () method:
protected function setDimensions ():void { var minWidth:Number = 0, minHeight:Number = 0, maxWidth:Number = 0, maxHeight:Number = 0; for each (var n2d:Number2D in _coordinates) { minWidth = n2d.x < minWidth ? n2d.x : minWidth; minHeight = n2d.y < minHeight ? n2d.y : minHeight; maxWidth = n2d.x > maxWidth ? n2d.x : maxWidth; maxHeight = n2d.y > maxHeight ? n2d.y : maxHeight; } _width = Math.abs (minWidth) + maxWidth; _height = Math.abs (minHeight) + maxHeight; }
To have these protected properties accessible externally, let's add implicit getters for them as well. Add the methods below above the other implicit getters at the bottom of the class:
public function get width ():Number { return _width; } public function get height ():Number { return _height; }
Step 18: Drawing the VectorShape3D
This method is called next inside the init () method. It takes care of adding the 3D graphics into UIComponent3D and assigns its ViewportLayer to _interfaceLayer for applying effects later. Remember the addSimpleDO3D () method in the Tester class. Plus now it takes in an array of Number2D coordinates to use for drawing. Add the following lines of code below setDimensions ():
protected function createInterface ($array:Array):void { var material:VectorShapeMaterial = new VectorShapeMaterial (); _interfaceGraphic = new VectorShape3D (material); var g:Graphics3D = _interfaceGraphic.graphics; g.beginFill (_fillColor, _fillAlpha); g.lineStyle (_lineThickness, _lineColor, _lineAlpha); for (var i:uint = 0; i < $array.length; i++) { if (i == 0) g.moveTo ($array[i].x, $array[i].y); else g.lineTo ($array[i].x, $array[i].y); } g.endFill (); _interfaceLayer = _viewport.getChildLayer (_interfaceGraphic); setLayerIndex (_interfaceLayer, _layerIndex); addChild (_interfaceGraphic); }
Inside the method is a call to setLayerIndex () method, its part of fixing our 'z' sorting issue. What it does is assign the passed in ViewportLayer parameter the layerIndex of the passed in index parameter. Add it below the end of createInterface () method.
protected function setLayerIndex ($layer:ViewportLayer, $index:Number):void { $layer.layerIndex = $index; }
Step 19: Adding Text3D
This was also part of the addSimpleDO3D () method, we just it separated for more control and to keeps things simple. Add the code above the setLayerIndex () method:
protected function createText ():void { var material:Letter3DMaterial = new Letter3DMaterial (0xDDFFFF); var font3D:Font3D = new Millennia; _text3D = new Text3D (name, font3D, material, name); _textLayer = _viewport.getChildLayer (_text3D); setLayerIndex (_textLayer, _layerIndex + 1); //assign to the front _text3D.x = -850; _text3D.y = 10; _text3D.z = -25; _text3D.align = 'left'; if (_showText == true) addChild (_text3D); }
The setLayerIndex () method is called again, this time passing in the Text3D's ViewportLayer and is assigned the value of _layerIndex + 1. With this the Text3D will always be drawn in front of the VectorShape3D. Also, notice how we check _showText is set to true, this is set to false by default as the last parameter for the Constructor method.
Step 20: Adding and Removing InteractiveScene3DEvent Listeners and Dispatching
For Papervision, this is the event you want to listen to for interactivity. Add the following codes below the setLayerIndex () method:
protected function addMouseListeners ($do3D:DisplayObject3D):void { $do3D.addEventListener (InteractiveScene3DEvent.OBJECT_OVER, onObjectOver); $do3D.addEventListener (InteractiveScene3DEvent.OBJECT_OUT, onObjectOut); $do3D.addEventListener (InteractiveScene3DEvent.OBJECT_CLICK, onObjectClick); } protected function removeMouseListeners ($do3D:DisplayObject3D):void { $do3D.removeEventListener (InteractiveScene3DEvent.OBJECT_OVER, onObjectOver); $do3D.removeEventListener (InteractiveScene3DEvent.OBJECT_OUT, onObjectOut); $do3D.removeEventListener (InteractiveScene3DEvent.OBJECT_CLICK, onObjectClick); } public function onObjectOver(e:InteractiveScene3DEvent = null):void { dispatchEvent (new InteractiveScene3DEvent (e.type, e.target as DisplayObject3D)); } public function onObjectOut(e:InteractiveScene3DEvent = null):void { dispatchEvent (new InteractiveScene3DEvent (e.type, e.target as DisplayObject3D)); } public function onObjectClick(e:InteractiveScene3DEvent = null):void { dispatchEvent (new InteractiveScene3DEvent (e.type, e.target as DisplayObject3D)); }
The first 2 simplifies adding and removing MouseOver, MouseOut, and MouseClick events. The last 3 takes care of dispatching them. These all help the extending subclass - Button3D. I've decided to put these methods inside UIComponent3D to keep Button3D focused on its other responsibilities.
Step 21: External Access to ViewportLayers for Effects and Filters
Add the methods below to allow other classes to apply effects for each UIComponent3D. This will be used by the DistortionEffect3D you will create later.
public function get interfaceLayer ():ViewportLayer { return _interfaceLayer; } public function get textLayer ():ViewportLayer { return _textLayer; }
Step 22: External Access to Text3D for Positioning
We'll have to manually adjust the position for each Text3D when we get to creating a couple of descendant classes extending Button3D. Add the code below after the get textLayer () method:
public function get text3D():Text3D { return _text3D; }
This completes our UIComponent3D class! Save it before going for the next step.
Step 23: Milestone Testing out UIComponent3D
Go back to the Tester class and comment out the addSimpleDO3D () method call. Next, add addCustomDO3D () method call below it. Then add the method listed below after the addSimpleDO3D () method:
private function addCustomDO3D ():void { _uic = new UIComponent3D ( 'sample', viewport, [new Number2D ( -199, 106), new Number2D ( -199, -106), new Number2D (199, -106), new Number2D (199, 106), new Number2D ( -199, 106)], true ); _uic.init (); _uic.text3D.x = -650;//center the text visually scene.addChild (_uic); }
Remember how we set up UIComponent3D's constructor? We have 3 required parameters and 1 optional which defaults to false. The first parameter is used as both the name and text. The second is the Viewport3D property of BasicView to access ViewportLayers for UIComponent3D. The third is an array of Number2D's which is used to draw the VectorShape3D. And last, the optional parameter $showText, which here we set to true. The next thing we need to after instantiating UIComponent3D is call its init () method. Then we position its Text3D and add it into the scene. The visual properties are what we set as default. To change color and transparency for both fill and line style, just assign them before calling the init () method. We'll apply them later as we build more complex DisplayObject3D's like MainMenu3D. The Text3D doesn't have any way of changing colors since we don't need it here but you can always add another optional parameter for UIComponent3D's constructor for this. Declare _uic as an instance property of type UIComponent3D and then run the program.
Your result should be similar to the image below.

Ok. It works. But we still have that 'z' sorting issue even when we're already managing the layerIndex for our VectorShape3D and Text3D. For the layerIndex 'z' sorting to take effect, we need to change the viewport's containerSprite's sort mode to index sort. Go into Tester's init () method and add the code below before anything else.
viewport.containerSprite.sortMode = ViewportLayerSortMode.INDEX_SORT;
Make sure the following import is included inside Tester's package before running the app again.
import org.papervision3d.view.layer.util.ViewportLayerSortMode;
Alright! That takes care of the 'z' sorting issue. Again, if you're having bugs, check your code against the completed classes included with the source download. If all went well, let's start building the Button3D class.
Step 24: Creating Button3D
For Button3D, instead of having it simply dispatch mouse events then extending it to add effects, we'll do it all in one go. Also, this is part of the reason we added the listeners to Button3D's super class - UIComponent3D. It inherits from UIComponent3D then adds its own functionality. Here are the list of its responsibilities:
- play sound effects for mouse over and mouse click.
- add an interactive plane that will trigger the mouse events.
- apply effects for mouse over, mouse out, and mouse click.
- provide external control for adding and removing mouse event listeners.
- provide external control to remove mouse click listeners utilized when we create buttons that only listen for mouse over and mouse out to scroll Text3D as you saw in the About menu.
Create a new class and name it Button3D. Have it extend UIComponent3D and copy the code below into it to start things. I expect that you already know how to create new classes regardless which editor you are using.
package { import com.greensock.TweenMax; import flash.events.TimerEvent; import flash.filters.BlurFilter; import flash.filters.GlowFilter; import flash.media.Sound; import flash.media.SoundTransform; import flash.utils.Timer; import org.papervision3d.core.math.Number2D; import org.papervision3d.events.InteractiveScene3DEvent; import org.papervision3d.materials.ColorMaterial; import org.papervision3d.objects.DisplayObject3D; import org.papervision3d.objects.primitives.Plane; import org.papervision3d.view.Viewport3D; public class Button3D extends UIComponent3D { public static const OVER_POSITION:Number = -50; public static const OUT_POSITION:Number = 0; protected var _plane:Plane; //will work as an interactive object that will listen for mouse events protected var _buttonOverSound:Sound //played when the mouse is over this button protected var _buttonClickSound:Sound; //played when the mouse clicks this button protected var _soundTransform:SoundTransform; //used for volume control protected var _noClick:Boolean; //for subclass use (scroller) public function Button3D ($name:String, $viewport:Viewport3D, $coordinates:Array = null, $showText:Boolean = true) { //start here } } }
As usual, the required imports are at the top of the page as part of the package. Inside the class declaration, the first 2 properties are declared as public static constants since both will be used by external and descendant classes. They're for 'z' positioning for for the button. The other 5 are instance variables accessible for subclasses. See the comments for their use. The constructor method takes in the same parameters as the superclass, this time setting the 3rd parameter also optional. The 4th parameter is also modified to show the text by default.
Step 25: Button3D Constructor
Add the code below inside the constructor method:
if ($coordinates == null) _coordinates = [ new Number2D (-430, 65), new Number2D (-450, 35), new Number2D (-450, -35), new Number2D (-430, -65), new Number2D (400, -65), new Number2D (400, 5), new Number2D (450, 5), new Number2D (450, 65), new Number2D (-430, 65) ] else _coordinates = $coordinates; //if $coordinates is not null, assign it instead super ($name, $viewport, _coordinates, $showText); var customLoader:CustomLoader = CustomLoader.getInstance (); _soundTransform = new SoundTransform (.1); _buttonOverSound = customLoader.getSound ('buttonOver'); _buttonClickSound = customLoader.getSound ('buttonClick');
The first thing it does is check if $coordinates value is null, if it is, it assigns its own set. So whenever an instance of this class is created with the 3rd parameter set to null, it will have a default set of coordinates to draw. (I've included a simple drawing tool with the tutorial which you can use to generate these coordinates.)
A call to super () method is then made passing in the assigned values which is handled by UIComponent3D. The CustomLoader instance is then accessed for the sound effects used for mouse events. The volume of the sounds we're using is a little loud so we compensated with the SoundTransform class.
Step 26: Adding Functionality to the createInterface () Method
Aside from drawing the VectorShape3D, we also want to add a plane that will listen to mouse events here in Button3D. We can do that by overriding the createInterface () method inherited from UIComponent3D and apply new functionality to it. Add the code below after the constructor:
override protected function createInterface($array:Array):void { super.createInterface($array); addInteractivePlane (); }
This is the best example to show the effectiveness of OOP. By overriding the inherited method and then just adding to it, we avoid duplicate code. All we did here is call super.createInterface (), passing in the parameter to have UIComponent3D do its thing and then calling addInteractivePlane () to have Button3D add a plane to use for interactivity. Add the code below after the createInterface () method:
protected function addInteractivePlane ():void { _plane = new Plane (new ColorMaterial (0, 0, true), _width, _height); _plane.x = _interfaceGraphic.x; addChild (_plane); activate (); }
The method is simple. It assigns a new plane with the color of 0 (black) and an alpha of 0 to make it completely transparent. This is where we get to use the _width and _height properties inherited from UIComponent3D. To create a plane, we simply instantiate an instance by passing in 3 parameters, the color material, which we set to black with full transparency, then _width and _height. Once created, it's then placed in the same 'x' position as the inherited _interfaceGraphic VectorShape3D. Button3D then adds it as a child and calls the activate () method. The activate () method is called here so that once a Button3D is instantiated, it is automatically interactive. I'll explain more about the activate () and deActivate () methods when we get to them.
Step 27: Applying Effects to Reflect Interactivity
My favorite part, creating effects! Ok, so let's go over the effect we want to achieve. For mouse over, we want the button to move closer to the camera, play a sound, have it glow for a second then fade back to normal again. For mouse out, we just want the button to move back to its original position. For mouse click, we want the button to also move back to its original position, blink 3 times and then dispatch the button click event so that the menu containing them doesn't start close before the 3 blinks finish. Let's start with the mouse over effect. Add the following code below the addInteractivePlane () method:
override public function onObjectOver (e:InteractiveScene3DEvent = null):void { _buttonOverSound.play (0,0,_soundTransform); this.z = OVER_POSITION; _interfaceLayer.filters = [new BlurFilter (5, 5), new GlowFilter (0xFFFFFF, 1, 15, 15, 2)]; _textLayer.filters = [new BlurFilter (5, 5), new GlowFilter (0xFFFFFF, 1, 15, 15, 2)]; TweenMax.to ( _interfaceLayer, .4, { blurFilter: { blurX:0, blurY:0, quality:1 }, glowFilter: { color:0xFFFFFF, alpha:0, blurX:0, blurY:0, strength:0, quality:1 } } ); TweenMax.to ( _textLayer, .4, { blurFilter: { blurX:0, blurY:0, quality:1 }, glowFilter: { color:0xFFFFFF, alpha:0, blurX:0, blurY:0, strength:0, quality:1 } } ); super.onObjectOver(e); }
Firstly, we play the sound we assigned for mouse over. Then we move the button's 'z' position 50 units closer to the camera. Remember assigning the ViewportLayer properties of the VectorShape3D and Text3D into them when we were building UIComponent3D? We use TweenMax because it has the extra capability of applying blur and glow effects, not to mention others that TweenLite doesn't have. Here, we instantly assign an array of filters to both _interfaceLayer and _textLayer. It's as simple as assigning filters to a regular Sprite class since in essence, the ViewportLayer is a Sprite. TweenMax is used to fade the effects out. A call to UIComponent3D's onObjectOver () method is then made passing in the same InteractiveScene3DEvent that triggered this one.
Next, for mouse out, Add the code below after the onObjectOver () method:
override public function onObjectOut (e:InteractiveScene3DEvent = null):void { super.onObjectOut(e); this.z = OUT_POSITION; }
All this does is trigger the UIComponent3D's onObjectOut () method and position the button back to its original location.
Now for the mouse click effect. Add the following method below the onObjectOut () method:
override public function onObjectClick (e:InteractiveScene3DEvent = null):void { _buttonClickSound.play (0,0, _soundTransform); this.z = OUT_POSITION; flashText (); //helper method to make the text blink 3 times deActivate (); //prevents it from getting triggered more than once //dispatched immediately to inform MainMenu3D to deActivate the other Button3Ds it is holding dispatchEvent (new InteractiveScene3DEvent (InteractiveScene3DEvent.OBJECT_RELEASE)); }
When an instance of the Button3D is clicked, first thing it does is play a sound. It's then returned back to its original 'z' position. Two methods are then called - flashText () and deActivate (). We use InteractiveScene3D.OBJECT_RELEASE to inform the MainMenu3D holding this Button3D so that it can disable all the other buttons after a choice has already been made. Add the 2 helper methods below the onObjectClick () method:
protected function flashText ():void { var timer:Timer = new Timer (15, 9); timer.addEventListener (TimerEvent.TIMER, blink); timer.addEventListener (TimerEvent.TIMER_COMPLETE, dispatchClick); timer.start (); } protected function blink (e:TimerEvent):void { _text3D.visible = ! _text3D.visible; } protected function dispatchClick (e:TimerEvent):void { blink (e); //to make the text visible //dispatched only after all the button animations have completed, used as trigger to actually process the function this button triggers super.onObjectClick (new InteractiveScene3DEvent (InteractiveScene3DEvent.OBJECT_CLICK)); }
Ok, so there's another helper method helping the flashText () helper method. Hey, whatever it takes to achieve the effect, right?! =)
When flashText () is called, it creates a timer with a delay of 15 milliseconds which repeats 9 times. The blink () method is then assigned as the callback for every TIMER event, dispatchClick () is assigned as callback for the TIMER_COMPLETE event. every time the blink () method is called, _text3D's visibility toggles. This happens 9 times which makes it look like the text blinked really fast 3 times. Once the timer completes, blink () is called one more time to make _text3D visible again and the onObjectClick event is dispatched.
Step 28: External Controls for Adding and Removing MouseEvent Listeners
Add the following lines of code:
public function activate ():void { addMouseListeners (_plane); } public function deActivate ():void { removeMouseListeners (_plane); }
These methods will allow the MainMenu3D to control when the Button3D should listen to mouse over, mouse out, and mouse click events.
Step 29: Extra Functionality
Visit the demo app again and play with the About menu for a second. Notice how the scrollers don't need to listen for mouse clicks. For this, we'll have to add functionality to remove the mouse click listener automatically added by default. Add the 2 methods at the bottom of the class:
public function set noClick ($value:Boolean):void { _noClick = $value; } override protected function addMouseListeners($do3D:DisplayObject3D):void { super.addMouseListeners($do3D); if (_noClick == true) _plane.removeEventListener (InteractiveScene3DEvent.OBJECT_CLICK, onObjectClick); }
The set noClick () setter method simply assigns the Boolean parameter passed in to it. To remove the click listener, just call this method and pass in a value of true before calling the button's init () method. Button3D overrides the addMouseListeners () method and checks if the _noClick property is true, if it is, it stops listening for click events.
Step 30: Milestone Testing Button3D
Go back to Tester.as and comment out the addCustomDO3D () method call inside the init () method. Next, add a call to addButton () method below it. Then add the 2 methods listed below after the init () method:
private function addButton ():void { var b:Button3D = new Button3D ('button 0', viewport); b.init (); //remember to call this after modifying the button's properties b.addEventListener (InteractiveScene3DEvent.OBJECT_CLICK, onButtonClick); scene.addChild (b); } private function onButtonClick (e:InteractiveScene3DEvent):void { trace ('button3d was clicked'); Button3D (e.target).activate (); }
First, it assigns a local variable named 'b' as an instance of Button3D, and passes in a name and text of 'button 0' and the viewport. Remember, the last 2 parameters are optional. Now run it. You should see something like the cut-out below:

Oops! No mouse interaction?! To listen for mouse events, first, we need to make the viewport interactive. Go back inside Tester's init () method and add the code below the sortMode assignment:
viewport.interactive = true;
That's it! Run it again and see how it reacts to the mouse. The onButtonClick () method informs us that the button is clicked via trace and then we call Button3D.activate () on the button to have it listen for mouse events again. Remember, we have this set to disable itself as soon as the button is clicked. Also, you can change the speed of the blinks if you think its a little fast - see step 27. But once effects are added, you might need to re-adjust it again.
Let's do another button, this time, let's customize it a bit since the last one had all default properties. Change the code inside addButton to what we have below:
var coordinates:Array = [ new Number2D ( -54, 75), new Number2D (54, 75), new Number2D (98, 0), new Number2D (54, -75), new Number2D ( -54, -75), new Number2D ( -98, 0), new Number2D ( -54, 75) ]; var b:Button3D = new Button3D ('hex', viewport, coordinates); //passed in a custom drawing coordinates b.fillAlpha = .3; //change the fill alpha value from .6 b.lineAlpha = .3; //change the line alpha value from 0 b.lineThickness = 5; //change the lineThickness value from 2 b.fillColor = 0x808080; //change the fill color value from 0x2D2D2D b.lineColor = 0x80FF00; //change the line color value from 0x007500 b.noClick = true; //stop listening for mouse clicks b.init (); //after all modifications, make the button b.text3D.x = -430; //position the text on the center of the button manually b.text3D.scale = .75; //scale it to fit b.addEventListener (InteractiveScene3DEvent.OBJECT_CLICK, onButtonClick); //same event listener, won't get triggered b.addEventListener (InteractiveScene3DEvent.OBJECT_OVER, onButtonOver); //added to listen for mouse over b.addEventListener (InteractiveScene3DEvent.OBJECT_OUT, onButtonOut); //added to listen for mouse out scene.addChild (b); //add into the scene
Just so we know the mouse over and mouse out event listeners are working, add the code below after the onButtonClick () method before running another test:
private function onButtonOver(e:InteractiveScene3DEvent):void { trace ('button3d over'); } private function onButtonOut(e:InteractiveScene3DEvent):void { trace ('button3d out'); }
Here's a snapshot of our new Button3D with the custom properties we assigned.

And that completes our Button3D class. Now relax, take a break and come back when you're ready for more.
Step 31: Creating the Main Menu - MainMenu3D
This DisplayObject3D instance will be what you see as the main page that opens up when the application first starts. It holds 4 buttons - start game, settings, about, and exit. It manages its own animation and processes events before sending them up the display hierarchy. Here is its list of responsibilities:
- create 2 borders to act as casing for the contents
- create and add the 4 buttons and add listeners for them
- prepare for intro animation
- add effects and animations
- provide external control for playing intro and exit animations
- manage activating and deactivating the buttons
- dispatch custom events triggered by the 4 buttons so that UserInterface3D can load the appropriate submenu, have the Main document load the game sprite, or close the entire application
Create a new class and name it MainMenu3D. Have it extend DisplayObject3D. Once the class file is open, just copy and paste the code below replacing the content of the class.
package { import com.greensock.easing.Expo; import com.greensock.TweenMax; import flash.events.Event; import flash.media.Sound; import flash.media.SoundTransform; import org.papervision3d.core.math.Number2D; import org.papervision3d.events.InteractiveScene3DEvent; import org.papervision3d.objects.DisplayObject3D; import org.papervision3d.view.Viewport3D; public class MainMenu3D extends DisplayObject3D { //used as a custom event notifying UserInterface3D to load back MainMenu3D when the back button is clicked from the 2 other submenus public static const BACK_TO_MAIN:String = 'backToMain'; public static const START_GAME_CLICKED:String = 'startGameClicked'; //informs UserInterface3D that the start game button was clicked public static const SETTINGS_CLICKED:String = 'settingsClicked'; //informs UserInterface3D that the settings button was clicked public static const ABOUT_CLICKED:String = 'aboutClicked'; //informs UserInterface3D that the about button was clicked public static const EXIT_CLICKED:String = 'exitClicked'; //informs UserInterface3D that the exit button was clicked public static const EXIT_COMPLETE:String = 'exitComplete'; //informs UserInterface3D that the menu's exit animation has finished protected var _topNav:UIComponent3D; //for the top border encasing the 4 buttons protected var _bottomNav:UIComponent3D; //for the bottom border encasing the 4 buttons protected var _viewport:Viewport3D; //passed in from Tester or UserInterface3D for access to ViewportLayers to apply effects protected var _buttonPosY:Array = [220, 70, -80, -230]; //button y positions from top to bottom protected var _buttonTitles:Array = ['start game', 'settings', 'about', 'exit']; //button names and text protected var _buttonArray:Array; //holds references for the 4 buttons //a custom effect class we'll create later, play the preview from the link at the top of the page to see this ///private var _distortion:DistortionEffect3D; //leave it commented out for now public function MainMenu3D($name:String, $viewport:Viewport3D) { //start here } } }
First, the required imports inside the package declaration then come the instance variables and class constants.
Step 32: MainMenu3D Constructor
The constructor takes in 2 required parameters, $name and $viewport. super () is called to assign the $name for the DisplayObject3D instance. And $viewport is saved for reference later. Add the code below inside the constructor method:
super ($name); _viewport = $viewport; init ();
Step 33: Instance Initialization
The init () method triggers 3 other methods that abstract the creation for the borders, the buttons, and the distortion effect. Add the code below after the constructor:
protected function init ():void { createFrame (); addButtons (); setMenuEffect (); }
The 1st method, createFrame () simply adds the 2 borders as children. We use UIComponent3D since all they need to be are visual components. Add the following code below the init () method:
protected function createFrame ():void { _topNav = new UIComponent3D ( 'topNav', _viewport, [ new Number2D ( -400, 26), new Number2D ( -360, 26), new Number2D ( -329, 53), new Number2D (60, 53), new Number2D (90, 26), new Number2D (400, 26), new Number2D (450, -13), new Number2D (450, -53), new Number2D (400, -53), new Number2D (360, -33), new Number2D ( -2, -33), new Number2D ( -35, -53), new Number2D ( -295, -53), new Number2D ( -450, -53), new Number2D ( -400, 26) ] ); _topNav.fillColor = 0xCCCC00; //change color from default to olive green _bottomNav = new UIComponent3D ( 'bottomNav', _viewport, [ new Number2D (-450, 33), new Number2D (-50, 33), new Number2D (-20, 7), new Number2D (376, 7), new Number2D (390, 33), new Number2D (450, 33), new Number2D (395, -33), new Number2D (-402, -33), new Number2D (-450, 33) ] ); _topNav.init (); _bottomNav.init (); addChild (_topNav); addChild (_bottomNav); }
The addButtons () method takes care of adding the 4 buttons. Add the code below next:
protected function addButtons ():void { _buttonArray = []; for (var i:uint = 0; i < _buttonTitles.length; i++) { var button:Button3D = new Button3D (_buttonTitles[i], _viewport); button.addEventListener (InteractiveScene3DEvent.OBJECT_CLICK, onButtonClick);/// button.addEventListener (InteractiveScene3DEvent.OBJECT_RELEASE, onButtonRelease);/// button.y = _buttonPosY[i]; _buttonArray.push (button); button.init (); //remember to call init () for any object that inherits from UIComponent3D, after customization addChild (button); } }
Here, _buttonArray is assigned a new Array to hold references for the 4 buttons. As _buttonTitles is iterated through, listeners for OBJECT_CLICK and OBJECT_RELEASE are then added for each button created. they are then positioned on their 'y' axis based on _buttonPosY for the current iteration. Remember how we setup the onObjectClick () method for Button3D, the 1st takes care of the extra animation effects for the button while the other propagates up to MainMenu3D. You'll see its use when we get to the event handlers later.
Lastly, the setMenuEffect () method. Add the code below:
protected function setMenuEffect ():void { //leave blank for now }
This is where we will add the distortion effect. For now, just leave it until we create the custom DistortionEffect3D class. We'll do that next as soon as we finish MainMenu3D.
Step 34: Handling the Intro Animation
If you study how the main menu from the preview link at the top of the page, you'll see the sequence of operation. The menu borders fade in and move to open. Once open, the buttons start to show from the bottom to the top. All flashing and playing sound. To start, the intro animation will need to be prepared before playing it. Add the code below after the setMenuEffect () method:
protected function prepareIntro ():void { //add distortion effect activation later _topNav.y = 50; //position 50 units up from center _bottomNav.y = -50; //50 units down from center _topNav.interfaceLayer.alpha = _bottomNav.interfaceLayer.alpha = 0; //set alphas to 0 by applying it to each UIComponent3D's interfaceLayer property (ViewportLayer) for each (var button:Button3D in _buttonArray) button.visible = false; }
This is where we will add control for activating the distortion effect you'll be creating later. The code above positions the 2 borders close to the center so they can animate outwards in an opening manner. Their alphas are set to 0 so they can slowly appear when the intro starts. All the buttons are then made invisible so they can appear 1 by 1 in sequence once the borders have finished opening.
Add the code below next. This will take care of playing the intro and provide external access for it:
public function startIntro ():void { prepareIntro (); var sound:Sound = CustomLoader.getInstance ().getSound ('menuExpand'); TweenMax.to (_topNav.interfaceLayer, .4, { alpha:1, delay:.5, onStart:sound.play, onStartParams:[0,0,new SoundTransform (1)],overwrite:false } ); TweenMax.to (_bottomNav.interfaceLayer, .4, { alpha:1, delay:.5, overwrite:false } ); TweenMax.to (_topNav, .5, { y:354, delay:.75, ease:Expo.easeInOut, overwrite:false } );//0 TweenMax.to (_bottomNav, .5, { y:-350, delay:.75, ease:Expo.easeInOut, overwrite:false, onComplete:initializeButtons } );//0 }
This method calls prepareIntro () to get everything in proper place before starting the animation. A local variable sound of type Sound is assigned a reference to the sound effect named 'menuExpand' saved in the CustomLoader instance. Next, TweenMax is used to slowly fade the 2 borders' alpha property in. This tween has a duration of 0.4 seconds and has a delay factor of 0.5 seconds. The sound is called to play when it starts, and the tween is set not to get overwritten. Another call to TweenMax is assigned for the actual movement of the 2 borders, this time with a delay of .75 seconds. .25 seconds after the fade in starts, the 2 borders are animated to open. The 2 borders move to open in 0.5 seconds and when finished, calls the initializeButtons () method.
Add the code below:
protected function initializeButtons ():void { for each (var button:Button3D in _buttonArray) button.activate (); //make sure the buttons are active for (var i:uint = 0; i < _buttonArray.length; i++) { TweenMax.delayedCall (((_buttonArray.length - 1) - i) * .07, showButtons, [_buttonArray[i]]); } }
This is how the 4 buttons are activated and then added to the menu in sequence. It counts backwards from the number of buttons delayed by .07 seconds. At each iteration a call to showButtons () is made passing in _buttonArray's value for the the current iteration. Below is the last method which completes the intro animation:
protected function showButtons ($button:Button3D):void { $button.visible = true; $button.onObjectOver (new InteractiveScene3DEvent (InteractiveScene3DEvent.OBJECT_OVER)); //simulate a mouse over for the button $button.z = Button3D.OUT_POSITION; }
Once this is called, the button passed in as parameter is worked on to. It first shows the button then then trigger its onObjectOver () method to simulate a mouse over event. Immediately after, it puts the button in its default mouse out position.
Step 35: Handling Mouse Events
From within MainMenu3D, the buttons will be handled by their name to identify which button was clicked and dispatch the appropriate event it needs to trigger. The onButtonClick () method will send the event to MainMenu3D's containing parent, either UserInterface3D or the Tester class we're currently using. Add the code below:
protected function onButtonClick (e:InteractiveScene3DEvent):void { switch (e.target.name) { case 'start game': dispatchEvent (new Event (START_GAME_CLICKED)); break; case 'settings': dispatchEvent (new Event (SETTINGS_CLICKED)); break; case 'about': dispatchEvent (new Event (ABOUT_CLICKED)); break; case 'exit': dispatchEvent (new Event (EXIT_CLICKED)); break; } startExit (); //starts the exit animation }
The rest of the animation for the button is done so by the button itself. Remember, we set this method to get triggered by each button's onObjectClick () method. That method takes care of all the button animation effects before dispatching this event upward. After dispatching the appropriate event, the menu is informed to exit. Next, add the code below for the onButtonRelease () method:
protected function onButtonRelease (e:InteractiveScene3DEvent):void { for each (var button:Button3D in _buttonArray) { button.deActivate (); } }
This is the simplest method for MainMenu3D, its sole purpose is to make sure that when a button is clicked, the user, no matter how fast he tries, won't be able to click another button and possibly trigger something undesirable. Go back to Button3D's onObjectClick () method to see how this is triggered.
Step 36: Handling the Exit Animation
Once the user has decided what to do, either edit the settings, play the game, whatever, Mainmenu3D will start exiting and inform Tester or UserInterface3D when its finished. Note that this method is declared public, matching its counterpart startIntro (). UserInterface3D will have no need to call this method since it is handled internally. Add the code below:
public function startExit ():void { //add distortion effect deactivation later for (var i:uint = 0; i < _buttonArray.length; i++) { TweenMax.delayedCall ((i) * .07, hideButtons, [_buttonArray[i]]); } }
This is where we will add control to deactivate the distortion effect mentioned earlier. Here, the buttons are hidden from view one by one in sequence. The opposite of how they appeared in the initializeButtons () method. Add the hideButtons () method next:
protected function hideButtons ($button:Button3D):void { $button.onObjectOver (new InteractiveScene3DEvent (InteractiveScene3DEvent.OBJECT_OVER)); $button.z = Button3D.OUT_POSITION; TweenMax.delayedCall (.1, hideObject, [$button]); if ($button == _buttonArray[_buttonArray.length - 1])//when all the buttons are hidden { closeNav (); } }
This method is what makes the buttons flash one by one and then hide in sequence before the 2 borders animate to close. It first simulates a mouse over effect. Another delayedCall () is applied so that the button stays long enough to finish the button over effect before being removed from visibility. After that, it checks if the button being worked on is the last button from the list of buttons and if it is, calls the closeNav () method. Add the 2 methods next:
protected function hideObject ($object:DisplayObject3D):void { $object.visible = false; } //sets the button invisible after the button simulates a button over event protected function closeNav ():void { var sound:Sound = CustomLoader.getInstance ().getSound ('menuCollapse'); TweenMax.to (_topNav.interfaceLayer, .2, { alpha:0, delay:.4, overwrite:false } ); TweenMax.to (_bottomNav.interfaceLayer, .2, { alpha:0, delay:.4, overwrite:false, onComplete:dispatchEvent, onCompleteParams:[new Event (EXIT_COMPLETE)] } ); TweenMax.to (_topNav, .5, { y:50, onStart: sound.play, ease:Expo.easeInOut, overwrite:false } ); TweenMax.to (_bottomNav, .5, { y:-50, ease:Expo.easeInOut, overwrite:false } ); }
Once all the buttons have been set invisible, the 2 borders begin to close. The first 2 calls to TweenMax take care of fading out the top and bottom borders. The fade out takes 0.2 seconds with a delay of 0.4 seconds. The end of the fade dispatches the EXIT_COMPLETE event. For the second set of TweenMax calls, the borders are animated inwards in a closing manner. It takes 0.5 seconds to complete, uses the Expo.easeInOut for the ease and doesn't have any delay. This call also plays the sound 'menuCollapse' for the closing sound effect. And we're done with MainMenu3D! Alright, save and get ready for testing.
Step 37: Milestone Testing MainMenu3D
Go back to the Tester class' init () method. Comment out the call to addButton () and add a call to addMainMenu () below it. Copy the method below after the init () method:
private function addMainMenu ():void { var mm:MainMenu3D = new MainMenu3D ('menu', viewport); mm.addEventListener (MainMenu3D.START_GAME_CLICKED, navigateTo); mm.addEventListener (MainMenu3D.SETTINGS_CLICKED, navigateTo); mm.addEventListener (MainMenu3D.ABOUT_CLICKED, navigateTo); mm.addEventListener (MainMenu3D.EXIT_CLICKED, navigateTo); mm.addEventListener (MainMenu3D.EXIT_COMPLETE, onMenuExitComplete); scene.addChild (mm); mm.startIntro (); }
Here, MainMenu3D is instantiated passing in the name 'menu' for the name and viewport for its _viewport property. Listeners are then assigned for all the events the MainMenu3D instance will dispatch. After adding it to the scene, we need to call the MainMenu3D's public method startIntro () to have it load. Let's add the corresponding methods for the listeners before testing. Add the code below after the addMainMenu () method:
private function navigateTo(e:Event):void { switch (e.type) { case MainMenu3D.START_GAME_CLICKED: trace ('load the game and play'); break; case MainMenu3D.SETTINGS_CLICKED: trace ('load settings menu'); break; case MainMenu3D.ABOUT_CLICKED: trace ('load about menu'); break; case MainMenu3D.EXIT_CLICKED: trace ('exit the application'); break; } } private function onMenuExitComplete (e:Event):void { MainMenu3D (e.target).startIntro (); }
Now run the application. Click here to see the result:
Once any of the 4 buttons is clicked, the MainMenu3D instance dispatches the event then exits. You should see print outs of what the button triggered. Here, we just load the menu again when the MainMenu3D.EXIT_COMPLETE event is received. Again, the buttons might be blinking a little fast for anyone to notice but it should slow down once we start adding effects. Play with it a little to get more familiar with it. We're going to extend this class for both the AboutMenu3D and SettingsMenu3D submenus later. Now, let's work on the DistortionEffect3D before we move on to higher grounds.
Conclusion
Thanks for reading up to this point! Join me again for the second and concluding part where we'll finish our interface.
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Update me weeklyEnvato Tuts+ tutorials are translated into other languages by our community members—you can be involved too!
Translate this post