How to make a platform engine in QuickBox2D

Check out part 2 RIGHT HERE

What’s up? Ready to learn a little something about QuickBox2D? Wait, you don’t know what QuickBox2D is? Well, QuickBox2D is a simpler way of using Box2D — A physics library originally written in C++ and ported to many other platforms including AS3 — and is much cleaner and easier to use than the straight Box2D library. Anyways, QuickBox2D was made by Zevan Rosser, who is a teacher at the School of Visual Arts in NYC. You can see a lot of great examples of how to use QuickBox2D at his blog, ActionSnippet.

Let’s get started here. First thing you need to do is get the latest version of Box2D and QuickBox2D at these links:
Box2DFlashAS3
QuickBox2D
And Save them in a new folder called ‘PhysicsPlatformer’.

Now open Flash CS3/4 and create a new Flash AS3 Document File. Go back to the File menu button and add another file, this time we are going to add an Actionscript Document (.as file). Save the .fla as game.fla, and save the .as file as global.as.

The global.as file is going to contain one variable (kind of a waste, I know, but it can be useful when you expand your engine a little bit more, like for implementing wall-jumping, it could be useful). Since global.as is the smallest, we’ll start with that. This is the code that should be in your global.as class file:

package {

	public class global {

		public static var grounded:Boolean = false;

		public function global():void {

		}
	}
}

The important thing to notice here is ‘static’ on line 5. When you use the word static in the definition of a variable, it means that you can access the variable by stating the class name then the variable name without instantiating the class. This is really great because when we need to access our boolean, grounded, all we type is “global.grounded”.

The next part is probably the hardest part to do, actually making the engine. When I was in works of creating the engine, I had some real trouble getting the main character (a ball in our case) to stay still on a slope, and i came up with an ingenious way of fixing this problem, and you’ll see in the tutorial here how I did it.

Lets start by setting up our QuickBox2D simulation:

import com.actionsnippet.qbox.*;

var sim:QuickBox2D = new QuickBox2D(this);
sim.setDefault({lineAlpha:0, fillColor:0x333333});
sim.createStageWalls();

sim.start();

If you paste this in the timeline of game.fla and test it, you should see a dark grey box surrounding your stage, If not, then look through your code checking for typos or something because we have a long way to go.

Next we can add our character, a simple ball will do:

import com.actionsnippet.qbox.*;

var sim:QuickBox2D = new QuickBox2D(this);
sim.setDefault({lineAlpha:0, fillColor:0x333333});
sim.createStageWalls();

var main:QuickObject = sim.addCircle({x:3, y:3, restitution:0, lineAlpha:1, fillColor:0x888888, allowSleep:false, fixedRotation:true});

sim.start();

There is obviously only one addition to this, it’s on line 7. First we make a new variable that’s typcasted as a QuickObject, then we set it equal to the results of the addCircle function. You’ll notice in the function that there is one argument, and it’s an object with a lot of stuff in it. I’ll point out the most important parts of those parameters. allowSleeping: false. If our object could ‘sleep’ then we would not be able to move it unless we wake it up, I suppose it’s fine to leave this as true (by default) and then whenever you want to move the character wake it up, but it’s not making the hugest impact on the game right now, so don’t worry about it. And, fixedRotation: true. Let’s say we are controlling our circle and we get to a slope, and we are trucking up the slope, but then we have to stop for a second because a friend is calling or something, when we let go of our keys, the ball will just start rolling down the slope, and we can’t have that.

So, great, we have a ball. How do we move it? We move the ball by adjusting it’s linear velocity. But first we have to set up a few functions to handle our keyboard input and such:

import com.actionsnippet.qbox.*;
import Box2D.Common.Math.*

var sim:QuickBox2D = new QuickBox2D(this);
sim.setDefault({lineAlpha:0, fillColor:0x333333});
sim.createStageWalls();

var main:QuickObject = sim.addCircle({x:3, y:3, restitution:0, lineAlpha:1, fillColor:0x888888, allowSleep:false, fixedRotation:true});

sim.start();

stage.addEventListener(Event.ENTER_FRAME, loop);
stage.addEventListener(KeyboardEvent.KEY_UP, checkKeysUp);
stage.addEventListener(KeyboardEvent.KEY_DOWN, checkKeysDown);

var keyArray:Array = new Array();
var speed:int = 5;
for (var i=0; i<222; i++) {
	keyArray.push(false);
}

function loop(e:Event):void {
	if(keyisdown(39)){
		main.body.SetLinearVelocity(new b2Vec2(speed, main.body.GetLinearVelocity().y));
	}
	if(keyisdown(37)){
		main.body.SetLinearVelocity(new b2Vec2(-speed, main.body.GetLinearVelocity().y));
	}
}

function checkKeysDown(event:KeyboardEvent):void {
	keyArray[event.keyCode]=true;
}
function checkKeysUp(event:KeyboardEvent):void {
	keyArray[event.keyCode]=false;
}
function keyisdown(X:Number):Boolean {
	return keyArray[X];
}

You’ll notice that there is A LOT of new code here, such as line 2, and pretty much the second half of the code. Don’t worry about the last 3 functions, those handle the keyboard input and modify/access the array to make it easier to use in a loop. The most important part is in our loop function with the key checking statements. When we press keycode 39 or arrow right, set our linear velocity of our box to speed (which is at 5 units), and vice versa for when we press the left arrow, set it to negative speed. To prevent a potential problem of being able to float if we go off of a platform, we get our objects current y velocity with main.body.GetLinearVelocity().y.

Well, this is great, let’s add the ability to jump! What we need to do to get a fully functional jump is modify the b2ContactListener.as file. The b2ContactListener class has 4 methods that execute when a certain part of a collision happens, such as when a new collision is detected, or there is another collision going on, or the collision stops happening (the objects were separated). We are going to capitalize on these methods, and use them to change our grounded variable. The b2ContactListener file is located in your Box2D>Dynamics folder, now, open it in flash. You need to change the file so that it looks like this:

package Box2D.Dynamics{

import Box2D.Collision.*;
import Box2D.Collision.Shapes.*;
import Box2D.Dynamics.Contacts.*;
import Box2D.Dynamics.*;
import Box2D.Common.Math.*;
import Box2D.Common.*;

/// Implement this class to get collision results. You can use these results for
/// things like sounds and game logic. You can also get contact results by
/// traversing the contact lists after the time step. However, you might miss
/// some contacts because continuous physics leads to sub-stepping.
/// Additionally you may receive multiple callbacks for the same contact in a
/// single time step.
/// You should strive to make your callbacks efficient because there may be
/// many callbacks per time step.
/// @warning The contact separation is the last computed value.
/// @warning You cannot create/destroy Box2D entities inside these callbacks.
public class b2ContactListener
{

	// /// Called after a contact point is added.
	public virtual function Add(point:b2ContactPoint):void {

			trace(point.normal.x+", "+point.normal.y);
			if(point.normal.y >= -1 && point.normal.y < 0){
				global.grounded = true;
			}

		}

		/// Called when a contact point persists. This includes the geometry
		/// and the forces.
		public virtual function Persist(point:b2ContactPoint):void {
			if(point.normal.y >= -1 && point.normal.y < 0){
				global.grounded = true;
			}
		}

		/// Called when a contact point is removed. This includes the last
		/// computed geometry and forces.
		public virtual function Remove(point:b2ContactPoint):void {
			global.grounded = false;
		}

		/// Called after a contact point is solved.
		public virtual function Result(point:b2ContactResult):void {
		}

};

}

It’s pretty self explanatory, so I’m not going to explain anything, if you need me to explain anything, feel free to post in the comments.
Back in our timeline code we need add support for our jump key, I’m going to use arrow up, but you can use the space bar if you want to, all you have to do is change the 38 to a 32 in the statement. We also need to add a new instance of our contact listener:

import com.actionsnippet.qbox.*;
import Box2D.Common.Math.*
import Box2D.Dynamics.*;

var sim:QuickBox2D = new QuickBox2D(this);
sim.setDefault({lineAlpha:0, fillColor:0x333333});
sim.createStageWalls();

var m_contactListener = new b2ContactListener();
sim.w.SetContactListener(m_contactListener);

var main:QuickObject = sim.addCircle({x:3, y:3, restitution:0, lineAlpha:1, fillColor:0x888888, allowSleep:false, fixedRotation:true});

sim.start();

stage.addEventListener(Event.ENTER_FRAME, loop);
stage.addEventListener(KeyboardEvent.KEY_UP, checkKeysUp);
stage.addEventListener(KeyboardEvent.KEY_DOWN, checkKeysDown);

var keyArray:Array = new Array();
var speed:int = 5;
var jumpSpeed:int = -12;

for (var i=0; i<222; i++) {
	keyArray.push(false);
}

function loop(e:Event):void {
	if(keyisdown(39)){
		main.body.SetLinearVelocity(new b2Vec2(speed, main.body.GetLinearVelocity().y));
	}
	if(keyisdown(37)){
		main.body.SetLinearVelocity(new b2Vec2(-speed, main.body.GetLinearVelocity().y));
	}
	if(keyisdown(38)){
		if(global.grounded){
			main.body.SetLinearVelocity(new b2Vec2(main.body.GetLinearVelocity().x, jumpSpeed));
		}
	}
}

function checkKeysDown(event:KeyboardEvent):void {
	keyArray[event.keyCode]=true;
}
function checkKeysUp(event:KeyboardEvent):void {
	keyArray[event.keyCode]=false;
}
function keyisdown(X:Number):Boolean {
	return keyArray[X];
}

On lines 9 and 10 we make a new contact listener, and we then go ahead and set our b2World’s contact listener in the simulation. Now whenever there is a collision those methods [in the contact listener] will be called.
On line 22 we make a simple variable to control jump height. This value must be negative, because if was positive you’d be jumping downward…
Finally, starting on line 35 we have the code that handles our jumping, and it’s a simple modification of our characters Y linear velocity, that we set to our jumpSpeed variable. Also, we wrapped that code in a conditional that checks if we are grounded. Because if we weren’t grounded we’d just fly around the screen.

Wow, so jumping around a rectangle is pretty fun… BUT, some obstacles and slopes would make it even better!

import com.actionsnippet.qbox.*;
import Box2D.Common.Math.*
import Box2D.Dynamics.*;

var sim:QuickBox2D = new QuickBox2D(this);
sim.setDefault({lineAlpha:0, fillColor:0x333333});
sim.createStageWalls();

var m_contactListener = new b2ContactListener();
sim.w.SetContactListener(m_contactListener);

sim.addBox({x:400/30, y:13, width:4, height:4, angle:45/(180/Math.PI), density:0});
sim.addBox({x:4, y:11, width:8, height:0.5, density:0});
sim.addBox({x:0, y:8, width:3, height:3, angle:45/(180/Math.PI), density:0});

var main:QuickObject = sim.addCircle({x:3, y:3, restitution:0, lineAlpha:1, fillColor:0x888888, allowSleep:false, fixedRotation:true});

sim.start();

stage.addEventListener(Event.ENTER_FRAME, loop);
stage.addEventListener(KeyboardEvent.KEY_UP, checkKeysUp);
stage.addEventListener(KeyboardEvent.KEY_DOWN, checkKeysDown);

var keyArray:Array = new Array();
var speed:int = 5;
var jumpSpeed:int = -12;

for (var i=0; i<222; i++) {
	keyArray.push(false);
}

function loop(e:Event):void {
	if(keyisdown(39)){
		main.body.SetLinearVelocity(new b2Vec2(speed, main.body.GetLinearVelocity().y));
	}
	if(keyisdown(37)){
		main.body.SetLinearVelocity(new b2Vec2(-speed, main.body.GetLinearVelocity().y));
	}
	if(keyisdown(38)){
		if(global.grounded){
			main.body.SetLinearVelocity(new b2Vec2(main.body.GetLinearVelocity().x, jumpSpeed));
		}
	}
}

function checkKeysDown(event:KeyboardEvent):void {
	keyArray[event.keyCode]=true;
}
function checkKeysUp(event:KeyboardEvent):void {
	keyArray[event.keyCode]=false;
}
function keyisdown(X:Number):Boolean {
	return keyArray[X];
}

Some quick information about our platforms: When creating stationary platforms that aren’t affected by the physics, set the objects density to 0… Also the angle of our platforms are in radians so you have to do some simple math to get your degrees to convert. One more thing, All units in Box2D and QuickBox2D are 1 unit is equal to 30 pixels by default (you can change this, but it’ll take some work). So if you want to plug in your pixel values, just divide them by 30, and your set.
While testing you might notice that your character can’t ‘stand’, and just slides all the way down. After many hours (literally) trying to figure out how to fix the problem, I got it. I’ll just change the gravity to zero whenever I’m on the ground and not touching any keys, it’s that simple. This is our final code:

import com.actionsnippet.qbox.*;
import Box2D.Common.Math.*
import Box2D.Dynamics.*;

var sim:QuickBox2D = new QuickBox2D(this);
sim.setDefault({lineAlpha:0, fillColor:0x333333});
sim.createStageWalls();

var m_contactListener = new b2ContactListener();
sim.w.SetContactListener(m_contactListener);

sim.addBox({x:400/30, y:13, width:4, height:4, angle:45/(180/Math.PI), density:0});
sim.addBox({x:4, y:11, width:8, height:0.5, density:0});
sim.addBox({x:0, y:8, width:3, height:3, angle:45/(180/Math.PI), density:0});

var main:QuickObject = sim.addCircle({x:3, y:3, restitution:0, lineAlpha:1, fillColor:0x888888, allowSleep:false, fixedRotation:true});

sim.start();

stage.addEventListener(Event.ENTER_FRAME, loop);
stage.addEventListener(KeyboardEvent.KEY_UP, checkKeysUp);
stage.addEventListener(KeyboardEvent.KEY_DOWN, checkKeysDown);

var keyArray:Array = new Array();
var speed:int = 5;
var jumpSpeed:int = -12;

for (var i=0; i<222; i++) {
	keyArray.push(false);
}

function loop(e:Event):void {
	if(global.grounded && !keyisdown(39) && !keyisdown(37)) {
		main.body.SetLinearVelocity(new b2Vec2(0, main.body.GetLinearVelocity().y));
		sim.gravity.y = 0;
	} else {
		sim.gravity.y = 20;
	}
	if(keyisdown(39)){
		main.body.SetLinearVelocity(new b2Vec2(speed, main.body.GetLinearVelocity().y));
	}
	if(keyisdown(37)){
		main.body.SetLinearVelocity(new b2Vec2(-speed, main.body.GetLinearVelocity().y));
	}
	if(keyisdown(38)){
		if(global.grounded){
			main.body.SetLinearVelocity(new b2Vec2(main.body.GetLinearVelocity().x, jumpSpeed));
		}
	}
}

function checkKeysDown(event:KeyboardEvent):void {
	keyArray[event.keyCode]=true;
}
function checkKeysUp(event:KeyboardEvent):void {
	keyArray[event.keyCode]=false;
}
function keyisdown(X:Number):Boolean {
	return keyArray[X];
}

You can see the fix on line X, where it’s a simple if statement modifying the gravity, and setting our X linear velocity to 0 to prevent more slipping, just in case.

I hope you enjoyed this relatively long tutorial, and that you’ll comment, and visit again.

Thanks for reading!

One thought on “How to make a platform engine in QuickBox2D”

Leave a Reply

Your email address will not be published. Required fields are marked *