Tutorial:PhysicsDrawing (日本語)

これはチュートリアルではなく物理演算プログラムのデバッグで便利な説明付きのコードスニペットです。

物理演算による衝突の描画

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
    -- 執筆予定
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
        -- 執筆予定
    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()

        -- 衝突処理の描画 (執筆予定)
    end
end

さて、描画してみましょう。様々な Shape があります。詳しく説明すると、

  • CircleShape
  • PolygonShape
  • EdgeShape
  • ChainShape

CircleShape の描画

まず、 CircleShape から説明しましょう。このコードで描画します。

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.

PolygonShape の描画

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) --> 4 の表示。

add(f()) --> 3 の表示。 We returned multiple parameters which are simply considered as following parameters. So a -> 1, b -> 2.

add(f(), 0) --> 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!

EdgeShape と ChainShape の描画

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()))

完成版のコード

画面上の正しい位置で衝突処理を全て描画する完成版のコードスニペットを掲載しておきます!

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

そのほかの言語