Physics part 3

In the last two physics tutorials, we looked at how to create bodies, shapes and how to move them around. Generally, we told Box2D what to do. Conversely, this tutorial focuses on getting information back from Box2D using queries and contact listeners.

Querying the world

Searching through a world is done using the "Query" function. The QueryAABB function accepts a rectangular area in the form of a b2.AABB. Box2D then finds all shapes that "potentially" intersect with the supplied rectangular area. It is important here to emphasize the word "potentially". The query may contain some shapes that do not overlap the supplied AABB. The reason why the QueryAABB function is so inconsistent is because it was designed as a quick filter used during Box2D's broad collision phase.

In the next bit of code, we are going to implement a "QueryPoint" function. This sort of function is useful in games where you want to select shapes or bodies using the mouse.

local slop = b2.linearSlop/2
local aabb = b2.AABB()
function b2.World:QueryPoint ( pt )
  -- query potential shapes
  local q = {}
  local qcb = b2.QueryCallback()
  qcb.ReportFixture = function(qcb, f)
    -- ignore shapes if the point is not inside them
    if f:TestPoint(pt) then
      table.insert(q, f)
    end
    return true
  end
  aabb.lowerBound:Set(pt.x - slop, pt.y - slop)
  aabb.upperBound:Set(pt.x + slop, pt.y + slop)
  self:QueryAABB(qcb, aabb)
  return q
end

Next, we are going to populate the Box2D world with some arbitrary shapes. Notice that we are using some Lua functions (such as "NewBody", "NewBox" and "OnCreate") that were written and explained in the previous physics tutorial.

-- create a new world
gravity = b2.Vec2 ( 0, 0 )
world = b2.World ( gravity )

-- create a bunch of bodies and shapes
for x = -100, 100, 10 do
  for y = -100, 100, 10 do
    local body = world:NewBody ( "staticBody", x, y, 0, 2, 2, false, false, false, false )
    local shape = body:NewBox ( 2, 2, 0.1, 0.5, 0.5, false )
    body:OnCreate ( )
  end
end

Using the "QueryPoint" function, we will allow the user select one body at a time by pressing the mouse. When selecting a body, its sprite changes color to RED. The tricky part here is transforming the mouse cursor position to world coordinates. This is necessary because the mouse cursor is in a different coordinate system than the physics simulation. In other words, the bodies and shapes rendered on screen are usually transformed and often scaled.

-- currently selected body
selected = nil

mouse.on_press = function ( mouse, button )
  -- deselect the previously selected body
  if selected then
    selected.sprite.color = WHITE
    selected = nil
  end

  -- make a new query at the cursor position
  local pt = b2.Vec2 ( mouse.xaxis, mouse.yaxis )
  pt.x, pt.y = camera:get_world_point ( pt.x, pt.y )
  local q = world:QueryPoint ( pt )

  -- color the selected body red
  if #q > 0 then
    selected = q[1]:GetBody ( )
    selected.sprite.color = RED
  end
end
Download:  physics3a.lua




Collision callbacks

In most games, you need to know when two particular objects have collided. Box2D reports these events to Lua using "contact listeners". b2.ContactListener is simply a class containing some callback functions that are evoked by Box2D. Let's look at some Lua code that creates and activates a new contact listener:

local listener = b2.ContactListener ( )

listener.BeginContact = function ( listener, ct )
   -- called when two fixtures begin to touch
end
listener.EndContact = function ( listener, ct )
   -- called when two fixtures cease to touch
end
listener.PreSolve = function ( listener, ct, om )
   -- called before a collision is resolved
end
listener.PostSolve = function ( listener, ct, ci )
   -- called after a collision is resolved
end

world:SetContactListener ( listener )

As you can see, b2.ContactListener has four callback functions. "PostSolve" would probably be the most useful callback in making an action game since it's called after the collision has been resolved. Notice that sensor shapes do not raise the "PreSolve" or "PostSolve" callbacks since they do not respond to collisions. I suggest looking at the Box2D documentation for the full details on the b2.Contact object.

There is a catch to using contact listeners. Box2D does not allow you to make changes to the world during a collision callback. This means that you cannot destroy or create a body or shape until the collision callback function has returned. What you usually have to do is mark the particular body or shape for deletion and destroy it later. Next, we will look at some example code which shows how this is done:

-- set up a contact listener
listener = b2.ContactListener ( )
listener.PostSolve = function ( listener, ct, ci )
  local fix1 = ct:GetFixtureA ( )
  local fix2 = ct:GetFixtureB ( )
  local body1 = fix1:GetBody ( )
  local body2 = fix2:GetBody ( )

  -- mark bodies for deletion
  if (body1.type == 'player' and body2.type == 'enemy') then
    body2.state = 'dead'
  elseif (body1.type == 'enemy' and body2.type == 'player') then
    body1.state = 'dead'
  end
end
world:SetContactListener ( listener )

There are several things to note at this point. First off, collision callbacks may be evoked at the moment a new shape is created (assuming that the newly created shape is initially in contact with another shape). This suggests that contact listeners should be set before any shapes have been generated. Secondly, the collision callbacks always work with shapes. b2.Contact contains the two colliding shapes, but you need to get the associated bodies yourself.

Going back to our example, we will now add some bodies to the world. After creating each body, we assign it its 'type' property (in this case a string value, either 'player' or 'enemy'). In this example, the collision callback looks up the 'type' property so it must be assigned before any of the callbacks could be evoked.

-- create a bunch of bodies and shapes
for x = -100, 100, 10 do
  for y = -100, 100, 10 do
    local body = world:NewBody ( "staticBody", x, y, 0, 2, 2, false, false, false, false )
    body.type = 'enemy'
    local shape = body:NewBox ( 2, 2, 0.1, 0.5, 0.5, false )
    body:OnCreate ( )
  end
end

-- create a user-controlled body
player = world:NewBody ( "dynamicBody", 0, 0, 0, 2, 0, false, false, false, false )
player.type = 'player'
local shape = player:NewCircle ( 1, 0, 0, 0.1, 0.5, 0.5, false )
player:OnCreate ( )

The last part of the code shouldn't come as a surprise, especially if you have read the last Box2D tutorial. A timer object is set up to continuously destroy the marked bodies, move the player and to update the world simulation. By running the script, you can see how any bodies that come in contact with our little 'player' object get destroyed.

timer = Timer ( )
timer:start ( 16, true )
timer.on_tick = function ( timer )
  -- destroy marked bodies
  local body = world:GetBodyList ( )
  while body do
    local next = body:GetNext ( )
    if body.state == 'dead' then
      body:OnDestroy ( )
      world:DestroyBody ( body )
    end
    body = next
  end

  -- check for player input
  local position = b2.Vec2 ( )
  player:GetPosition ( position )
  if keyboard:is_down ( KEY_UP ) == true then
    player:ApplyForce ( b2.Vec2 ( 0, 10 ), position, true )
  end
  if keyboard:is_down ( KEY_DOWN ) == true then
    player:ApplyForce ( b2.Vec2 ( 0, -10 ), position, true )
  end
  if keyboard:is_down ( KEY_LEFT ) == true then
    player:ApplyForce ( b2.Vec2 ( -10, 0 ), position, true )
  end
  if keyboard:is_down ( KEY_RIGHT ) == true then
    player:ApplyForce ( b2.Vec2 ( 10, 0 ), position, true )
  end

  -- update the physics simulation
  local seconds = timer:get_delta_ms ( ) / 1000
  world:Step ( seconds, 10, 8 )

  -- iterate all bodies and update their sprites
  local body = world:GetBodyList ( )
  while body do
    body:OnUpdate ( )
    body = body:GetNext ( )
  end
end
Download:  physics3b.lua