Box2D physics tutorial
Box2D is the library hiding under the hood of love.physics. In this tutorial, we'll learn how to use Box2D like a pro!
Debug drawingFirst off, we need a reliable way to draw our Box2D simulation on the screen. This is very important for debugging purposes and general feedback. One easy approach is to use the debug registry to inject our custom "draw" functionality into the existing Box2D objects. Save the following piece of code as "b2draw.lua" and drop it in your game folder:
local reg = debug.getregistry() local lg = love.graphics -- World function reg.World:draw() local bodies = self:getBodies() for _, v in ipairs(bodies) do v:draw() end lg.setColor(1,0,0,1) local joints = self:getJoints() for _, joint in ipairs(joints) do local x1, y1, x2, y2 = joint:getAnchors() if joint.getGroundAnchors then local x3, y3, x4, y4 = joint:getGroundAnchors() lg.line(x1, y1, x3, y3, x4, y4, x2, y2) else lg.line(x1, y1, x2, y2) end end end -- Body function reg.Body:draw() local x, y = self:getPosition() local r = self:getAngle() lg.push() lg.translate(x, y) lg.rotate(r) local fixtures = self:getFixtures() for _, fixture in ipairs(fixtures) do local shape = fixture:getShape() shape:draw() end lg.pop() end -- Shape function reg.CircleShape:draw() local x, y = self:getPoint() local r = self:getRadius() lg.circle("line", x, y, r, 32) lg.line(x, y, x + r, y) end function reg.PolygonShape:draw() lg.polygon("line", self:getPoints()) end function reg.ChainShape:draw() lg.line(self:getPoints()) end function reg.EdgeShape:draw() lg.line(self:getPoints()) end
"b2draw" makes drawing our physics simulation super easy! However, Box2D is based on the metric system so some scaling may be required. A circle shape with a radius of 100 is simulated as if its radius is 100 meters. 100 meters is nearly the length of a football field and is probably not the ideal size to simulate a bouncing ball. To keep the simulation realistic, we need to stick to everyday-sized objects. For example, a soccer ball is around 0.1 meters in radius. Rendering such tiny shapes in an 800 by 600 window requires scaling your sprites or using a "camera". Alternatively you could use love.physics.setMeter.
Simplifying the APISince Box2D was exported almost verbatim from C++, some of its syntax looks unattractive when programming in Lua. For example, creating new shapes and fixtures can be quite tedious. To simplify this process we are going to inject some additional Lua functions. Save the following piece of code as "b2access.lua":
local reg = debug.getregistry() local lp = love.physics local newBody = lp.newBody function reg.World:newBody(x, y, t) return newBody(self, x, y, t) end local destroyBody = reg.Body.destroy function reg.World:destroyBody(b) destroyBody(b) end local destroyJoint = reg.Joint.destroy function reg.World:destroyJoint(j) destroyJoint(j) end local newPolygonShape = lp.newPolygonShape local newFixture = lp.newFixture function reg.Body:newPolygon(...) local s = newPolygonShape(...) return newFixture(self, s) end local newRectangleShape = lp.newRectangleShape function reg.Body:newBox(w, h, lx, ly, la) lx, ly = lx or 0, ly or 0 la = la or 0 local s = newRectangleShape(lx, ly, w*2, h*2, la) return newFixture(self, s) end local newCircleShape = lp.newCircleShape function reg.Body:newCircle(radius, lx, ly) lx, ly = lx or 0, ly or 0 local s = newCircleShape(lx, ly, radius) return newFixture(self, s) end local newChainShape = lp.newChainShape function reg.Body:newChain(vertices, loop) local s = newChainShape(loop, vertices) return newFixture(self, s) end local newEdgeShape = lp.newEdgeShape function reg.Body:newEdge(x1, y1, x2, y2) local s = newEdgeShape(x1, y1, x2, y2) return newFixture(self, s) end local newRevoluteJoint = lp.newRevoluteJoint function reg.World:newRevoluteJoint(a, b, x, y, cc) return newRevoluteJoint(a, b, x, y, cc) end local newPrismaticJoint = lp.newPrismaticJoint function reg.World:newPrismaticJoint(a, b, x, y, ax, ay, cc) return newPrismaticJoint(a, b, x, y, ax, ay, cc) end local newDistanceJoint = lp.newDistanceJoint function reg.World:newDistanceJoint(a, b, p1x, p1y, p2x, p2y, cc) return newDistanceJoint(a, b, p1x, p1y, p2x, p2y, cc) end local newRopeJoint = lp.newRopeJoint local sqrt = math.sqrt function reg.World:newRopeJoint(a, b, p1x, p1y, p2x, p2y, l, cc) if l == nil then local lx, ly = p1x - p2x, p1y - p2y l = sqrt(lx*lx + ly*ly) end return newRopeJoint(a, b, p1x, p1y, p2x, p2y, l, cc) end local newPulleyJoint = lp.newPulleyJoint function reg.World:newPulleyJoint(a, b, p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y, ratio, cc) return newPulleyJoint(a, b, p1y, p2x, p2y, p3x, p3y, p4x, p4y, ratio, cc) end local newGearJoint = lp.newGearJoint function reg.World:newGearJoint(a, b, j1, j2, ratio, cc) return newGearJoint(j1, j2, ratio, cc) end local newWeldJoint = lp.newWeldJoint function reg.World:newWeldJoint(a, b, p1x, p1y, cc) return newWeldJoint(a, b, p1x, p1y, cc) end local newFrictionJoint = lp.newFrictionJoint function reg.World:newFrictionJoint(a, b, p1x, p1y, cc) return newFrictionJoint(a, b, p1x, p1y, cc) end local newWheelJoint = lp.newWheelJoint function reg.World:newWheelJoint(a, b, p1x, p1y, ax, ay, cc) return newWheelJoint(a, b, p1x, p1y, ax, ay, cc) end local newMouseJoint = lp.newMouseJoint function reg.World:newMouseJoint(a, x, y, mf) local joint = newMouseJoint(a, x, y) joint:setMaxForce(mf) return joint end
The code for creating new fixtures and shapes becomes much shorter and cleaner thanks to "b2access.lua". This is all achieved with barely any effect on performance.
require("b2draw") require("b2access") function love.load() world = love.physics.newWorld() body = world:newBody(100, 100) fixture = body:newCircle(10) end function love.update(dt) world:update(dt) end function love.draw() world:draw() end
Fixed timestepAccording to the official manual, Box2D should be updated with a constant or "fixed" time step. To keep the simulation as smooth as possible on varying machines, it is recommended to use accumulators. Let's look at an example of how it works. The following code snippet uses 1/60 = 16 milliseconds as the time step interval. You may use a different value for your game, however it should not be changed once the simulation is running.
accumulator = 0 interval = 1/60 maxframeskip = 10 function love.update(dt) accumulator = accumulator + dt accumulator = math.min(accumulator, maxframeskip*interval) while accumulator >= interval do world:update(interval) accumulator = accumulator - interval end end
Note that when using accumulators, not all of "delta" will be used during a single cycle. Some people use this tiny left-over "delta" to interpolate while drawing:
function love.draw() -- sync the sprites of all bodies local bodies = world:getBodies() for i, body in ipairs(bodies) do -- interpolate position local x, y = body:getPosition() local lvx, lvy = body:getLinearVelocity() x = x + lvx*accumulator y = y + lvy*accumulator -- draw body ... end end
Contact listsThe "Contact" object gives us plenty of information about potentially colliding fixtures. Each body in Box2D keeps a list of its potential contacts that you can iterate at any time:
local contacts = body:getContacs() for i, contact in ipairs(contacts) do ... endor you can iterate all contacts:
local contacts = world:getContacts()Additionally a reference to the "Contact" object is provided when using collision callbacks.
Contact lists may contain potential collisions of fixtures that may not be touching at all. Using the "contact:IsTouching()" function tells us if there an actual collision.
function reg.Body:IsTouching(other) -- iterate contacts local contacts = self:getContacts() for i, contact in ipairs(contacts) do -- make sure there's actual contact if contact:IsTouching() then -- look for a specific body local f1, f2 = contact:getFixtures() if f1:getBody() == other or f2:getBody() == other then return true end end end return false endNote that the code above returns true even if there is no "solid" contact. Non-solid contact occurs when one or both of the contacting fixtures is a "sensor". Therefore, the approach shown above is pretty good if you want to add "sensor triggers" in your game.
A contact between two fixtures may have 2, 1 or 0 contact points.
As mentioned above, with "non-solid" contact (involving one or two sensor fixtures) there are 0 contact points.
When a circle collides with another fixture or a polygon vertex hits an edge, we always get 1 contact point.
When there is an edge-to-edge collision between two polygons, we may get 2 contact points.
Three different contacts with the contact points shown in white
Left: circle with 1 contact (1 point)
Center: triangle with 1 contact (1 point, vertex to edge)
Right: rectangle with 1 contact (2 points, edge to edge)
Keeping in mind that all fixtures in Box2D are convex,
it's easy to realize that there cannot be more than 2 contact points
between the same pair of fixtures.
However, one fixture may be in contact with two or more other fixtures.
Therefore a body can have several contacts acting upon it at the same time:
Four contacts with the contact points shown in white
Left: circle with 2 contacts (1 point each)
Right: rectangle with 2 contacts (1 point each)
Another useful vector is the "collision normal" (see yellow lines in the figures above). Each contact has a "collision normal" which is basically the "axis of shortest separation". In layman terms, it's a (normalized) vector describing the direction in which the two fixtures are "pushing" each other.
Normal and tangent impulsesYou have probably heard of the formula F=ma:
force = mass*acceleration acceleration = changeInVelocity/time changeInVelocity = finalVelocity - initialVelocityAn impulse is similar, but with "time" removed from the equation:
impulse = mass*changeInVelocity changeInVelocity = finalVelocity - initialVelocitySo you can think of an "impulse" as an instant change in velocity of an object times its mass.
Each contact point has an impulse associated with it.
Box2D describes the magnitude of these impulses in two parts.
The direction of these impulses is determined by the "collision normal".
As you can see from the following figures, normal impulses (shown in yellow) push the two fixtures apart so that they are not inter-penetrating.
Rectangle going down an inclined slope.
Normal impulse shown in yellow and tangent impulse shown in green.
Left: with friction (1)
Right: without friction (0)
Tangent impulses are applied at 90-degrees relative to the "collision normal".
Tangent impulses are determined by the "friction" of fixtures and can cause the body to spin and roll.
Circle going down an inclined slope.
Normal impulse shown in yellow and tangent impulse shown in green.
Left: with friction (1)
Center: without friction (0)
Remember: if you want your circles to "roll" make sure to give them a "friction" value greater than 0! Otherwise, they will glide down awkwardly while remaining upright.
Torque and centers of massBox2D uses impulses (changes in momentum) to resolve collisions between bodies.
impulse = changeInVelocity*mass changeInVelocity = impulse/massKeep in mind that the exact point where an impulse is being applied is important too. The location of the contact point along with the center of mass of each body may produce torque and cause the body to spin.
Collisions of falling objects. Impulses shown in yellow, centers of mass shown in blue and contact points in white.
Left: circle with the contact point and impulse in line with the center of mass (no resulting torque)
Right: rectangle with the contact point off the side to the center of mass (torque spins the rectangle counter-clockwise)
Collisions in Box2D are solved in sub-steps so we need to use the "BeginContact", "PreSolve" and "PostSolve" callbacks or else we might miss the actual impulses that were applied to each body. Remember that resting contacts are also solved using impulses so for our purposes we need to distinguish between an "impact" and "resting contact".
Iterating the contact lists is great when you are interested in the "resting" contact between fixtures.
Sometimes however collisions may occur "between frames".
If you want to know how much impulse is applied in such collisions,
you probably want to use a ContactListener with the "PostSolve" callback.
One downside is that the "PostSolve" callback may be evoked many times during a single update step. I don't recommend "PostSolve" unless you need to know every impulse that is applied between fixtures.
Common questionsWhen making games with Box2D, one natural question that arises is: "what can we determine about the general state of a body given its velocity and collision information"?
Is the body moving?One approach is to compare the linear velocity of the body against a given threshold. This method returns true for a body resting on top of a moving platform.
function reg.Body:isMoving(treshold) treshold = treshold or 0 local lvx, lvy = self:getLinearVelocity() return (lvx*lvx + lvy*lvy) > treshold*treshold end
Is the body rotating?By looking at the angular velocity of a body we can determine if it's spinning. Note that the angular velocity could be negative. This approach would always return false for bodies with "fixed rotation".
function reg.Body:isRotating(treshold) treshold = treshold or 0 local angular = self:getAngularVelocity() return angular < -treshold or angular > treshold end
Is the body moving along an axis?This is a common question, when you want to find out if an object is "falling". We can use the dot product to check for movement along a given axis (ax, ay). For example, you can use the gravity vector as an axis.
function reg.Body:isMovingOnAxis(ax, ay) local lvx, lvy = self:getLinearVelocity(lv) return ax*lvx + ay*lvy > 0 end
Is the body pushed along an axis?Using the contact information, we can check if a body is "pushed" by other bodies in a given direction. This applies for resting contact too. Suppose that we have a rectangle supported on top of a moving platform. Using the gravity axis, we check if the platform is "pushing up" the rectangle. This is one of the most accurate ways to tell if a given body is "on the ground".
function reg.Body:isPushedOnAxis(ax, ay) local contacts = self:getContacts() for i, contact in ipairs(contacts) do if contact:isTouching() then local x1, y1, x2, y2 = contact:getPositions() if x1 and y1 then local nx, ny = contact:getNormal() local f1, f2 = contact:getFixtures() local other = f1:getBody() if other ~= self then nx, ny = -nx, -ny end if ax*nx + ay*ny > 0 then return true end end end end return false endOne further refinement could be to compute the total impulse acting on the body along the given axis. This can help us determine how firmly the body is being supported.
Is the fixture flat on its side?Consider again the example of a rectangle resting on top of a platform. The number of points in each contact tells us how the body is being supported. If there is one point, the rectangle is supporting itself by a single vertex. If there are two points, the rectangle is flat on its side. There cannot be more than two contact points between a pair of (convex) fixtures.
Rectangle supported by a platform. Contact points shown in white and collision normal in blue
Left: 1 contact with 1 point (vertex to edge)
Right: 1 contact with 2 points (edge to edge)
Note that it's important to consider the direction of each collision normal in order to determine if a contact is pushing the body up, down, sideways or both! One fixture can be in contact with several others, so it's possible for the poor rectangle to be "sandwiched" or pushed in opposite directions at the same time.
What is the "right" coefficient of restitution?The restitution coefficient affects colliding bodies along the normal axis and is described as "the degree of relative kinetic energy retained after a collision". Box2D provides the following function to get the restitution between two colliding fixtures:
restitution = contact:getRestitution()Depending on the restitution, we can categorize collisions in three types:
Perfectly elastic (restitution = 1)
No kinetic energy is lost so there is no sound or damage caused to the colliding objects.
Example: perfectly elastic ball that can bounce forever
Elastic (restitution > 0 and restitution < 1)
Some kinetic energy is converted into heat, sound or causes deformation.
Example: bouncing basketball
Inelastic (restitution = 0)
A lot of kinetic energy is converted into heat, sound or causes deformation.
The colliding objects remain together after the impact.
Example: ball made of soft clay that sticks to floor when dropped
Generally, momentum and energy are always conserved when dealing with a closed system. With Box2D, this is not particularly true for example when using "static" bodies with 0 restitution. Also note that, simulating deformation is beyond the scope of both Box2D and this tutorial.
As a general reference, let's look at the restitution coefficients of different types of balls:
0.858 golf ball
0.804 billiard ball
0.712 tennis ball
0.658 glass marble
0.597 steel ball bearing
What is the relative velocity of any two bodies?One simple approach is to subtract the linear velocities of the two bodies (assuming that their rotation or torque is insignificant).
velocity1 = firstBody:getLinearVelocity() velocity2 = secondBody:getLinearVelocity() velocityDiff = velocity1 - velocity2Next, we find how fast the two bodies are moving towards each other, given the their "difference in velocity" and position.
-- direction vector direction = firstBody:getPosition() - secondBody:getPosition() directionNormal = normalize(direction) -- relative speed (in Meters per second) relativeSpeed = dotProduct(velocityDiff, directionNormal)The resulting "relative speed" is:
The relative velocity in one dimension: two cars located at two positions (p1 and p2) moving at speeds (v1 and v2).
direction = p1 - p2
directionNormal = direction/abs(direction)
velocityDiff = v1 - v2
relativeSpeed = velocityDiff*directionNormal
What is the relative velocity at the moment of collision?Collisions in Box2D are resolved when the "world:Step" function is called. That's why there is the "ContactListener" class - so our game is notified of collision events that happen during "world:Step".
The "ContactListener" class has four callbacks: "BeginContact", "EndContact", "PreSolve" and "PostSolve". Just remember that you really, really want to be using "Pre-" and "PostSolve" to buffer only the most essential collision data. Bodies and fixtures that are irrelevant to your game should be ignored altogether. Collision callbacks may be evoked several times per fixture which could easily amount to hundreds, even thousands of calls per frame.
The following method should work best during the "BeginContact" or "PreSolve" callbacks, before the velocity of the body is modified by Box2D. Given a contact point between two fixtures, Chris Campbell shows us a clever way to determine the "relative velocity of the contact points on each body". Campbell's approach is useful because it implicitly takes into account the bodies' angular velocity.
function preSolve(contact) local x1, y1, x2, y2 = contact:getPositions() if x1 and y1 then local f1, f2 = contact:getFixtures() local b1 = f1:getBody() local b2 = f2:getBody() -- Campbell's method local lvx1, lvy1 = b1:getLinearVelocityFromWorldPoint(x1, y1) local lvx2, lvy2 = b2:getLinearVelocityFromWorldPoint(x1, y1) -- velocity difference vector local dvx, dvy = lvx1 - lvx2, lvy1- lvy2 -- impact speed (in Meters per second) local nx, ny = contact:getNormal() local impactSpeed = dvx*nx + dvy*ny -- dot productWhen used in the "PreSolve" callback, the result is the relative speed of the contact points at the moment of impact! When we know the "impact speed" it's possible to estimate the impulse which will later be applied to the body during the "PostSolve" callback. Again, remember that "impact speed" is actually the relative velocity of the points in contact. When torque is involved, it could be different than the linear velocity of the body.
Contact points shown in white, and their velocities shown in green.
Left: with torque
Right: without torque
- Box2D API by Erin Catto
- Anatomy of a collision by Chris Campbell
- Collision types by Joy Wagon
- Coefficient of restitution by Wikipedia
- The Physics Factbook by Glen Elert