Physics tutorial

Action games that require sophisticated collision response should definitely consider the Box2D library. Box2D is a rigid body physics library that does not provide any rendering functionality. In this tutorial, we'll look at how to run and render a Box2D simulation.

A quick overview of Box2D

Box2D has several different types of objects. To begin with, there is the b2.World object which represents the space used for the simulation. The world is populated by rigid bodies represented by the b2.Body object. The motion of these bodies is simulated frame by frame through the "world:Step" function. Each body can have one or more shapes (or fixtures) attached to it. These shapes are either circles or polygons and define the collision boundary of each body. It would be a wise move to study the official Box2D documentation for a more detailed description.

Rendering the simulation

Units in Box2D are based on the metric system. 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".

require ( 'Box2D' )

display:create ( "Physics Example", 800, 600, 32, true )

scene = Layer ( )

camera = Camera ( )
camera:set_scale ( 0.1, 0.1 )
scene:add_child ( camera )
display.viewport.camera = camera

There are a few ways to render a Box2D simulation using the AGen framework. The most efficient approach would be to create an individual sprite for each body. For this purpose we will introduce a few custom functions to the body object. The "OnCreate" function adds a new sprite representing the body and draws its existing shapes. After each step of the physics simulation, we need to call the "OnUpdate" function for every body in the world. It syncs the position and angle of each sprite to its associated body. Notice that Box2D uses radians instead of degrees and that bodies rotate in the opposite direction compared to sprites.

function b2.Body:OnCreate ( )
  local sprite = Sprite ( )
  scene:add_child ( sprite )

  -- iterate and draw all the shapes of a body
  local fix = self:GetFixtureList ( )
  while fix ~= nil do
    local shape = fix:GetShape ( )
    local type = shape:GetType ( )
    if type == "circle" then
      local localPosition = shape.position
      local radius = shape.radius
      sprite.canvas:move_to ( localPosition.x, localPosition.y )
      sprite.canvas:circle ( radius )
      sprite.canvas:rel_line_to ( 0, -radius )
    elseif type == "polygon" then
      local vertices = shape.vertices
      local vertexCount = #shape.vertices
      sprite.canvas:move_to ( vertices[1].x, vertices[1].y )
      for i = 1, vertexCount, 1 do
        sprite.canvas:line_to ( vertices[i].x, vertices[i].y )
      end
      sprite.canvas:close_path ( )
    end
    sprite.canvas:set_line_style ( 0.1, WHITE, 1 )
    sprite.canvas:stroke ( )
    fix = fix:GetNext ( )
  end

  -- attach a sprite to the body
  self.sprite = sprite
end

function b2.Body:OnDestroy ( )
  scene:remove_child ( self.sprite )
  self.sprite = nil
end

function b2.Body:OnUpdate ( )
  -- synchronize the sprite to the body
  local position = b2.Vec2 ( )
  self:GetPosition ( position )
  local angle = self:GetAngle ( )
  self.sprite:set_position ( position.x, position.y )
  self.sprite:set_rotation_r ( -angle )
end

Running the simulation

So far, we have added some rendering functionality to Box2D. Now it's time to put it to use and get the physics simulation rolling. It is not difficult to do so. Basically, we create a new world object and call its "Step" function repeatedly using a timer.

gravity = b2.Vec2 ( 0, -10 )
world = b2.World ( gravity )

timer = Timer ( )
timer:start ( 16, true )
timer.on_tick = function ( timer )
  -- 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

The physics simulation will run whenever we start the script. However, our Box2D world is still empty so let's add some bodies to it. First, we will make a ground body using a static rectangle shape. Box2D has a fairly specific way of creating new bodies and shapes that we won't get into for now. What you should know however is that shapes with a density of 0 are considered 'static'. Static shapes always remain immobile even when another body collides with them.

-- create ground object
local def = b2.BodyDef ( )
def.type = "staticBody"
def.position = b2.Vec2 ( 0, 0 )
ground = world:CreateBody ( def )

local def = b2.PolygonShape ( )
def:SetAsBox ( 10, 1 )
local fix = b2.FixtureDef ( )
fix.density = 0
fix.friction = 1
fix.restitution = 0
fix.isSensor = false
fix.shape = def

ground:CreateFixture ( fix )

To make this example interactive, we will let the user create circles by clicking with the mouse. We set the position for our new body based on the current location of the mouse cursor. You will notice that Box2D uses an object called b2.Vec2 to represent positions (and vectors). Also note that new shapes/fixtures require quite a few parameters. A few of them are of particular interest. Density (in kg/mē) affects the mass of the body. Friction (between 0 and 1) is the loss in velocity that occurs when two bodies are touching. Restitution (between 0 and 1) could be described as a measure of elasticity.

mouse.on_press = function ( mouse, button )
  local x, y = camera:get_world_point ( mouse.xaxis, mouse.yaxis )

  -- create a new body
  local def = b2.BodyDef ( )
  def.type = "dynamicBody"
  def.position = b2.Vec2 ( x, y )
  def.angle = 0
  def.linearDamping = 0
  def.angularDamping = 0
  def.fixedRotation = false
  def.bullet = false
  def.allowSleep = false
  def.awake = true
  local body = world:CreateBody ( def )

  -- add a circle shape to the body
  local def = b2.CircleShape ( )
  def.radius = 1
  def.position = b2.Vec2 ( 0, 0 )
  local fix = b2.FixtureDef ( )
  fix.density = 10
  fix.friction = 1
  fix.restitution = 0
  fix.isSensor = false
  fix.shape = def
  
  body:CreateFixture ( fix )

  body:OnCreate ( )
end

It should be pointed out that losing all references to a body from Lua does not remove it from the world. Bodies will remain in the simulation until an explicit call to "world:DestroyBody(body)".

Download:  physics.lua