One of the most complicated and math-intensive components of game
development is collision detection and response. If all you need is to
calculate when a couple of rectangles overlap, it’s not a big deal, but for
games that involve lots of irregularly shaped sprites moving and reacting to
each other realistically, you’ll want to use an industrial strength tool to get
the job done.
Enter the physics engine.
The idea behind using a physics engine in your game is that instead of
hacking together a very-loose approximation of how real objects might interact
for your game, dropping in an engine whose sole purpose is to run the physics
simulation allows you to leave it to the engine to handle object dynamics and
interactions and concentrate on the game itself.
To this end a number of different Physics engines, both 2D and 3D have made
their way to JavaScript, many of them ports from other languages, but some
written from scratch. For an overview of a number of different engines, take a
look at Chandler Prall’s JavaScript Physics Engine Comparison.
Of the myriad of open-source 2D engines that are available, one stands out:
Box2D. One reason for this is that it was used as the simulation engine behind
mega-hit Angry birds, whose physics-based gameplay required a bullet-proof
physics implementation. Box2D is the 2D physics engine of choice for many
games, and thus was ported wholesale over to Flash and ActionScript a while
back. To get a JavaScript version, some ambitious folks have taken advantage of
the similarities between ActionScript and JavaScript and created a converter
that converted the ActionScript to JavaScript.
There are a few ports floating about, but the most popular one is Box2DWeb:
Some work has also been done to convert directly from C++ to JavaScript
using emscripten (See box2d.js) but this has a different API
than box2dweb and doesn’t appear to be kept up.
Box2DWeb doesn’t have it’s own documentation per se, but it shares the same
API as the Flash library it was converted from, so the Box2DFlash Documention can be used to
figure out the Box2DWeb API. If you’re interested in really diving into Box2D,
the official Box2D C++ manual also makes for a
worthwhile read, although it contains some newer features not found in
Box2DWeb.
Bootstrapping
To get a Box2D world up and running, you’ll need to pull in the library,
which is packed up nicely in a single JavaScript file Box2dWeb-2.1.a.3.js
The examples below are going to use the following basic HTML wrapper:
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Basic Object Behavior</title>
<script src='js/Box2dWeb-2.1.a.3.js'></script>
<script src='js/example.js'></script>
<style> canavs {
background-color:black; } </style>
</head>
<body>
<canvas id='b2dCanvas'
width='1024'
height='500'></canvas>
</body>
</html>
This pulls in the Box2DWeb library and example.js file that will contain
all the example code.
All of Box2DWeb comes namespaced inside of the Box2D object,
sometimes pretty deeply nested under sub-objects. So that you don’t go insane
with all the namespacing, it’s recommended that you pull out some of the
most-used classes into easy-to-access variables, as shown below:
var b2Vec2 =
Box2D.Common.Math.b2Vec2;
var b2BodyDef =
Box2D.Dynamics.b2BodyDef;
var b2Body =
Box2D.Dynamics.b2Body;
var b2FixtureDef
= Box2D.Dynamics.b2FixtureDef;
var b2Fixture =
Box2D.Dynamics.b2Fixture;
var b2World =
Box2D.Dynamics.b2World;
var b2MassData =
Box2D.Collision.Shapes.b2MassData;
var b2PolygonShape
= Box2D.Collision.Shapes.b2PolygonShape;
var b2CircleShape
= Box2D.Collision.Shapes.b2CircleShape;
var b2DebugDraw = Box2D.Dynamics.b2DebugDraw;
This article is going to build up a small example that shows you how to use
Box2D and how it might integrate into your game engine. This example will wrap
many of the more verbose pieces of Box2D in wrapper classes to simplify the API
you need to expose to your game.
Creating the world
The main container object for simulations in Box2D is the b2World object.
It take two parameters, a gravity vector and whether or not it should skip
simulating inactive bodies. 2D Vectors in Box2DWeb are created using the b2Vec2
object, whose constructor takes in x and y components.
Creating a world with earth gravity that puts inactive objects to sleep would
look like:
gravity = new b2Vec2(0,9.8),
world = new b2World,gravity,true)
You generally want to set the second parameter to true so that Box2D
doesn’t waste CPU cycles simulating the big stack of boxes that’s just siting
there, not moving, until the player crashes into them and wakes them up. This
can, however, lead to situations where you need to be careful to progamatically
wake up objects when you manually move them to a state where they should be
simulated (like when you pick up a box with the mouse cursor and leave it
hanging in mid air).
One other thing you’ll want to take into consideration is the relationship
between Box2D units and your rendering target (canvas in these examples.) The
Box2D length unit is the meter and having objects that are sized in meters
makes it happiest (happiness, in regards to a physics engine, refers to
calculation stability.) But since many games like to think in pixels, you’ll
want some idea of a scaling ratio between Box2D units and pixels. Let’s settle
on a scale of 30 pixels per meter for now, but you can adjust this as you like.
Wrapping world creation and a couple of additions in a simple constructor
function gives the following code:
var Physics =
window.Physics = function(element,scale)
{
var gravity
= new b2Vec2(0,9.8);
this.world
= new b2World(gravity, true);
this.element
= element;
this.context
= element.getContext("2d");
this.scale
= scale || 30;
this.dtRemaining
= 0;
this.stepAmount
= 1/60;
};
Stepping the world
To advance the simulation by a certain amount of time, you’ll need to
manually call the b2World.Stepmethod (notice, all methods in Box2DWeb
begin with capital letters, which is un-javascript-like but matches the
Box2DFlash API). Step takes three parameters: a time step, the number
of velocity iterations. and the number of position iterations.
The Box2D manual suggests using 8 and 3 as velocity
and position iterations respectively, but these can be tuned as needed, trading
accuracy for speed by increasing those numbers.
The time step parameter is fairly self explanatory: it’s the amount of time
in fractions of a second that Box2D should simulate the world for. Box2D works
best when this is a fixed amount of time and not a variable amount of time
dependant on the framerate. This means that you’ll need to do some house
keeping to make sure that the physics simulation runs at a constant frame rate,
even when being driven by requestAnimationFrame.
Building on the Physics class
from the last code listing, you could write a step method as:
Physics.prototype.step = function (dt) {
this.dtRemaining
+= dt;
while
(this.dtRemaining > this.stepAmount) {
this.dtRemaining
-= this.stepAmount;
this.world.Step(this.stepAmount,
8, // velocity
iterations
3); // position
iterations
}
if (this.debugDraw)
{
this.world.DrawDebugData();
}
}
As you can see above, Box2D also supports a debug draw mode which, when
passed a canvas element, will draw a basic vector debug rendering output onto
that canvas. This will be used for the first few examples and a helper method
to activate it is shown below:
Physics.prototype.debug = function() {
this.debugDraw
= new b2DebugDraw();
this.debugDraw.SetSprite(this.context);
this.debugDraw.SetDrawScale(this.scale);
this.debugDraw.SetFillAlpha(0.3);
this.debugDraw.SetLineThickness(1.0);
this.debugDraw.SetFlags(b2DebugDraw.e_shapeBit
| b2DebugDraw.e_jointBit);
this.world.SetDebugDraw(this.debugDraw);
};
Typing this together with initialization and a gameLoop using
requestAnimationFrame gets you the following:
|
var physics,
lastFrame = new Date().getTime();
window.gameLoop = function() {
var
tm = new Date().getTime();
requestAnimationFrame(gameLoop);
var
dt = (tm - lastFrame) / 1000;
if(dt
> 1/15) { dt = 1/15; }
physics.step(dt);
lastFrame =
tm;
};
function init() {
physics =
new Physics(document.getElementById("b2dCanvas"));
physics.debug();
requestAnimationFrame(gameLoop);
}
Two items to note in the above listing. The first is that since
requestAnimationFrame stops firing when your game’s tab is unfocused, if you
run your simulation code from a requestAnimationFrame loop you’ll want to put
in an upper time limit on the size of dt to prevent your game from
fast-forwarding when the user refocuses the tab.
The other option is to put your simulation in a setTimeout or setInterval
driven loop which doesn’t exhibit this behavior, but using
requestAnimationFrame as a sort of automatic pause for your game can be useful
in single-player games. If you’re building a multi-player game, it’s probably not
a good plan and using setTimeout or setInterval might be better.
You’ll also want to add in the requestAnimationFrame Shim
Adding in Bodies
An empty world without anything going on in it is pretty boring. In order
to give it some life, we’ll need to add some Box2D bodies to it. Bodies are
what Box2D calls the objects that it simulates.
Bodies can either be static, kinematic, or dynamic. Static and kinematic
bodies are used for walls and platforms that don’t need to react to collisions,
while dynamic bodies are used for everything that’s needs to be simulated
normally. Both static and kinematic bodies are treated as if they have infinite
mass and can be moved manually by the developer. Kinematic bodies can also have
a velocity. Static and kinematic bodies don’t collide with other static and
kinematic bodies, only with dynamic bodies.
To create a Box2D body that interacts with other bodies, you need two
things, a body definition and one or more fixture definitions. The body
definition defines the initial attributes of the entire body, such as it’s
position, velocity, angular velocity, damping (the inverse of bounciness) and
boolean attributes such as whether to start out active, if the body is allowed
to sleep. You can also decide whether the body should be treated
as bullet, which garners it more processor intensive but accurate
collision detection to prevent small, fast moving objects from passing through
other objects entirely. You can reuse body definitions if you like.
One additional feature of bodies is that you can set a piece of
arbitrary userData on them. This userDatais often used to tie
the physics object back to a graphical representation.
Fixtures are the glue that binds shapes to a body. A body can have one or
more fixtures. All fixtures are rigid inside of a single body, which means that
the individual fixtures don’t move relative to each other but rather combine to
create a single rigid body. Fixtures also contain additional information such
as density, friction, restitution, and collision flags that control with other
fixtures to collide with. You can also flag fixtures as sensors to prevent a
collision response but still receive notification of a collision. Fixtures are
created by calling b2Body.CreateFixture and passing in a fixture
definition.
To create a fixture definition you can attach to a body, you need to give
it a shape to work with. Box2D supports two different types of shapes. The
first type, b2CircleShape represent, not surprisingly, shapes that
are perfect circles. The second type b2PolygonShape are used for
shapes that can be represented with a series of vertices. The vertices
of b2PolygonShape’s must define a convex polygon in clockwise order (if your
vertices are counter-clockwise, your shape won’t collide with anything.)
Since boxes are such a common shape, Box2D provides a helper method to
easily create the vertices for ab2PolygonShape box of a given width and
height called SetAsBox, which takes a two parameters that correspond to
1/2 the width and 1/2 the height of the vertices to create.
Convex polygons are polygons that have no indents (visually, it means you
should only be able to draw a line from one vertex to it’s neighbors without
passing through the shape.) This means if you want to create a more complicated
shape, such as the capital letter L, you cannot do so with a single shape in a
single fixture. You must add multiple fixtures to your body instead. Once thing
to remember is that your collision shape does not need to match your game
sprite exactly, but can instead be a simplified representation of that shape.
So even if your video game character running around with a big bazooka doesn’t
look like a convex shape, modeling them as one is probably ok.
Keeping your collision shapes simple with result in faster collision
detection and will likely make for better gameplay. Concave polygons tend to
get stuck on things, so even if you could realistically model Bazooka guy, he
might be better off as concave anyway so his Bazooka doesn’t get caught on
ledges and leave him dangling off cliffs unrealistically as a multi-fixture
rigid-body.
With all that in mind, let’s create a simple class that makes it easy to
create bodies. This class will take in the physics object from the previous
section and go through the steps necessary to add a new body to the world:
var Body =
window.Body = function
(physics, details) {
this.details
= details = details || {};
// Create the definition
this.definition
= new b2BodyDef();
// Set up the definition
for (var k in this.definitionDefaults) {
this.definition[k] = details[k] || this.definitionDefaults[k];
}
this.definition.position
= new b2Vec2(details.x || 0,
details.y || 0);
this.definition.linearVelocity
= new b2Vec2(details.vx || 0,
details.vy || 0);
this.definition.userData
= this;
this.definition.type =
details.type == "static" ?
b2Body.b2_staticBody : b2Body.b2_dynamicBody;
// Create the Body
this.body
= physics.world.CreateBody(this.definition);
// Create the fixture
this.fixtureDef
= new b2FixtureDef();
for (var
l in this.fixtureDefaults) {
this.fixtureDef[l]
= details[l] || this.fixtureDefaults[l];
}
details.shape = details.shape || this.defaults.shape;
switch
(details.shape) {
case
"circle":
details.radius = details.radius || this.defaults.radius;
this.fixtureDef.shape
= new b2CircleShape(details.radius);
break;
case
"polygon":
this.fixtureDef.shape
= new b2PolygonShape();
this.fixtureDef.shape.SetAsArray(details.points,
details.points.length);
break;
case
"block":
default:
details.width = details.width || this.defaults.width;
details.height = details.height || this.defaults.height;
this.fixtureDef.shape
= new b2PolygonShape();
this.fixtureDef.shape.SetAsBox(details.width
/ 2,
details.height / 2);
break;
}
this.body.CreateFixture(this.fixtureDef);
};
Body.prototype.defaults = {
shape:
"block",
width:
5,
height:
5,
radius:
2.5
};
Body.prototype.fixtureDefaults = {
density:
2,
friction:
1,
restitution:
0.2
};
Body.prototype.definitionDefaults = {
active:
true,
allowSleep:
true,
angle:
0,
angularVelocity:
0,
awake:
true,
bullet:
false,
fixedRotation: false
};
The Body defines a simple constructor function that, using a set
of defaults defined above, let’s you create single-fixture Box2D objects easily
without jumping through all the normal definition and fixture hoops.
Modifying the init() method from above to add in some objects as
shown below to get some objects on the screen:
function init() {
physics =
window.physics = new Physics(document.getElementById("b2dCanvas"));
// Create some walls
new
Body(physics, { type: "static",
x: 0, y: 0, height: 50, width: 0.5
});
new
Body(physics, { type: "static",
x:51, y: 0, height: 50, width: 0.5});
new
Body(physics, { type: "static",
x: 0, y: 0, height: 0.5,
width: 120 });
new
Body(physics, { type: "static",
x: 0, y:25, height: 0.5,
width: 120 });
window.bdy = new Body(physics, { x:
5, y: 8
});
new
Body(physics, { x: 13, y: 8 });
new Body(physics, { x: 8, y: 3
});
requestAnimationFrame(gameLoop);
}
If you view the example you’ll see the three boxes fall down and then
change color to gray when they are put to sleep by the simulation.
Taking over rendering
While Box2dWeb’s debug rendering is a boon to getting started quickly, at
some point you’re going to want to handle your own rendering so that you can
build a game other than “Attack of the Transparent Boxes.”
There are two ways to go about this. The first is to keep a list of bodies
separate from Box2D and loop over those after each step and render them. The
second is to use the world.GetBodyList() to return the first body and
then call body.GetNext() to return the next element. This works
because Box2D keeps a linked list of bodies to iterate over with. You can pull
the element you’ve associated with the body out by callingGetUserData().
Either method will work and the second makes bookkeeping easier, but if you
have game elements that aren’t simulated with Box2D (which you might) you must
use the former.
In these examples, were going to use the latter method as it’s less code.
Bodies will also need how to render themselves. The easiest way to handle
this is simply to add in a draw()method to our simple Body wrapper class
that looks at its type and then renders itself by drawing the appropriate
shape. To make this more applicable to normal games as well, bodies will also
support drawing an image.
Drawing shapes with canvas isn’t particularly difficult, but with Box2D,
the main thing to watch out for here is correctly positioning and rotating the
drawn such that the way it’s drawn on the screen correctly reflects Box2D’s
internal representation.
By correctly applying the matrix operations using the built-in canvas
matrix transformations in global to specific order, we can make drawing the
shapes straightforward.
The three transforms we need to worry about are the global scale (if you
remember, we’re drawing objects at 30 pixels to a meter), the object
translation and then the object’s angle. The global scale transform can be
handled outside of the main loop, while the latter two transformations both
need to be applied per object.
Using the Box2D GetBodyList() method,
the Physics.step() method is modified to draw the objects itself
unless debug rendering is on:
if (this.debugDraw)
{
this.world.DrawDebugData();
} else {
this.context.clearRect(0, 0, this.element.width,
this.element.height);
var
obj = this.world.GetBodyList();
this.context.save();
this.context.scale(this.scale,
this.scale);
while
(obj) {
var
body = obj.GetUserData();
if
(body) {
body.draw(this.context);
}
obj =
obj.GetNext();
}
this.context.restore();
}
In a normal game if you already have a rendering loop, you’d iterate over
the sprites and then reach in and grab the Box2D representation data each step.
Next up is the Body class drawing method. This method will save the current
transformation matrix, translate and then rotate the matrix so that the object
can be drawn centered at the origin and then will draw the object based on the
shape. In this example, the shape will be drawn if the object has
a color property set.
Lastly, to demonstrate how to draw images, it will draw an image if
the image property is set (based on the width and height properties -
these need to be set even if the object type itself isn’t block)
The full method is shown below:
Body.prototype.draw = function (context)
{
var
pos = this.body.GetPosition(),
angle =
this.body.GetAngle();
// Save the context
context.save();
// Translate and rotate
context.translate(pos.x, pos.y);
context.rotate(angle);
// Draw the shape outline if the shape has a color
if (this.details.color)
{
context.fillStyle = this.details.color;
switch
(this.details.shape) {
case
"circle":
context.beginPath();
context.arc(0,
0, this.details.radius, 0, Math.PI *
2);
context.fill();
break;
case
"polygon":
var
points = this.details.points;
context.beginPath();
context.moveTo(points[0].x, points[0].y);
for
(var i = 1; i <
points.length; i++) {
context.lineTo(points[i].x, points[i].y);
}
context.fill();
break;
case
"block":
context.fillRect(-this.details.width
/ 2, -this.details.height /
2,
this.details.width,
this.details.height);
default:
break;
}
}
// If an image property is set, draw the image.
if (this.details.image)
{
context.drawImage(this.details.image, -this.details.width /
2, -this.details.height / 2,
this.details.width,
this.details.height);
}
context.restore();
We get the current position and rotation by
calling body.GetPosition() and body.GetAngle().
Circle shapes are simply drawn as a 360 degree arc path and then filled.
Polygon shapes draw a path using the same points as the shape definition.
Blocks just use the fillRect method to draw a rectangle.
See example 2 for some more shapes all falling together, rendered by the
above rendering code.
Picking and Moving
Depending on the genre of game you are building, you may want to add in the
ability to directly interact with your game object via the mouse or touch. In
order to achieve this you must first be able to determine the object at a
specific pixel position on your canvas.
Box2D natively supports a way to query bodies’ bounding boxes given a world
position. Given the returned body you could then loop over each of the bodies
fixtures and match the position against the exact shapes. Luckily Box2DWeb has
wrapped this functionality up in a single method call: QueryPoint().
Given a vector location in world coordinates,
the QueryPoint() method will execute a callback for each fixture that
matches.
As a first pass, let’s add in the ability to click on an object and have it
impart an impulse onto the object. With Box2D there are three ways you can get
an dynamic object moving: set its velocity, apply a force or apply an impulse.
One thing you should not do is just try to set a dynamic object’s position, as
this effectively breaks the simulation and can cause unexpected effects with
objects behaving badly. If you really need you move an object to a position,
you can call body.SetTransform, but this method is undocumented in the
Box2DFlash documentation, which should give you an idea of whether it’s a good
practice or not.
Among the three good options, applying a force or an impulse are
essentially the same thing - the only difference is that the force takes into
consideration the length of the time step to determine the impact on the
object, while the impulse is independent of the length of the time step. Forces
are good to use when you want consistent speed over a number of frames, while
impulses are great as you can just set and forget them. You might use the
former to keep a player running at a consistent speed while the latter works
well for things like explosions.
You set a force on an object with ApplyForce(force,point) and an
impulse withApplyImpulse(impulse,point).
Both ApplyForce and ApplyImpulse take a vector as the first
argument that controls direction and strength. They also both take a second
argument that defines the point where the force is applied. If you don’t want
the object to torque (i.e. start to rotate), you can apply the force to an
object’s center using body.GetWorldCenter().
Forces work well with situations where you want objects to behave
realistically and do things like smoothly speed up from a standstill. A lot of
times in games, however, you don’t necessarily want your player to have to slow
down to a stop before changing direction. You can defy the laws of physics by
directly setting the velocity on an object as opposed to applying forces and
impulses. If you set the velocity each frame, you can get de-facto consistent speed
that is useful in things like space shooters and platformers. The only thing to
watch out for is that dynamic object-to-object interactions will be slightly
out-of-whack when two objects of different masses that both have their velocity
set interact.
You can set linear and angular (rotational) velocity
using SetLinearVelocity(direction) andSetAngularVelocity(number).
While you might be tempted to try to keep reseting the angular velocity to try
to prevent a object from rotating, this won’t work well and you’re better off
setting thefixedRotation property on the body definition or
calling SetFixedRotation(true).
Starting with picking, let’s add a method to the top-level Physics object
that will call a callback whenever an object is mousedown’d on or touched:
Physics.prototype.click = function(callback)
{
var
self = this;
function
handleClick(e) {
e.preventDefault();
var
point = {
x:
(e.offsetX || e.layerX) / self.scale,
y:
(e.offsetY || e.layerY) / self.scale
};
self.world.QueryPoint(function(fixture) {
callback(fixture.GetBody(),
fixture,
point);
});
}
this.element.addEventListener("mousedown",handleClick);
this.element.addEventListener("touchstart",handleClick);
};
This uses the native addEventListener syntax for portability sake, but if
you’re using jQuery you can substitute $.fn.on to make the function
more compact.
To test this method out to apply a vertical impulse, we’ll add the
following to the init method:
// ...
physics.click(function(body) {
body.ApplyImpulse({ x: 1000, y:
-1000 }, body.GetWorldCenter());
});
You can see this in action on Example 3 Clicking on an object will
launch it up and to the right. If you want to visually see the differences
between ApplyImpulse, ApplyForce and SetLinearVelocity, try
swapping the call to ApplyImpulse out with the others to see the
effect.
You’ll notice the larger the object, the less effect the impulse has. This
is because the Box2D assumes objects have a uniform density (controlled by the
density property in the fixture definition) and so larger objects weigh more
and need more force to move.
In addition to the QueryPoint, Box2DWeb also provides a number of
other method to query the world for objects, including by bounding box
(QueryAARB), by arbitrary shape (QueryShape), and by querying for collisions
along a line (RayCast). RayCast is particularly useful for doing
things like visibility detection (can enemy A see the player?) and determining
what bullets and shrapnel will hit.
Listening for collisions
Depending on the game you are creating, simply being able to create and
control discrete objects may be enough to get going, but if you want to add
true physics-based gameplay (such as determining the strength of collisions and
which objects are touching which, you’ll want to use Box2D’s contact listener
functionality.
Box2D provides four different callbacks you can listen for when collision
occur. The first BeginContactoccurs when contact is initiated between two
objects. The second EndContact is triggered when two objects stop
touching. The third callback PreSolve, is called on every collision, but
before the collision is sent to the solver to resolve. All three of these only
provide details on what two objects are in contact and normal details with
what’s known as the contact manifold, but don’t give us a strength-of-impact
impulse.
To get that detail, you’ll need to use the fourth callback, PostSolve,
which is called whenever there is an impulse on a body caused by another body.
As bodies are crashing into each other all the time, PostSolvewill get
called very frequently, so it’s important not to do too much processing each
time the callback is triggered. Two bodies coming into contact with each other
could each only get a single BeginContact andEndContact callback,
but will mostly likely get a number
of PreSolve and PostSolve callbacks.
Two important words of warning: first, you should not, under any
circumstance, add, change, or remove bodies during the collision callbacks as
this will most likely disrupt Box2D’s collision resolution process. If you need
to make changes to any aspects of the physics simulation as a result of one of
these four callbacks, you should queue up what needs to be done and then make
those changes after the step call. Secondly, the data structure for contact
information is reused, so if you do queue up steps to change afterwards, you
should make sure to copy the individual pieces of that structure out to
separate objects.
Because it provides the most interesting data, we’re going to use
the PostSolve callback to highlight objects based on the strength of
their most recent impact.
Because the callback is global, we’ll need to reach into the object on
contact and grab our wrapper object usingGetUserData() if you want to have
per-object collision reactions.
For example, to add in a collision listener that calls
a contact method on each body (if that method exists) you could
extend the Physics wrapper as is shown below:
Physics.prototype.collision = function
() {
this.listener
= new Box2D.Dynamics.b2ContactListener();
this.listener.PostSolve
= function (contact, impulse) {
var
bodyA = context.GetFixtureA().GetBody().GetUserData(),
bodyB = context.GetFixtureB().GetBody().GetUserData();
if
(bodyA.contact) {
bodyA.contact(contact, impulse, true)
}
if
(bodyB.contact) {
bodyB.contact(contact, impulse, false)
}
};
this.world.SetContactListener(this.listener);
};
The above method can be extended to add listener support
for BeginContact and EndContact by adding those callbacks
to
the listener object. BeginContact and EndContact both
only take thecontact object as a parameter.
To extend a body with a contact callback, you could do something like this:
var body =
new Body(physics, {
color:
"blue",
x: 8,
y: 3
});
body.contact = function (contact,
impulse, first) {
var
magnitude = Math.sqrt(
impulse.normalImpulses[0] * impulse.normalImpulses[0] + impulse.normalImpulses[1] * impulse.normalImpulses[1]),
color =
Math.round(magnitude / 2);
if
(magnitude > 10) {
this.details.color
= "rgb(" + color +
",50,50)";
}
};
Here the blue box will change it’s color based on the strength of the last
impact. You can use this callback to determine when objects have received
enough damage to be destroyed or explode.
Take a look at Example 4 to see this code in action.
The contact callback has been added to the box that is originally blue, and the
add-an-impulse with a click behavior from Example 3 is still in there so you
can see how collisions trigger.
Adding Joints
The last main piece of Box2D that we haven’t talked about yet is joints.
Joints allow you to constrain bodies in one or more dimensions to another body
or another point
Box2D supports a number of different joints, with descriptions paraphrased
from the Box2D manual:
1. Distance Joints - the distance between
two points on two bodies will stay constant
2. Revolute Joints - a joint that forces
two bodies to share a common anchor point, acting like a hinge. You can
constrain this joint to limit the angles between the two objects as well as
turn on a motor to drive rotation.
3. Prismatic Joint - allows for movement
between two objects along a single axis. You can limit constrain the joint to
limit the minimum and maximum distance as well as turn on a motor to drive
translation.
4. Pulley Joint - used to create an
idealized pulley, which connects two bodies. As one body goes up, the other
goes down.
5. Gear joint - a joint that combines two
of any combination of revolute and prismatic joints and acts like an idealized
gear. You can control the ratio of rotation or translation.
6. Mouse Joint - used to connect a single
body to a changeable destination point. Primarily used for “soft” drag and drop
of objects.
Mouse Joints
Joints are easiest to see work when you can manipulate the bodies directly,
so to start to play around with them the first thing to do is set up a Mouse
Joint to allow you to drag objects around the screen (using mouse joints in a
testbed is the most common use for this type of joint)
Mouse joints are created by first creating a joint definition object of the
correct type (in this caseBox2D.Dynamics.Joints.b2MouseJointDef), then setting
the affected bodies, bodyA and bodyB, and then setting any
additional tuning parameters to control the strength and effect of the joint.
Finallyworld.CreateJoint is called to create an actual joint from the
definition.
For simplicity’s sake, the code below uses only
the mousedown, mousemove and mouseup events. To add in
touch support, you could add
in touchstart, touchmove and touchend events. Adding
support for multiple touches would allow you to drag multiple objects around
the page at once (see Mobile
Game Primer for how to do multi-touch) To add in object drag-and-drop, the following code does the trick:
Physics.prototype.dragNDrop = function
() {
var
self = this;
var
obj = null;
var
joint = null;
function
calculateWorldPosition(e) {
return
point = {
x:
(e.offsetX || e.layerX) / self.scale,
y:
(e.offsetY || e.layerY) / self.scale
};
}
this.element.addEventListener("mousedown", function (e) {
e.preventDefault();
var
point = calculateWorldPosition(e);
self.world.QueryPoint(function (fixture) {
obj =
fixture.GetBody().GetUserData();
},
point);
});
this.element.addEventListener("mousemove", function (e) {
if
(!obj) {
return;
}
var
point = calculateWorldPosition(e);
if
(!joint) {
var
jointDefinition = new Box2D.Dynamics.Joints.b2MouseJointDef();
jointDefinition.bodyA
= self.world.GetGroundBody();
jointDefinition.bodyB = obj.body;
jointDefinition.target.Set(point.x, point.y);
jointDefinition.maxForce = 100000;
jointDefinition.timeStep = self.stepAmount;
joint =
self.world.CreateJoint(jointDefinition);
}
joint.SetTarget(new b2Vec2(point.x, point.y));
});
this.element.addEventListener("mouseup", function (e) {
obj =
null;
if
(joint) {
self.world.DestroyJoint(joint);
joint = null;
}
});
Most of this is relatively straightforward, but one piece that might need
some explaining is theself.world.GetGroundBody() spot. Box2D by default
creates an empty body with no fixtures in it that it calls the Ground Body. In
this case, the MouseJoint attaches the first body to the Ground body, but that
body isn’t actually used for anything.
For the other joints, all of which do act on two bodies, the ground body is
useful when you need a static body to connect to (remember static bodies won’t
move) This pinions one end of the joint to prevent it from moving at all. You
don’t need to use the ground body, you could use any other static body, but the
ground body is nice because it’s always there and has no fixtures so you don’t
need to worry about collisions.
You can see from the code above that each time the mouse is depressed, we
look for a body at the point. If we find one, the object is saved for future
use. Then, the first time the mouse is moved, it creates a new mouse joint. On
that and every subsequent mousemove event, we update the target of
the joint to be the new location of the mouse, which will move the object towards
the mouse if possible (the object will not move through static or dynamic
objects, and will exert a force only upto the maxForce parameter.)
Finally, when the mouse is released, the joint is destroyed.
Take a look at Example 5 and try dragging objects
around with your mouse.
Distance Joints
Distance joints, as mentioned above, enforce a set distance between to
points on two bodies. The joint doesn’t, however, limit rotation in any way.
You can hook up any types of bodies, but the joint will only move dynamic
bodies.
With the exception of the MouseJoint above and
the GearJoint, described at the end of this section, most joints are
created by calling an Initialize method on the Joint definition and
passing in some initial parameters.
To create a distance joint, you need to pass in the two affected bodies
to Initialize and then two 2D vectors defining the anchor locations
(in world coordinates) for both bodies.
For example:
body1 = new Body(physics, {
color:
"red",
x: 15,
y: 12
}).body;
body2 = new Body(physics, {
color:
img,
x: 35,
y: 12
}).body;
def = new
Box2D.Dynamics.Joints.b2DistanceJointDef();
def.Initialize(body1,
body2,
body1.GetWorldCenter(),
body2.GetWorldCenter());
var joint = world.CreateJoint(def);
This will anchor two boxes at their center (see the distance joint example
in Example 6).
Additionally you can set properties on the joint definition
for dampingRatio and frequencyHz, which control, respectively,
how efficiently distance is realigned and the response frequency. These allow you
to turn distance joints into soft springs by lowering the damping ratio and
upping the response frequency. (See the second distance example to watch this
in action).
Once a joint is created, if you want to draw the joint on screen (or just
need additional information about the joint) you can query the joint itself
using GetAnchor1() and GetAnchor2() to return 2D vectors is
world coordinates.
Revolute Joints
Revolute joints connect two bodies together at a single point, but allow
rotation around that point. If you connect two dynamic bodies together, it will
act as if there is a hinge between those bodies at that point. If you connect
to a static body, the dynamic body will rotate around that point.
The Initialize method takes the two bodies and a third parameter
defining the hinge location in world coordinates.
For example:
body1 = new Body(physics, { color:"red", x: 20, y: 12 }).body;
body2 = new Body(physics, { image:
img, x: 24, y: 12 }).body;
def = new
Box2D.Dynamics.Joints.b2RevoluteJointDef();
def.Initialize(body1,
body2,
new
b2Vec2(22,14));
var joint = world.CreateJoint(def);
This creates two boxes hinged at one corner (See the Revolute joint example
in Example 6
Revolute joints also support setting a limit on rotation and optionally
turning on a motor to drive torque around the joint. See the Flash Documentation for the
additional options on the b2RevoluteJointDefclass.
Prismatic Joints
Prismatic Joints act like a frictionless piston between two bodies.
Creating one is similar to creating a distance joint: you specify two bodies
and then two anchor points, as shown below:
body1 = new Body(physics, { color:"red", x: 15, y: 12 }).body;
body2 = new Body(physics, { image:
img, x: 25, y: 12 }).body;
def = new Box2D.Dynamics.Joints.b2PrismaticJointDef();
def.Initialize(body1,body2,
new
b2Vec2(20,14),
new
b2Vec2(1,0));
def.enableLimit = true;
def.lowerTranslation = 4;
def.upperTranslation = 15;
var joint = world.CreateJoint(def);
var joint = world.CreateJoint(def);
The example above also turns on a upper and lower bounds limit
using enableLimit and setting
thelowerTranslation and upperTranslation.
Like the RevoluteJoint you can turn on a motor to drive motion
along the joint’s axis. See the Box2DWeb Documentation for more
details on the options that enable motor movement.
Pulley Joints
The pulley joint behaves exactly as you might expect: it connects two
bodies together in a method that mimics a pulley. In general you’ll want to
connect either two dynamic bodies or a dynamic and kinematic body, otherwise
the pulley won’t do much.
Initializing a pulley joint is slightly more complicated as you need to
provide four points in addition to the two bodies: two static anchor points
that represent the top of the pulley for each body and then the anchor point on
each body. As a final parameter to Initialize you need to provide a
movement ratio. To have both boxes move the same distance you and use a ratio
of 1. To have body1 move twice as far as body2, use a ratio of 2.
Here’s an example that creates a pulley between two boxes:
body1 = new Body(physics, { color:"red", x: 15, y: 12 }).body;
body2 = new Body(physics, { image:
img, x: 25, y: 12 }).body;
def = new
Box2D.Dynamics.Joints.b2PulleyJointDef();
def.Initialize(body1, body2,
new
b2Vec2(13,0),
new b2Vec2(25,0),
body1.GetWorldCenter(),
body2.GetWorldCenter(),
1);
var joint = world.CreateJoint(def);
Pulley joints run into problems when the distance between the pulley anchor
and object is reduced to 0, so you should put some other static object in the
way to prevent this from happening. In the pulley example inExample 6 The static anchors are
blocked by the static body at the top of the page.
Gear Joints
The final joint supported by Box2DWeb is the Gear joint. Gear joints take
any combination of two Revolute or Prismatic joints and create a joint linking
their motion.
The Revolute or Prismatic joints you use must both have a static body as
their first body (this is a good place to use the ground body). You can also
set a ratio property to control the movement ratio between bodies.
The Gear joint definition doesn’t have an Initialize method to
get things going, so you need to set up the bodies and joints yourself on the
properties of the joint definition.
To set up a simple gear between two rotating boxes, you would first create
the initial two Revolute joints, and then add them to a new Gear joint, as
shown below:
body1 = new Body(physics, { color:"red", x: 15, y: 12 }).body;
body2 = new Body(physics, { image:
img, x: 25, y: 12 }).body;
var def1 =
new Box2D.Dynamics.Joints.b2RevoluteJointDef();
def1.Initialize(physics.world.GetGroundBody(),
body1,
body1.GetWorldCenter());
var joint1 =
physics.world.CreateJoint(def1);
var def2 =
new Box2D.Dynamics.Joints.b2RevoluteJointDef();
def2.Initialize(physics.world.GetGroundBody(),
body2,
body2.GetWorldCenter());
var joint2 =
physics.world.CreateJoint(def2);
def = new
Box2D.Dynamics.Joints.b2GearJointDef();
def.bodyA = body1;
def.bodyB = body2;
def.joint1 = joint1;
def.joint2 = joint2;
def.ratio = 2;
var joint = world.CreateJoint(def);
If you need to destroy the joints, make sure you destroy the Gear joint
first before either of the sub joints or the simulation could go haywire.
Joint Playground
The example used to this point has been modified to demonstrate some simple
joints between two boxes. You can press any key on the keyboard to switch
between joint types and use the mouse to manipulate bodies. See Example 6.
Wrapping Up
This article went over the details of getting started with Box2DWeb, a
JavaScript port of the popular Box2D 2D physics library. It covered setup, rendering,
picking, collisions, impulses, and joints. Hopefully this is enough of an
introduction to Box2DWeb to get you started building your own physics-based
simulations and games. If you want to dig further, there’s no substituion for
the Box2DFlash Reference Docs and
the Original
C++ Box2D Reference Materials. Now go and simulate some bodies!
Tidak ada komentar:
Posting Komentar