Tutorial:PhysicsDrawing
This is not so much a tutorial but an explained code snippet that helps with physics debugging.
Contents
Drawing Physics Collisions
When developing with Box2D (aka love.physics) you will likely get to a point where the collisions don't quite match up with what you expect them to be due to some minor mistake.
At the end of this guide there's a complete snippet that will draw all collisions at the correct location. If you just want the code feel free to jump down and just copy & paste it. But we will go over what the code does bit by bit.
love.physics works via a few different sets of objects. Most notably. "World", "Body", "Shape" and "Fixture".
- "World" is the physics world. You can have multiple ones but only objects within the same world will collide.
- "Body" is the location of your object within the physics world. It defines which world this object belongs to, if it can move and the mass of the object (which will be calculated from the size of your shape by default). It also holds a reference to the fixture.
- "Shape" is the collision itself. If two shapes collide, the physics system will handle it. It also holds a reference to the fixture.
- "Fixture" is how we attach a shape to a body. It defines a few additional properties how collisions will be handled (such as friction, collision channels and a few more). It also holds a reference to the body and the shape.
In order to draw all shapes we need to start at the world. It handles all collisions. So it's gotta contain a list of things. Specifically. It contains a list of bodies which we can get in the following way:
listOfBodies = World:getBodyList()
We don't need a list of bodies though. We just wanna do something for every body. So we make a for loop out of it:
for _, body in pairs(World:getBodyList()) do
-- ToDo
end
A body can have multiple shapes which are attached with multiple fixtures. So we have to iterate over the list of fixtures as well:
for _, body in pairs(World:getBodyList()) do
for _, fixture in pairs(body:getFixtureList()) do
-- ToDo
end
end
A fixture however can only have one shape. So now we already have a reference to all collision fixtures (and therefore shapes) there are within this loop!
We can simply store a reference to the shape by doing
for _, body in pairs(World:getBodyList()) do
for _, fixture in pairs(body:getFixtureList()) do
local shape = fixture:getShape()
-- ToDo Draw collision
end
end
So next up is the drawing. There are multiple types of shapes. To be specific we have
- CircleShape
- PolygonShape
- EdgeShape
- ChainShape
Drawing circle shapes
So let's first look at the circle. We will draw it with the following code:
local cx, cy = body:getWorldPoints(shape:getPoint())
love.graphics.circle("fill", cx, cy, shape:getRadius())
cx & cy stand for "CircleX" and "CircleY". The position we want to draw it at.
body:getWorldPoints() will take any amount of points and convert them into world space (a point being two numbers. One for x and one for y). In other words. It will do "x + body:getX()", "y + body:getY()" for all points provided.
Then we simply draw the circle at the position we just calculated with the radius of the shape. I choose "fill" as first parameter. You can also use "line". "fill" means it'll be drawn as circle. "fill" will draw it as a ring.
Drawing a polygon shapes
Next up is the PolygonShape. A polygon is a list of points with up to 8 vertices (aka 16 numbers representing x and y. e.g. (x1, y1, x2, y1, ..., xn, yn) )
We can abuse a little bit of functionality of lua to make this a super small snippet of code. We can get all the points by calling "shape:getPoints()" (once we verified it's a polygon shape).
This will return up to 16 individual numbers. However. If we put this directly into another function without following parameter. It will simply put the next number as next parameter.
add = function(a, b)
print( a + b )
end
f = function()
return 1, 2
end
add(2, 2) --> Will print 4
add(f()) --> Will print 3. We returned multiple parameters which are simply considered as following parameters. So a -> 1, b -> 2.
add(f(), 0) --> Will print 1. We returned multiple parameters. However those are directly followed by another value. So only the first return value is used and the others will be discarded. a -> 1, b -> 0
Knowing this we can do the following:
love.graphics.polygon("fill", body:getWorldPoints(shape:getPoints()))
Since there is no following parameter it will simply put as many points in the function as the shape has. We don't have to handle anything else!
Drawing edge and chain Shapes
Lastly we have edge and chain shapes. They are essentially the same. Internally they are handled a bit differently. But for our case we can assume an EdgeShape to be the same as a ChainShape with two points. A ChainShape can have two or more points (this is the only shape without a limit).
However. Both are made up of lines that may or may not loop around. And both have the function "shape:getPoints()" as well! So we do not have to treat them separately.
We pretty much do the same as we did for the polygon. But instead of drawing a polygon we draw a line.
love.graphics.line(body:getWorldPoints(shape:getPoints()))
Final code
This leaves us with the final code snippet that will draw all of our collisions at the correct location on the screen!
Happy debugging!
for _, body in pairs(world:getBodyList()) do
for _, fixture in pairs(body:getFixtureList()) do
local shape = fixture:getShape()
if shape:typeOf("CircleShape") then
local cx, cy = body:getWorldPoints(shape:getPoint())
love.graphics.circle("fill", cx, cy, shape:getRadius())
elseif shape:typeOf("PolygonShape") then
love.graphics.polygon("fill", body:getWorldPoints(shape:getPoints()))
else
love.graphics.line(body:getWorldPoints(shape:getPoints()))
end
end
end