10 min read

Away3D 3.6 Cookbook

Away3D 3.6 Cookbook

Over 80 practical recipes for creating stunning graphics and effects with the fascinating Away3D engine

Cameras are an absolutely essential part of the 3D world of computer graphics. In fact, no real-time 3D engine can exist without having a camera object. Cameras are our eyes into the 3D world.

Away3D has a decent set of cameras, which at the time of writing, consists of Camera3D, TargetCamera3D, HoverCamera3D, and SpringCam classes. Although they have similar base features, each one has some additional functionality to make it different.

Creating an FPS controller

There are different scenarios where you wish to get a control of the camera in first person, such as in FPS video games. Basically, we want to move and rotate our camera in any horizontal direction defined by the combination of x and y rotation of the user mouse and by keyboard keys input. In this recipe, you will learn how to develop such a class from scratch, which can then be useful in your consequential projects where FPS behavior is needed.

Getting ready

Set up a basic Away3D scene extending AwayTemplate and give it the name FPSDemo. Then, create one more class which should extend Sprite and give it the name FPSController.

How to do it…

FPSController class encapsulates all the functionalities of the FPS camera. It is going to receive the reference to the scene camera and apply FPS behavior “behind the curtain”.

FPSDemo class is a basic Away3D scene setup where we are going to test our FPSController:

FPSController.as

package utils
{
public class FPSController extends Sprite
{
private var _stg:Stage;
private var _camera:Object3D
private var _moveLeft_Boolean=false;
private var _moveRight_Boolean=false;
private var _moveForward_Boolean=false;
private var _moveBack_Boolean=false;
private var _controllerHeigh:Number;
private var _camSpeed_Number=0;
private static const CAM_ACCEL_Number=2;
private var _camSideSpeed_Number=0;
private static const CAM_SIDE_ACCEL_Number=2;
private var _forwardLook_Vector3D=new Vector3D();
private var _sideLook_Vector3D=new Vector3D();
private var _camTarget_Vector3D=new Vector3D();
private var _oldPan_Number=0;
private var _oldTilt_Number=0;
private var _pan_Number=0;
private var _tilt_Number=0;
private var _oldMouseX_Number=0;
private var _oldMouseY_Number=0;
private var _canMove_Boolean=false;
private var _gravity:Number;
private var _jumpSpeed_Number=0;
private var _jumpStep:Number;
private var _defaultGrav:Number;
private static const GRAVACCEL_Number=1.2;
private static const MAX_JUMP_Number=100;
private static const FRICTION_FACTOR_Number=0.75;
private static const DEGStoRADs:Number = Math.PI / 180;

public function FPSController(camera:Object3D,stg:Stage,
height_Number=20,gravity:Number=5,jumpStep:Number=5)
{
_camera=camera;
_stg=stg;
_controllerHeigh=height;
_gravity=gravity;
_defaultGrav=gravity;
_jumpStep=jumpStep;
init();
}
private function init():void{
_camera.y=_controllerHeigh;
addListeners();
}
private function addListeners():void{
_stg.addEventListener(MouseEvent.MOUSE_DOWN,
onMouseDown,false,0,true);
_stg.addEventListener(MouseEvent.MOUSE_UP,
onMouseUp,false,0,true);
_stg.addEventListener(KeyboardEvent.KEY_DOWN,
onKeyDown,false,0,true);
_stg.addEventListener(KeyboardEvent.KEY_UP,
onKeyUp,false,0,true);

}
private function onMouseDown(e:MouseEvent):void{
_oldPan=_pan;
_oldTilt=_tilt;
_oldMouseX=_stg.mouseX+400;
_oldMouseY=_stg.mouseY-300;
_canMove=true;
}
private function onMouseUp(e:MouseEvent):void{
_canMove=false;
}
private function onKeyDown(e:KeyboardEvent):void{
switch(e.keyCode)
{
case 65:_moveLeft = true;break;
case 68:_moveRight = true;break;
case 87:_moveForward = true;break;
case 83:_moveBack = true;break;
case Keyboard.SPACE:
if(_camera.y<MAX_JUMP+_controllerHeigh){
_jumpSpeed=_jumpStep;
}else{
_jumpSpeed=0;
}

break;
}
}
private function onKeyUp(e:KeyboardEvent):void{
switch(e.keyCode)
{
case 65:_moveLeft = false;break;
case 68:_moveRight = false;break;
case 87:_moveForward = false;break;
case 83:_moveBack = false;break;
case Keyboard.SPACE:_jumpSpeed=0;break;
}
}
public function walk():void{
_camSpeed *= FRICTION_FACTOR;
_camSideSpeed*= FRICTION_FACTOR;
if(_moveForward){ _camSpeed+=CAM_ACCEL;}
if(_moveBack){_camSpeed-=CAM_ACCEL;}
if(_moveLeft){_camSideSpeed-=CAM_SIDE_ACCEL;}
if(_moveRight){_camSideSpeed+=CAM_SIDE_ACCEL;}
if (_camSpeed < 2 && _camSpeed > -2){
_camSpeed=0;
}
if (_camSideSpeed < 0.05 && _camSideSpeed > -0.05){
_camSideSpeed=0;
}
_forwardLook=_camera.transform.deltaTransformVector(new
Vector3D(0,0,1));
_forwardLook.normalize();
_camera.x+=_forwardLook.x*_camSpeed;
_camera.z+=_forwardLook.z*_camSpeed;

_sideLook=_camera.transform.deltaTransformVector(new
Vector3D(1,0,0));
_sideLook.normalize();
_camera.x+=_sideLook.x*_camSideSpeed;
_camera.z+=_sideLook.z*_camSideSpeed;

_camera.y+=_jumpSpeed;
if(_canMove){
_pan = 0.3*(_stg.mouseX+400 – _oldMouseX) + _oldPan;
_tilt = -0.3*(_stg.mouseY-300 – _oldMouseY) +
_oldTilt;
if (_tilt > 70){
_tilt = 70;
}

if (_tilt < -70){
_tilt = -70;
}
}
var panRADs_Number=_pan*DEGStoRADs;
var tiltRADs_Number=_tilt*DEGStoRADs;
_camTarget.x = 100*Math.sin( panRADs) * Math.cos
(tiltRADs) +_camera.x;
_camTarget.z = 100*Math.cos( panRADs) * Math.cos
(tiltRADs) +_camera.z;
_camTarget.y = 100*Math.sin(tiltRADs) +_camera.y;
if(_camera.y>_controllerHeigh){
_gravity*=GRAVACCEL;
_camera.y-=_gravity;
}
if(_camera.y<=_controllerHeigh ){
_camera.y=_controllerHeigh;
_gravity=_defaultGrav;
}
_camera.lookAt(_camTarget);
}
}
}


Now let’s put it to work in the main application:

FPSDemo.as
package
{
public class FPSDemo extends AwayTemplate
{
[Embed(source=”assets/buildings/CityScape.3ds”,mimeType=”
application/octet-stream”)]
private var City:Class;
[Embed(source=”assets/buildings/CityScape.png”)]
private var CityTexture:Class;
private var _cityModel:Object3D;
private var _fpsWalker:FPSController;
public function FPSDemo()
{
super();
}

override protected function initGeometry() : void{
parse3ds();
}
private function parse3ds():void{
var max3ds_Max3DS=new Max3DS();
_cityModel=max3ds.parseGeometry(City);
_view.scene.addChild(_cityModel);
_cityModel.materialLibrary.getMaterial(“bakedAll [Plane0”).
material=new BitmapMaterial(Cast.bitmap(new CityTexture()));
_cityModel.scale(3);
_cityModel.x=0;
_cityModel.y=0;
_cityModel.z=700;
_cityModel.rotate(Vector3D.X_AXIS,-90); _cam.z=-1000;
_fpsWalker=new FPSController(_cam,stage,_view,20,12,250);
}
override protected function onEnterFrame(e:Event) : void{
super.onEnterFrame(e);
_fpsWalker.walk();
}
}
}


How it works…

FPSController class looks a tad scary, but that is only at first glance. First we pass the following arguments into the constructor:

  1. camera: Camera3D reference (here Camera3D, by the way, is the most appropriate one for FPS).
  2. stg: References to flash stage because we are going to assign listeners to it from within the class.
  3. height: It is the camera distance from the ground. We imply here that the ground is at 0,0,0.
  4. gravity: Gravity force for jump.
  5. JumpStep: Jump altitude.

Next we define listeners for mouse UP and DOWN states as well as events for registering input from A,W,D,S keyboard keys to be able to move the FPSController in four different directions.

In the onMouseDown() event handler, we update the old pan, tilt the previous mouseX and mouseY values as well as by assigning the current values when the mouse has been pressed to _oldPan, _oldTilt, _oldMouseX, and _oldMouseY variables accordingly. That is a widely used technique. We need to do this trick in order to have nice and continuous transformation of the camera each time we start moving the FPSController. In the methods onKeyUp() and onKeyDown(), we switch the flags that indicate to the main movement execution code. This will be seen shortly and we will also see which way the camera should be moved according to the relevant key press. The only part that is different here is the block of code inside the Keyboard.SPACE case. This code activates jump behavior when the space key is pressed.

On the SPACE bar, the camera jumpSpeed (that, by default, is zero) receives the _jumpStep incremented value and this, in case the camera has not already reached the maximum altitude of the jump defined by MAX_JUMP, is added to the camera ground height.

Now it’s the walk() function’s turn. This method is supposed to be called on each frame in the main class:

_camSpeed *= FRICTION_FACTOR;
_camSideSpeed*= FRICTION_FACTOR;


Two preceding lines slow down, or in other words apply friction to the front and side movements. Without applying the friction. It will take a lot of time for the controller to stop completely after each movement as the velocity decrease is very slow due to the easing.

Next we want to accelerate the movements in order to have a more realistic result. Here is acceleration implementation for four possible walk directions:

if(_moveForward){ _camSpeed+= CAM_ACCEL;}
if(_moveBack){_camSpeed-= CAM_ACCEL;}
if(_moveLeft){_camSideSpeed-= CAM_SIDE_ACCEL;}
if(_moveRight){_camSideSpeed+= CAM_SIDE_ACCEL;}


The problem is that because we slow down the movement by continuously dividing current speed when applying the drag, the speed value actually never becomes zero. Here we define the range of values closest to zero and resetting the side and front speeds to 0 as soon as they enter this range:

if (_camSpeed < 2 && _camSpeed > -2){
_camSpeed=0;

}

if (_camSideSpeed < 0.05 && _camSideSpeed > -0.05){
_camSideSpeed=0;
}


Now we need to create an ability to move the camera in the direction it is looking. To achieve this we have to transform the forward vector, which present the forward look of the camera, into the camera space denoted by _camera transformation matrix. We use the deltaTransformVector() method as we only need the transformation portion of the matrix dropping out the translation part:

_forwardLook=_camera.transform.deltaTransformVector(new
Vector3D(0,0,1));
_forwardLook.normalize();
_camera.x+=_forwardLook.x*_camSpeed;
_camera.z+=_forwardLook.z*_camSpeed;


Here we make pretty much the same change as the previous one but for the sideways movement transforming the side vector by the camera’s matrix:

_sideLook=_camera.transform.deltaTransformVector(new Vector3D(1,
0,0));
_sideLook.normalize();
_camera.x+=_sideLook.x*_camSideSpeed;
_camera.z+=_sideLook.z*_camSideSpeed;


And we also have to acquire base values for rotations from mouse movement. _pan is for the horizontal (x-axis) and _tilt is for the vertical (y-axis) rotation:

if(_canMove){
_pan = 0.3*(_stg.mouseX+400 – _oldMouseX) + _oldPan;
_tilt = -0.3*(_stg.mouseY-300 – _oldMouseY) + _oldTilt;

if (_tilt > 70){
_tilt = 70;
}

if (_tilt < -70){
_tilt = -70;
}
}


We also limit the y-rotation so that the controller would not rotate too low into the ground and conversely, too high into zenith. Notice that this entire block is wrapped into a _canMove Boolean flag that is set to true only when the mouse DOWN event is dispatched. We do it to prevent the rotation when the user doesn’t interact with the controller.

Finally we need to incorporate the camera local rotations into the movement process. So that while moving, you will be able to rotate the camera view too:

var panRADs_Number=_pan*DEGStoRADs;
var tiltRADs_Number=_tilt*DEGStoRADs;
_camTarget.x = 100*Math.sin( panRADs) * Math.cos(tiltRADs) +
_camera.x;
_camTarget.z = 100*Math.cos( panRADs) * Math.cos(tiltRADs) +
_camera.z;
_camTarget.y = 100*Math.sin(tiltRADs) +_camera.y;


And the last thing is applying gravity force each time the controller jumps up:

if(_camera.y>_controllerHeigh){
_gravity*=GRAVACCEL;
_camera.y-=_gravity;
}
if(_camera.y<=_controllerHeigh ){
_camera.y=_controllerHeigh;
_gravity=_defaultGrav;
}


Here we first check whether the camera y-position is still bigger than its height, this means that the camera is in the “air” now. If true, we apply gravity acceleration to gravity because, as we know, in real life, the falling body constantly accelerates over time. In the second statement, we check whether the camera has reached its default height. If true, we reset the camera to its default y-position and also reset the gravity property as it has grown significantly from the acceleration addition during the last jump.

To test it in a real application, we should initiate an instance of the FPSController class. Here is how it is done in FPSDemo.as:

_fpsWalker=new FPSController(_cam,stage,20,12,250);


We pass to it our scene camera3D instance and the rest of the parameters that were discussed previously.

The last thing to do is to set the walk() method to be called on each frame:

override protected function onEnterFrame(e:Event) : void{
super.onEnterFrame(e);
_fpsWalker.walk();
}


Now you can start developing the Away3D version of Unreal Tournament!

LEAVE A REPLY

Please enter your comment!
Please enter your name here