Create a 3D Bowling Game with JiglibFlash and Papervision 3D

by Devon 16

Possibly the hottest thing to hit the Flash community since the advent of several excellent 3D engines is the recent debut of 3D physics engines. Here we’ll examine a way of using one of the more popular 3D engines, JiglibFlash, in conjunction with the prominent 3D engine Papervision3D to create a bowling game. Now this won’t be fully developed game, as there’ll be no frames or score tracking – I’ll leave all that hoo hah up to you – but it will be a good solid start and will hopefully provide a strong foundation of knowledge on how to integrate these two engines for future physics based 3D games and applications.

Result

You can see below what we’re aiming for. To play, first click on the green rectangle to horizontally position the ball. Then, click the red square to roll the ball. The higher up on the square you click, the more forward power the ball will have. The angular force on the ball is determined by the horizontal position of the click. This may sound confusing (and if I were to develop this into a fully featured game, it’s something I would certainly give more thought to), but trying it out a couple times should make the mechanism obvious. Seven seconds after each roll the ball will reappear and be ready to roll again.

Requirements

While I used FlashDevelop and the Flex 3 SDK to create this project, it could also be completed with FlexBuilder 3 (or FlashBuilder 4) or with Flash CS4 and the Flex SDK (using Main.as as the .fla’s document class). In any case, you will need something to compile Flash Player 10 .swf files.

Pre-Requesites

This is certainly not an introductory actionscript tutorial and a number of assumptions are made: (1) that you are familiar with Actionscript 3 and basic OOP principles and are comfortable working with packages and class files, (2) you already have the JiglibFlash and Papervision3D libraries downloaded and placed within your classpath. For this tutorial I used Papervision3D 2.0.0 (March 12, 2009) and JiglibFlash v0.32 (2009-5-14). I just mention that as you never know when the API’s may change. And, finally, (3) you have worked with Papervision3D or another Flash 3D engine before and have at least a basic understanding of how it is used. But in any case, aside from the JiglibFlash and Papervision3D libraries, all necessary project files are included in the available download.

Download Source Files

Some Structure and a Brief Overview of JiglibFlash with Papervision3D

Before starting in on any coding, let’s take a look quick look at how JiglibFlash and Papervision3D play together. Very broadly speaking, when you combine JiglibFlash with Papervision3D, you will wind up creating two instances of each item you want added to your physics based “world”. One instance will be the visual representation of the item. This object will at some point extend Papervision3D’s DisplayObject3D class and will be added to your PV3D scene. The other instance is a physics representation of the item and will extend the JiglibFlash RigidBody class. This instance is then added to the JiglibFlash physics engine. It is this physics representation that will have various forces applied (gravity, collisions, etc), or be moved about using the RigidBody moveTo() method, but it is your visual DisplayObject3D instance that will have its appearance changed as a result of these forces.

So, for example, if you wanted to create a quick falling ball, you would first create a Papervision3D primitive Sphere instance. Then, using the mesh of that Sphere object, a JiglibFlash JSphere instance with the same dimensions would be created. Once your PV3D Sphere has been added to your Scene3D and your JSphere has been added to your physics engine instance (in the case of Papervision3D, this would be an instance of the Papervision3DPhysics class), you will see your ball start to fall. It should be mentioned at this point though, that in order to see your physics bodies move about, it is necessary to constantly update the physics engine instance. Normally this is done in an Enter Frame event handler just before you render your Papervision3D scene. To update the JiglibFlash physics engine you have two options. You can use the step() method of the physics engine instance, or you can use the static integrate() method of the JiglibFlash PhysicsSystem Singleton class. While either will work, I personally find the integrate() method produces better results. I’ll leave it to you to experiment with both and decide which works better in different circumstances.

To recap then, the general workflow for JiglibFlash and Papervision3D is this: (1) create a Papervision3D DisplayObject3D and add it to your PV3D scene. (2) create a similar JiglibFlash RigidBody object using the mesh of your PV3D object and add it to your JiglibFlash physics engine. (3) In each frame, update your physics engine, then render your PV3D scene. This is really the most important concept of this tutorial. The game we’ll create is just a practical illustration of this abstract idea. Once you understand these general concepts, though, understanding the structure of the bowling game we’re about to create as well as any future projects you may work on will be a piece of cake.

Now that we have a basic idea of how JiglibFlash and Papervision3D do their thing together, let’s consider what classes/objects we’ll require for our example bowling game. The Main class (or Document class, or Entry Point, or what have you) will simply instatiate an instance of our game and add it to the display list. That’s simple enough, so we’ll worry about that last. Because this is a fairly simple game and because we’re focussing more on JiglibFlash/PV3D integration rather than serious object oriented programming, we’ll only need a small handful of classes. In a package aptly named “game” we’ll make a BowlingGame class that will be this project’s meat and potatoes. Because we’ll want to add a PV3D viewport to the BowlingGame and eventually want to add it to Flash’s Display List, it will extend the Sprite class. In addition to BowlingGame, we’ll have a couple gravy classes, BowlingBall and BowlingPin. Those will be our physics representations of those items so will descend from the JiglibFlash RigidBody class. And since we don’t want to throw good OOP completely out the window, we’ll make a simple IBowlingObject interface for our BowlingPin and BowlingBall to implement. Now that we have an idea of where we’re going, let’s go ahead and get started.

The (I)Bowling Objects

As mentioned, our two bowling objects, BowlingBall and BowlingPin, will be physics representations of those items, so, for that reason, will extend a couple items from the JiglibFlash geometry package (which in turn extend RigidBody). Additionally, because these two objects will share most properties and methods, we’ll create the IBowlingObject interface for them to implement. Both the BowlingBall and BowlingPin will need a “resetPosition” property. This property will hold a JNumber3D (part of the JiglibFlash library) instance that will provide the 3D coordinates of where the object needs to be placed when it is returned to its original position. Both the BowlingBall and BowlingPin will also require a reset() method. This method will simply remove all forces applied to the object, reset its physical orientation, then move the object back to its starting point (the 3D point contained in its resetPosition property). With that in mind, the IBowlingObject interface will look like this then:

package game {

	import jiglib.math.JNumber3D;

	public interface IBowlingObject {
		function reset():void;
		function get resetPosition():JNumber3D;
		function set resetPosition(value:JNumber3D):void;
	}
}

BowlingBall.as will extend the JiglibFlash RigidBody, JSphere and implement IBowlingObject as so:

package game {

	import jiglib.geometry.JSphere;
	import jiglib.math.JMatrix3D;
	import jiglib.math.JNumber3D;
	import jiglib.plugin.ISkin3D;

	public class BowlingBall extends JSphere implements IBowlingObject {

		private var _resetPosition:JNumber3D;

		public function BowlingBall(skin:ISkin3D, r:Number) {
			super(skin, r);
		}

		// remove any forces, reset the orientation and move the ball to its starting position
		public function reset():void {
			clearForces();
			currentState.orientation = new JMatrix3D();
			moveTo(_resetPosition);
		}

		public function get resetPosition():JNumber3D { return _resetPosition; }

		public function set resetPosition(value:JNumber3D):void {
			_resetPosition = value;
		}
	}
}

Notice how we implement the reset() method. The clearForces() method of the RigidBody object will remove all currently applied forces. Setting the currentState.orientation property equal to a new JMatrix3D will reset the ball’s physical state. Finally we use the RigidBody’s moveTo() method to move the BowlingBall back to its resetPosition.

The BowlingPin will extend the JiglibFlash JCapsule object (sort of a tube like thing – a physics equivalent of the Papervision3D Cylinder object, which, logically enough, will be our visual representation of the BowlingPin). The BowlingPin will implement the reset() method exactly as the BowlingBall object. We need one additional method for our BowlingPin object, however. We need to a way of telling if the pin has been knocked down. The isDown() getter method does just that for us. But how do we tell if a pin is down or not? Now your first instinct (as was mine) to determine if a pin has fallen might be to take a look at its rotation properties (rotationX, rotationY, or rotationZ). Unfortunately, the JiglibFlash RigidBody objects do not have these rotation properties. Also, unfortunately, their visual counterparts (the PV3D DisplayObject3D items) will never have their rotation properties altered. What I finally opted on was looking at the transform property of the RigidBody skin property. Now this transform property is actually a JMatrix3D instance that describes the state of our visual representation’s skin. I won’t go into any detail about 3D Matrix math here (and probably couldn’t even if I wanted to), but, in a nutshell, what we are doing is checking the height of the pin and saying if it is less than half, then the pin is down. You are more than welcome to play around with this method to come up with a more accurate check, but this actually does quite nicely. The other thing we’ll test in this isDown() method is the y property of the BowlingPin. If the y value is less than -20, then the BowlngPin must have fallen right off the lane and is spinning out into the void so we can safely assume that it is down. All that said, the BowlingPin class, then, looks a little like this:

package game {

	import jiglib.geometry.JCapsule;
	import jiglib.math.JMatrix3D;
	import jiglib.math.JNumber3D;
	import jiglib.plugin.ISkin3D;

	public class BowlingPin extends JCapsule implements IBowlingObject {

		private var _resetPosition:JNumber3D;

		public function BowlingPin(skin:ISkin3D, r:Number, l:Number) {
			super(skin, r, l);
		}

		// if the pin is less that half in height or it's y value is less than -20, assume it's down
		public function get isDown():Boolean {
			return skin.transform.n22 < .5 || y < -20;
		}

		// clear any forces, reset the pin's orientation and move the pin to its starting position
		public function reset():void {
			clearForces();
			currentState.orientation = new JMatrix3D();
			moveTo(_resetPosition);
		}

		public function get resetPosition():JNumber3D { return _resetPosition; }

		public function set resetPosition(value:JNumber3D):void {
			_resetPosition = value;
		}
	}
}

Now that we’ve got this gravy ready, let’s move on to our meat and potatoes, the BowlingGame class.

BowlingGame.as Overview

Before we get our hands all dirtied up with code, let’s back up a bit and take a higher look at what our BowlingGame class does and what it will need to all that. Generally speaking here is a quick walkthrough of what our BowlingGame does. First it waits for you to set the horizontal position of the ball. Then you roll the ball down the lane and (hopefully) knock over some pins. When the roll is complete (in our case after seven seconds), it clears away the pins that have been knocked down. If this is your first roll (and you didn’t get a strike), it resets the pins that are still standing (as they may have been nudged out of place). If it’s your second roll (or you did get a strike, you lucky dog), it clears away the downed pins then resets all the pins. The ball is then reset to its original starting place and you’re ready to roll again. And so on.

So, going over these steps, what will we need to accomplish these tasks? Well, we’ll need two collections of pins – one collection will be the visual representations, the other the physics representations (our BowlingPin class, for those taking notes). While not so obvious, we’ll also want a collection of positions for those pins. We’ll also, of course, want a ball, and two Boolean values that tell us whether or not the ball has been positioned or if it is currently rolling. We’ll also require a Timer to let us know when the roll is up. Finally we’ll need something to tell us what roll we’re on, first or second. In addition to all that, we’ll also need the typical things you find in every Papervision3D application (some material bitmaps, a viewport, a scene, a camera, a render engine, etc), some variables to control our camera movement, and, let’s not forget, a JiglibFlash physics engine instance.

Once we have all those class properties set, we’ll just need a group of methods to initialize and control it all. Here then is a stripped down, bare bones look at our BowlingGame class:

package game {

	/**
	 * A stripped down skeleton version of BowlingGame class file
	 * @author Devon O Wolfgang
	 */
	public class BowlingGame extends Sprite {

		[Embed(source = "../../assets/lane_material.jpg")]
		private var LaneImage:Class;

		[Embed(source = "../../assets/ball_material.jpg")]
		private var BallImage:Class;

		private static const NUM_PINS:int = 10;
		private static const ROLL_SECONDS:int = 7;

		private var _view:Viewport3D;
		private var _scene:Scene3D;
		private var _camera:Camera3D;
		private var _engine:BasicRenderEngine;
		private var _light:PointLight3D;
		private var _physics:Papervision3DPhysics;

		private var _visualPins:Vector. = new Vector.();
		private var _physicsPins:Vector. = new Vector.();
		private var _pinPositions:Vector. = new Vector.();

		private var _physicsBall:BowlingBall;
		private var _ballIsSet:Boolean = false;
		private var _ballIsRolling:Boolean = false;

		private var _rollTimer:Timer;

		private var _currentRoll:int = 0;

		private var _moveCamRight:Boolean = false;
		private var _moveCamLeft:Boolean = false;
		private var _moveCamUp:Boolean = false;
		private var _moveCamDown:Boolean = false;
		private var _moveAmount:int = 10;

		public function BowlingGame() {
			addEventListener(Event.ADDED_TO_STAGE, init);
		}

		// initialize a few properties and call the methods that initialize everything else
		private function init(event:Event):void {
			removeEventListener(Event.ADDED_TO_STAGE, init);

			_rollTimer = new Timer(ROLL_SECONDS * 1000, 1);
			_rollTimer.addEventListener(TimerEvent.TIMER_COMPLETE, endRoll);

			// these positions were determined by trial and error - no magic here
			_pinPositions.push(new JNumber3D(0, 41, 350), new JNumber3D( -35, 41, 390), new JNumber3D(35, 41, 390), new JNumber3D(-70, 41, 430), new JNumber3D(0, 41, 430), new JNumber3D(70, 41, 430), new JNumber3D( -105, 41, 470), new JNumber3D( -35, 41, 470), new JNumber3D(35, 41, 470), new JNumber3D(105, 41, 470));

			// initialize everything
			init3d();
			initLane();
			initPins();
			initBall();
			initRollerUI();

			stage.addEventListener(KeyboardEvent.KEY_DOWN,keyDownHandler);
			stage.addEventListener(KeyboardEvent.KEY_UP, keyUpHandler);
			stage.addEventListener(MouseEvent.MOUSE_WHEEL, mouseWheelHandler);

			startRender();
		}

		// to move the camera around
		private function keyDownHandler(event:KeyboardEvent):void {
			// change the vars that will move the camera around
		}

		// to stop moving the camera around
		private function keyUpHandler(event:KeyboardEvent):void {
			// reset the vars that are moving the camera
		}

		// to move the camera forwards and backwards
		private function mouseWheelHandler(event:MouseEvent):void {
			// move the camera in and out depending on mousewheel
		}

		// initialize the papervision3d elements and the jiglibflash physics engine
		private function init3d():void {
			// initialize all the Papervision3D objects as well as our physics engine
		}

		// initialize the visual and physics elements of the lane
		private function initLane():void {
			// create a visual lane and a physics lane
		}

		// initialize the visual and physics elements of the pins
		private function initPins():void {
			// create 10 visual pins and 10 physics pins and add them to the correct collection
		}

		// initialize the visual and physics elements of the ball
		private function initBall():void {
			// create a visual ball and an instance of our BowlingBall class (the physics ball)
			// and place the ball in position
		}

		// initialize the gui elements that are used to position and roll the ball
		private function initRollerUI():void {
			//
		}

		// position the ball on the x axis
		private function setBall(event:MouseEvent):void {
			//
		}

		// roll the ball
		private function rollBall(event:MouseEvent):void {
			// start the timer counting up to 7 seconds
		}

		// called 7 seconds after the ball is rolled by default
		private function endRoll(event:TimerEvent):void {
			// reset the timer, add one to our current roll,
			// reset the ball and clear the pins away
			// the ball is no longer set or rolling
		}

		// move the ball back to its reset position
		private function resetBall():void {
			//
		}

		// remove the pins that have been knocked over
		private function clearPins():void {
			// remove the downed pins. remove the visual pin from the scene
			// and the physics pin from the physics engine.
			// if there's a strike or it's the second roll, reset all the pins
			// otherwise just reset those that haven't fallen
		}

		// reset just the pins that are still left standing
		private function resetPins():void {
			// if a pin hasn't fallen (in other words, if the pin is still in our scene),
			// reset it.
		}

		// reset all the pins
		private function resetAllPins():void {
			// if a pin has fallen, we'll have to add it back to our scene and physics engine befor we reset it
		}

		// start the enter frame event that will render the 3D scene
		private function startRender():void {
			addEventListener(Event.ENTER_FRAME, render);
		}

		// move the camera, update the physics engine, then render the 3D scene
		private function render(event:Event):void {

			// move the camera about

			// update the physics engine

			// update the 3D view
		}
	}
}

Notice that our Timer instance, _rollTimer, and the collection (a Vector object) of pin positions are created immediately after the BowlingGame has been added to the Display List, in the init() method. Everything else is delegated to separate methods which we’ll now look at in more detail.

Initializing the Third Dimension

As in every Papervision3D project, the first thing we’ll want to do is create our necessary 3D objects. These include a Scene3D, a Camera3D, a BasicRenderEngine, a PointLight3D (used for a material later on), and a Viewport3D which is then added to the Display List. In this case, we will also create an instance of the JiglibFlash Papervision3DPhysics object to use as our physics engine. We’ll pass this instance a reference to our Scene3D instance and a speed value of 3 (I just liked the way 3 looked. The default speed value is 1. You can play around and see what you think looks best). Here then is the completed init3d() method:

// initialize the papervision3d elements and the jiglibflash physics engine
private function init3d():void {
	_scene = new Scene3D();
	_camera = new Camera3D();
	_camera.zoom = 100;
	_camera.y = 200;
	_engine = new BasicRenderEngine();
	_light = new PointLight3D();
	_light.y = 400;
	_light.z = -500;
	_view = new Viewport3D(stage.stageWidth, stage.stageHeight);
	addChild(_view);

	_physics = new Papervision3DPhysics(_scene, 3);
}

Initializing the Lane

As has been pointed out, for every object in our 3D physics world we need to create both a visual representation and a physics representation. So, in the initLane() method we will create a Papervision3D Plane object for the visual representation (using a simple BitmapMaterial for skin). We’ll go ahead and use this plane as our 3D camera’s target so the camera will always be looking here despite where it may move around to. For the physics representation of the lane, we’ll use a squished JiglibFlash Jbox instance with the same dimensions as our plane. Notice also how we use the mesh of the visual lane to create the physics lane by passing in a new PV3DMesh instance to the constructor method. This is how all RigidBody instances will be created when combining JiglibFlash with Papervision3D. We don’t want the lane to be affected by gravity or other forces, so we’ll set its movable property to false. Then we’ll add the the physics lane to our physics engine instance using the addBody() method. Here is the initLane() method:

// initialize the visual and physics elements of the lane
private function initLane():void {
	var laneMat:BitmapMaterial = new BitmapMaterial(new LaneImage().bitmapData);
	var visualLane:Plane = new Plane(laneMat, 250, 1000, 8, 32);
	visualLane.rotationX = 90;
	_camera.target = visualLane;
	_scene.addChild(visualLane);

	var physicsLane:JBox = new JBox(new Pv3dMesh(visualLane), 250, 1, 1000);
	_physics.addBody(physicsLane);
	physicsLane.movable = false;
	physicsLane.rotationX = 90;
}

Initializing the Pins

For the visual pins, we’ll use a Papervision3D GouraudMaterial over a Cylinder instance to make them nice and smooth and shadowy.

NOTE: I originally attempted using a DAE model created in Swift3D for the visual representations of the pins, but found this too processor intensive. If you would like to play around with the idea, though, the .dae file is included in with the download files.

After creating the material, we’ll loop through the number of pins (uh, that’s ten, if you’re not a bowler. Just to be on the safe size, I stored this number in the constant property NUM_PINS) and create a Cylinder instance for a visual representation and an instance of our BowlingPin class for the physics instance. We’ll add these items to their respective collections (push them into Vectors, that is). For each physics representation, we’ll set its resetPosition property from the collection of pin positions we made in our init() method, call its reset() method to position it, make it movable, add a little mass so it doesn’t knock down too easy, then add it to our physics engine instance. Each visual pin (PV3D Cylinder) is given a name so it may be easily referenced later and is added to our Scene3D instance so we can actually see the pins. The initPins() method then, winds up looking like this:

// initialize the visual and physics elements of the pins
private function initPins():void {
	var mat:GouraudMaterial = new GouraudMaterial(_light);

	for (var i:int = 0; i < NUM_PINS; i++) {
		var visualPin:Cylinder = new Cylinder(mat, 10, 75);
		visualPin.name = String("pin_" + i);
		_visualPins.push(visualPin);
		_scene.addChild(visualPin);

		var physicsPin:BowlingPin = new BowlingPin(new Pv3dMesh(visualPin), 10, 55);
		physicsPin.resetPosition = _pinPositions[i];
		physicsPin.reset();
		physicsPin.movable = true;
		physicsPin.mass = 3;
		_physics.addBody(physicsPin);
		_physicsPins.push(physicsPin);
	}
}

Initializing the Ball

This method is very much like the previous, except without a loop as we only need one. We create a visual Papervision3D Sphere instance using another simple BitmapMaterial and add it to our Scene3D. Then we create an instance of our BowlingBall class, set its resetPosition property, add it to our physics engine and call its reset() method (via the resetBall() method) to move it into position. Starting to be old hat, now.

// initialize the visual and physics elements of the ball
private function initBall():void {
	var ballMat:BitmapMaterial = new BitmapMaterial(new BallImage().bitmapData);
	var visualBall:Sphere = new Sphere(ballMat, 20);
	_scene.addChild(visualBall);

	_physicsBall = new BowlingBall(new Pv3dMesh(visualBall), 20);
	_physicsBall.resetPosition = new JNumber3D(0, 50, -450);
	_physics.addBody(_physicsBall);
	resetBall();
}

Initializing the GUI

The simple initRollerGUI() method merely creates two Sprite instances using the Flash drawing API and adds listeners to them so they respond to mouse clicks to either position or roll the ball. Kinda like this:

// initialize the gui elements that are used to position and roll the ball
private function initRollerUI():void {
	var ballSetter:Sprite = new Sprite();
	ballSetter.graphics .beginFill(0x006600);
	ballSetter.graphics.drawRect(0, 0, 100, 10);
	ballSetter.graphics.endFill();
	ballSetter.addEventListener(MouseEvent.CLICK, setBall);
	ballSetter.x = 4;
	ballSetter.y = 4;
	addChild(ballSetter);

	var ballRoller:Sprite = new Sprite();
	ballRoller.graphics.beginFill(0x660000);
	ballRoller.graphics.drawRect(0, 0, 100, 100);
	ballRoller.graphics.endFill();
	ballRoller.addEventListener(MouseEvent.CLICK, rollBall);
	ballRoller.x = 4;
	ballRoller.y = ballSetter.y + ballSetter.height + 4;
	addChild(ballRoller);
}

Positioning and Rolling the Ball

The Sprites created in the previous step use the setBall() and rollBall() methods as listeners for their click events. If the ball hasn’t already been positioned and isn’t currently rolling, the setBall() method will tell the application that the ball has been positioned, then takes the mouseX position where the green rectangle was clicked and converts it to a number between -1 and 1. It multiplies that ratio by 100 (just found by trial and error) and moves our _physicsBall instance to that x position using the RigidBody moveTo() method like so:

// position the ball on the x axis
private function setBall(event:MouseEvent):void {
	if (!_ballIsSet && !_ballIsRolling) {
		_ballIsSet = true;

		// between -1 and 1
		var xpos:Number = ((event.currentTarget.mouseX / event.currentTarget.width) - .5) * 2;
		xpos *= 100;

		_physicsBall.moveTo(new JNumber3D(xpos, 20, -450));
	}
}

If the ball has been positioned with the setBall() method and is not currently rolling, the rollBall() method notifies the application that the ball is now rolling (i.e. _ballIsRolling = true), then finds an xforce and zforce according to where the red square was clicked (much like the setBall() method determined an xpos). Then comes the fun part. To actually get the ball moving, we add a world force to it with the appropriately named addWorldForce() method of the RigidBody class. The addWorldForce() method takes two JNumber3D instances as arguments. The first is the force to add to the RigidBody. The x and z properties of this JNumber3D are the xforce and zforce numbers we just created. We’ll leave the y property at 0 as we don’t want the ball moving up or down. The second JNumber3D is the force currently applied to the RigidBody (which can be determined from the object’s currentState.position property). Finally we start our rollTimer so that seven seconds later our roll will come to an end. All together, the rollBall() method looks like this:

// roll the ball
private function rollBall(event:MouseEvent):void {
	if (_ballIsSet && !_ballIsRolling) {
		_ballIsRolling = true;

		var xforce:Number = ((event.currentTarget.mouseX / event.currentTarget.width) - .5) * 2;
		xforce *= 500;

		var zforce:Number = (event.currentTarget.mouseY / event.currentTarget.height) - 1;
		zforce *= -850;

		_physicsBall.addWorldForce(new JNumber3D(xforce, 0, zforce), _physicsBall.currentState.position);

		_rollTimer.start();
	}
}

Ending the Roll

So, how to tell when the roll is over? Originally, I thought I would wait until the ball reached a certain -y position (meaning it had fallen off the lane), to notify the application that the roll was over. I found, though, that sometimes the ball never left the lane and other times it left the lane so quickly that pins that would have fallen weren’t given the opportunity to do so. After trying a couple other ideas, I finally decided on an arbitrary amount of time of 7 seconds because (a) it seemed to work quite well, and (b) 7 Seconds was a great hardcore punk band back in the 80’s. If desired, though, you can adjust this amount of time to taste by playing with the ROLL_SECONDS constant property.

In any case, once the roll is complete, the rollTimer instance calls the endRoll() method. This method resets the timer, bumps up the current roll, calls the resetBall() and clearPins() methods, and lets the application know that the ball is neither set in position nor rolling. The endRoll() method looks like this:

// called 7 seconds after the ball is rolled by default
private function endRoll(event:TimerEvent):void {
	_rollTimer.reset();
	_currentRoll++;
	resetBall();
	clearPins();
	_ballIsRolling = false;
	_ballIsSet = false;
}

The resetBall() method is obvious. It merely calls the reset() method of our BowlingBall instance. Let’s take a look at that clearPins() method though. The first thing clearPins() does is set a counter variable, that will count the number of fallen pins, to zero.

HINT: If you’re thinking of implementing a scoring system, this would be a good place to start.

Next we loop through our Vector of BowlingPins and check if each pin is down (you remember the isDown() method we wrote earlier, right?). If the pin is down, that physics representation is removed from our physics engine instance using the removeBody() method while its visual counterpart is removed from the PV3D scene with removeChild(), and the counter is increased by 1. After the loop, we take a look at the counter to see if all 10 pins are down. If they are, we’ll go ahead and call that roll number 2 (no sense rolling again – you just got a strike, champ!). Next we say, if that was roll number 2, let’s reset all of the pins and change our current roll back to 0. Otherwise (if it was still roll 1), we only reset those pins that are left still standing (as they may have been bumped a bit, or even still teetering around). In code form, the clearPins() method looks like this:

// remove the pins that have been knocked over
private function clearPins():void {
	var numPinsDown:int = 0;
	for (var i:int = 0; i < NUM_PINS; i++) {
		if (_physicsPins[i].isDown) {
			_physics.removeBody(_physicsPins[i]);
			_scene.removeChild(_visualPins[i]);
			numPinsDown++;
		}
	}

	// if there's a STRIKE, don't roll a second time
	if (numPinsDown == NUM_PINS) _currentRoll = 2;

	if (_currentRoll == 2) {
		resetAllPins();
		_currentRoll = 0;
	} else {
		resetPins();
	}
}

Now comes the tricky parts – well, they were tricky for me, anyway – resetting the pins. Let’s begin by resetting the pins that are still standing. The resetPins() method loops through number of pins (ten, still), and looks for each pin by name within the Scene3D instance (recall how we set the name property when we created each visual pin). If the pin can be found in the scene (i.e. it isn’t null) then it must not have fallen (because we removed all the downed pins from the scene, y’know). If it hasn’t fallen, then, we call the reset() method of the physics counterpart of the visual pin still there (told you it was a little tricky). Here is the resetPins() method:

// reset just the pins that are still left standing
private function resetPins():void {
	for (var i:int = 0; i < NUM_PINS; i++) {
		var p:DisplayObject3D = _scene.getChildByName(String("pin_" + i));
		if (p != null) _physicsPins[i].reset();
	}
}

The resetAllPins() method is very similar to resetPins(), but kind of backwards. That is, this time as we loop through the 10 pins, if we can’t find a visual pin in our scene, we both add the visual pin back to the scene and add its physics representation counterpart back to the physics engine. Finally, we reset the BowlingPin instance. And here we have resetAllPins():

// reset all the pins
private function resetAllPins():void {
	for (var i:int = 0; i < NUM_PINS; i++) {
		var p:DisplayObject3D = _scene.getChildByName(String("pin_" + i));
		if (p == null) {
			p = _visualPins[i] as DisplayObject3D;
			_scene.addChild(p);
			_physics.addBody(_physicsPins[i]);
		}
		_physicsPins[i].reset();
	}
}

So Let’s See It

Finally, to make everything up to this point actually appear on the screen, we come to our render() method. This method is called in each frame of our application and is actually quite simple. First, it moves the camera around if necessary (I won’t event bother explaining the camera movements. If you take a look through the code, it’s all quite self-explanatory). Next, it updates the physics engine. As mentioned previously, we could either do this with the step() method of the physics engine instance, or we could use the static integrate() method of the PhysicsSystem Singleton class. I prefer the integrate() method, but try both and see which you like best. Finally, the render() method uses our BasicRenderEngine instance to render our Papervision3D Scene3D. The render() method, then, looks like this:

// move the camera, update the physics engine, then render the 3D scene
private function render(event:Event):void {

	if (_moveCamRight) {
		if (_camera.x < 1000) _camera.x += _moveAmount;
	}

	if (_moveCamLeft) {
		if (_camera.x > -1000) _camera.x -= _moveAmount;
	}

	if (_moveCamUp) {
		if (_camera.y < 500) _camera.y += _moveAmount;
	}

	if (_moveCamDown) {
		if (_camera.y > 50) _camera.y -= _moveAmount;
	}

	PhysicsSystem.getInstance().integrate(.2);
	// Could use
	//_physics.step();
	// instead

	_engine.renderScene(_scene, _camera, _view);
}

And finally, the moment I’m sure you’ve been waiting for, if we put it all together, this is our BowlingGame.as class in its glorious entirety:

package game {

	// Flash items
	import flash.display.Sprite;
	import flash.events.KeyboardEvent;
	import flash.events.TimerEvent;
	import flash.ui.Keyboard;
	import flash.utils.Timer;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import jiglib.math.JMatrix3D;

	// JiglibFlash items
	import jiglib.geometry.JBox;
	import jiglib.geometry.JCapsule;
	import jiglib.geometry.JSphere;
	import jiglib.math.JNumber3D;
	import jiglib.physics.PhysicsSystem;
	import jiglib.plugin.papervision3d.Papervision3DPhysics;
	import jiglib.plugin.papervision3d.Pv3dMesh;

	// Papervision3D items
	import org.papervision3d.cameras.Camera3D;
	import org.papervision3d.lights.PointLight3D;
	import org.papervision3d.materials.BitmapMaterial;
	import org.papervision3d.materials.shadematerials.GouraudMaterial;
	import org.papervision3d.objects.DisplayObject3D;
	import org.papervision3d.objects.primitives.Cylinder;
	import org.papervision3d.objects.primitives.Plane;
	import org.papervision3d.objects.primitives.Sphere;
	import org.papervision3d.render.BasicRenderEngine;
	import org.papervision3d.scenes.Scene3D;
	import org.papervision3d.view.Viewport3D;

	/**
	 * 3D Bowling game with physics engine
	 * @author Devon O Wolfgang
	 */
	public class BowlingGame extends Sprite {

		[Embed(source = "../../assets/lane_material.jpg")]
		private var LaneImage:Class;

		[Embed(source = "../../assets/ball_material.jpg")]
		private var BallImage:Class;

		private const NUM_PINS:int = 10;
		private const ROLL_SECONDS:int = 7;

		private var _view:Viewport3D;
		private var _scene:Scene3D;
		private var _camera:Camera3D;
		private var _engine:BasicRenderEngine;
		private var _light:PointLight3D;
		private var _physics:Papervision3DPhysics;

		private var _visualPins:Vector. = new Vector.();
		private var _physicsPins:Vector. = new Vector.();
		private var _pinPositions:Vector. = new Vector.();

		private var _physicsBall:BowlingBall;
		private var _ballIsSet:Boolean = false;
		private var _ballIsRolling:Boolean = false;

		private var _rollTimer:Timer;

		private var _currentRoll:int = 0;

		private var _moveCamRight:Boolean = false;
		private var _moveCamLeft:Boolean = false;
		private var _moveCamUp:Boolean = false;
		private var _moveCamDown:Boolean = false;
		private var _moveAmount:int = 10;

		public function BowlingGame() {
			addEventListener(Event.ADDED_TO_STAGE, init);
		}

		// initialize a few properties and call the methods that initialize everything else
		private function init(event:Event):void {
			removeEventListener(Event.ADDED_TO_STAGE, init);

			_rollTimer = new Timer(ROLL_SECONDS * 1000, 1);
			_rollTimer.addEventListener(TimerEvent.TIMER_COMPLETE, endRoll);

			// these positions were determined by trial and error - no magic here
			_pinPositions.push(new JNumber3D(0, 41, 350), new JNumber3D( -35, 41, 390), new JNumber3D(35, 41, 390), new JNumber3D(-70, 41, 430), new JNumber3D(0, 41, 430), new JNumber3D(70, 41, 430), new JNumber3D( -105, 41, 470), new JNumber3D( -35, 41, 470), new JNumber3D(35, 41, 470), new JNumber3D(105, 41, 470));

			init3d();
			initLane();
			initPins();
			initBall();
			initRollerUI();

			stage.addEventListener(KeyboardEvent.KEY_DOWN,keyDownHandler);
			stage.addEventListener(KeyboardEvent.KEY_UP, keyUpHandler);
			stage.addEventListener(MouseEvent.MOUSE_WHEEL, mouseWheelHandler);

			startRender();
		}

		// to move the camera around
		private function keyDownHandler(event:KeyboardEvent):void {
			if (event.keyCode == Keyboard.UP) _moveCamUp = true;
			if (event.keyCode == Keyboard.DOWN) _moveCamDown = true;
			if (event.keyCode == Keyboard.LEFT) _moveCamLeft = true;
			if (event.keyCode == Keyboard.RIGHT) _moveCamRight = true;
		}

		// to stop moving the camera around
		private function keyUpHandler(event:KeyboardEvent):void {
			if (event.keyCode == Keyboard.UP) _moveCamUp = false;
			if (event.keyCode == Keyboard.DOWN) _moveCamDown = false;
			if (event.keyCode == Keyboard.LEFT) _moveCamLeft = false;
			if (event.keyCode == Keyboard.RIGHT) _moveCamRight = false;
		}

		// to move the camera forwards and backwards
		private function mouseWheelHandler(event:MouseEvent):void {
			_camera.z += _moveAmount * event.delta;
			if (_camera.z < -1500) _camera.z = -1500;
			if (_camera.z > -200) _camera.z = -200;
		}

		// initialize the papervision3d elements and the jiglibflash physics engine
		private function init3d():void {
			_scene = new Scene3D();
			_camera = new Camera3D();
			_camera.zoom = 100;
			_camera.y = 200;
			_engine = new BasicRenderEngine();
			_light = new PointLight3D();
			_light.y = 400;
			_light.z = -500;
			_view = new Viewport3D(stage.stageWidth, stage.stageHeight);
			addChild(_view);

			_physics = new Papervision3DPhysics(_scene, 3);
		}

		// initialize the visual and physics elements of the lane
		private function initLane():void {
			var laneMat:BitmapMaterial = new BitmapMaterial(new LaneImage().bitmapData);
			var visualLane:Plane = new Plane(laneMat, 250, 1000, 8, 32);
			visualLane.rotationX = 90;
			_camera.target = visualLane;
			_scene.addChild(visualLane);

			var physicsLane:JBox = new JBox(new Pv3dMesh(visualLane), 250, 1, 1000);
			_physics.addBody(physicsLane);
			physicsLane.movable = false;
			physicsLane.rotationX = 90;
		}

		// initialize the visual and physics elements of the pins
		private function initPins():void {
			var mat:GouraudMaterial = new GouraudMaterial(_light);

			for (var i:int = 0; i < NUM_PINS; i++) {
				var visualPin:Cylinder = new Cylinder(mat, 10, 75);
				visualPin.name = String("pin_" + i);
				_visualPins.push(visualPin);
				_scene.addChild(visualPin);

				var physicsPin:BowlingPin = new BowlingPin(new Pv3dMesh(visualPin), 10, 55);
				physicsPin.resetPosition = _pinPositions[i];
				physicsPin.reset();
				physicsPin.movable = true;
				physicsPin.mass = 3;
				_physics.addBody(physicsPin);
				_physicsPins.push(physicsPin);
			}
		}

		// initialize the visual and physics elements of the ball
		private function initBall():void {
			var ballMat:BitmapMaterial = new BitmapMaterial(new BallImage().bitmapData);
			var visualBall:Sphere = new Sphere(ballMat, 20);
			_scene.addChild(visualBall);

			_physicsBall = new BowlingBall(new Pv3dMesh(visualBall), 20);
			_physicsBall.resetPosition = new JNumber3D(0, 50, -450);
			_physics.addBody(_physicsBall);
			resetBall();
		}

		// initialize the gui elements that are used to position and roll the ball
		private function initRollerUI():void {
			var ballSetter:Sprite = new Sprite();
			ballSetter.graphics .beginFill(0x006600);
			ballSetter.graphics.drawRect(0, 0, 100, 10);
			ballSetter.graphics.endFill();
			ballSetter.addEventListener(MouseEvent.CLICK, setBall);
			ballSetter.x = 4;
			ballSetter.y = 4;
			addChild(ballSetter);

			var ballRoller:Sprite = new Sprite();
			ballRoller.graphics.beginFill(0x660000);
			ballRoller.graphics.drawRect(0, 0, 100, 100);
			ballRoller.graphics.endFill();
			ballRoller.addEventListener(MouseEvent.CLICK, rollBall);
			ballRoller.x = 4;
			ballRoller.y = ballSetter.y + ballSetter.height + 4;
			addChild(ballRoller);
		}

		// position the ball on the x axis
		private function setBall(event:MouseEvent):void {
			if (!_ballIsSet && !_ballIsRolling) {
				_ballIsSet = true;

				var xpos:Number = ((event.currentTarget.mouseX / event.currentTarget.width) - .5) * 2;
				xpos *= 100;

				_physicsBall.moveTo(new JNumber3D(xpos, 20, -450));
			}
		}

		// roll the ball
		private function rollBall(event:MouseEvent):void {
			if (_ballIsSet && !_ballIsRolling) {
				_ballIsRolling = true;

				var xforce:Number = ((event.currentTarget.mouseX / event.currentTarget.width) - .5) * 2;
				xforce *= 500;

				var zforce:Number = (event.currentTarget.mouseY / event.currentTarget.height) - 1;
				zforce *= -850;

				_physicsBall.addWorldForce(new JNumber3D(xforce, 0, zforce), _physicsBall.currentState.position);

				_rollTimer.start();
			}
		}

		// called 7 seconds after the ball is rolled by default
		private function endRoll(event:TimerEvent):void {
			_rollTimer.reset();
			_currentRoll++;
			resetBall();
			clearPins();
			_ballIsRolling = false;
			_ballIsSet = false;
		}

		// move the ball back to its reset position
		private function resetBall():void {
			_physicsBall.reset();
		}

		// remove the pins that have been knocked over
		private function clearPins():void {
			var numPinsDown:int = 0;
			for (var i:int = 0; i < NUM_PINS; i++) {
				if (_physicsPins[i].isDown) {
					_physics.removeBody(_physicsPins[i]);
					_scene.removeChild(_visualPins[i]);
					numPinsDown++;
				}
			}

			// if there's a STRIKE, don't roll a second time
			if (numPinsDown == NUM_PINS) _currentRoll = 2;

			if (_currentRoll == 2) {
				resetAllPins();
				_currentRoll = 0;
			} else {
				resetPins();
			}
		}

		// reset just the pins that are still left standing
		private function resetPins():void {
			for (var i:int = 0; i < NUM_PINS; i++) {
				var p:DisplayObject3D = _scene.getChildByName(String("pin_" + i));
				if (p != null) _physicsPins[i].reset();
			}
		}

		// reset all the pins
		private function resetAllPins():void {
			for (var i:int = 0; i < NUM_PINS; i++) {
				var p:DisplayObject3D = _scene.getChildByName(String("pin_" + i));
				if (p == null) {
					p = _visualPins[i] as DisplayObject3D;
					_scene.addChild(p);
					_physics.addBody(_physicsPins[i]);
				}
				_physicsPins[i].reset();
			}
		}

		// start the enter frame event that will render the 3D scene
		private function startRender():void {
			addEventListener(Event.ENTER_FRAME, render);
		}

		// move the camera, update the physics engine, then render the 3D scene
		private function render(event:Event):void {

			if (_moveCamRight) {
				if (_camera.x < 1000) _camera.x += _moveAmount;
			}

			if (_moveCamLeft) {
				if (_camera.x > -1000) _camera.x -= _moveAmount;
			}

			if (_moveCamUp) {
				if (_camera.y < 500) _camera.y += _moveAmount;
			}

			if (_moveCamDown) {
				if (_camera.y > 50) _camera.y -= _moveAmount;
			}

			PhysicsSystem.getInstance().integrate(.2);
			// Could use
			//_physics.step();
			// instead

			_engine.renderScene(_scene, _camera, _view);
		}
	}
}

Bringin’ it all Back Home

You can now take a deep breath. The hard part’s over. Because our BowlingGame class extends Flash’s Sprite class, we can use our document class Main.as to simply add it to the Display List and you’re good to go. Although you may want to display some basic instructions first as I did here in my Main.as file:

package {

	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.KeyboardEvent;
	import flash.text.AntiAliasType;
	import flash.text.TextField;
	import flash.text.TextFieldAutoSize;
	import flash.text.TextFormat;
	import flash.text.TextFormatAlign;
	import flash.ui.Keyboard;
	import game.BowlingGame;

	/**
	 * Document class for 3D Bowling Game
	 * @author Devon O Wolfgang
	 */

	[SWF(width='540', height='450', backgroundColor='#000000', frameRate='60')]
	public class Main extends Sprite {

		private var _game:BowlingGame;
		private var _startText:TextField;

		public function Main():void {
			if (stage) init();
			else addEventListener(Event.ADDED_TO_STAGE, init);
		}

		private function init(e:Event = null):void {
			removeEventListener(Event.ADDED_TO_STAGE, init);

			initStartText();

			stage.addEventListener(KeyboardEvent.KEY_DOWN, startGame);
		}

		// Add some intstructions
		private function initStartText():void {
			var fmt:TextFormat = new TextFormat("_sans", 18, 0xFFFFFF);
			fmt.align = TextFormatAlign.CENTER;
			_startText = new TextField();
			_startText.defaultTextFormat = fmt;
			_startText.mouseEnabled = false;
			_startText.selectable = false;
			_startText.multiline = true;
			_startText.antiAliasType = AntiAliasType.ADVANCED;
			_startText.autoSize = TextFieldAutoSize.LEFT;
			_startText.text = "To Play:\n\n1. click the green rectangle to position the ball.\n2. click the red square to roll the ball.\n3. use arrow keys and mouse wheel to move camera.\n\nPRESS SPACE TO BEGIN";
			_startText.x = Math.round(stage.stageWidth * .5 - _startText.width * .5);
			_startText.y = Math.round(stage.stageHeight * .5 - _startText.height * .5);
			addChild(_startText);
		}

		// Create the actual game
		private function initGame():void {
			_game = new BowlingGame();
			addChild(_game);
		}

		// Remove the instructions and create the game
		private function startGame(event:KeyboardEvent):void {
			if (event.keyCode == Keyboard.SPACE) {
				stage.addEventListener(KeyboardEvent.KEY_DOWN, startGame);
				removeChild(_startText);
				initGame();
			}
		}
	}
}

And that is that. Compile Main.as with FlexBuilder, FlashBuilder, FlashDevelop, or Flash CS4 and you have a physics based bowling game built with JiglibFlash and Papervision3D. Hopefully along the way though, you also picked up some good tips on thow to integrate these two powerhouses to use in your own games or applications. Or you can just say, “F*ck it, Dude. Let’s go bowling…”

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>