Physics part 2

Simplifying Box2D object creation

Since Box2D was exported almost verbatim from C++, some of its syntax looks unattractive when programming in Lua. For example, creating new shapes and bodies can be quite tedious. An intermediate 'definition' object must be created for each new body, shape or fixture. To simplify this process we are going to write several Lua functions. These functions not only circumvent the Box2D definition objects but also fill in some of the initial values.

The following function adds a new rigid body to the world. The first two parameters (x and y) define the position of our new body. The third parameter (a) is the initial angle or rotation of the body in radians. Linear and angular damping (ld and ad) are optional values that slow down the body's velocity over time. Since Box2D does not simulate the effects of air resistance, a moving body with 0 damping will continue moving until it collides with another body. The greater the damping value, the faster the moving body will grind to a halt. The rest of the parameters are not required and get a bit technical so please refer to the official Box2D documentation if you want to learn more.

-- simplify body and shape creation
local def = b2.BodyDef ( )
function b2.World:NewBody ( type, x, y, a, ld, ad, fr, ib, as, is )
  def.type = type
  def.position:Set( x, y )
  def.angle = a or 0
  def.linearDamping = ld or 0
  def.angularDamping = ad or 0
  def.fixedRotation = fr or false
  def.bullet = ib or false
  def.allowSleep = as or false
  def.awake = is or true
  return self:CreateBody ( def )
end

The next couple of functions are meant to simplify adding new shapes to a body. Rectangles take in a width and height (w and h) and circles require a radius and a local position (lx and ly). I have taken the liberty of adding a couple of 'assert' calls that will raise a Lua error if you specify a bad parameter. Keep in mind that Box2D works with meters so don't supply a radius of 100 if you plan on simulating a tennis ball. The density, friction and restitution parameters (d, f and r) affect how bodies respond when colliding with each other. A shape with 0 density (or zero mass) is considered static and won't move at all. The greater the density, the more massive the shape will become and the more force will be required to accelerate it. Friction affects how slippery the surface of your shape is and must be between 0 and 1. A shape with 0 friction will slide along the surface of other shapes. Restitution measures the elasticity of a shape and should also be between 0 and 1. A restitution of 0 means that your shape will bounce off other shapes without losing (almost) any velocity. The last parameter (is) specifies if your shape is a 'sensor'. Sensor shapes don't respond to collisions at all and will simply pass through other shapes.

local def = b2.PolygonShape ( )
local fix = b2.FixtureDef ( )
function b2.Body:NewBox ( w, h, d, f, r, is )
  assert ( w > 0 and h > 0 )
  def:SetAsBox ( w, h )
  fix.density = d or 0
  fix.friction = f or 0
  fix.restitution = r or 0
  fix.isSensor = is or false
  fix.shape = def
  return self:CreateFixture ( fix )
end

local def = b2.CircleShape ( )
local fix = b2.FixtureDef ( )
function b2.Body:NewCircle ( radius, lx, ly, d, f, r, is )
  assert ( radius > 0 )
  def.radius = radius
  def.position:Set( lx or 0, ly or 0 )
  fix.density = d or 0
  fix.friction = f or 0
  fix.restitution = r or 0
  fix.isSensor = is or false
  fix.shape = def
  return self:CreateFixture ( fix )
end

Properties of b2Body

Get functionUnitsSet function
pGetPosition ( )meters (m)SetTransform ( position, angle )
GetAngle ( )radians
GetMass ( )kilograms (kg)SetMassData ( massData )
vGetLinearVelocity ( )meters/secondSetLinearVelocity ( velocity )
wGetAngularVelocity ( )radians/secondSetAngularVelocity ( velocity )
GetInertia ( )kg-mē

Each rigid body in Box2D updates its position and angle based on its current linear and angular velocity. Let's look at some pseudo-code to better understand how it works:

futurePosition = position + ( linearVelocity + gravity ) * ( 1 - linearDamping )
futureAngle = angle + angularVelocity * ( 1 - angularDamping )

The real code is a bit more elaborate, but I just wanted to illustrate how gravity and damping affect the velocity of a body. The damping parameter can be larger than 1 at which point the damping effect becomes increasingly sensitive to the time step.

Moving a body

FunctionUnitsDescription
ApplyForce ( force, position )Newtons (N)Apply a force at a world point. If the force is not applied at the center of mass, it will generate a torque and affect the angular velocity
ApplyForceToCenter ( force )Newtons (N)Apply a force to the center of mass
ApplyLinearImpulse ( impulse, position )N-seconds or kg-m/sApply an impulse at a point. This immediately modifies the velocity. It also modifies the angular velocity if the point of application is not at the center of mass
ApplyAngularImpulse ( impulse )kg*mē/sApply an angular impulse
ApplyTorque ( torque )N-metersApply a torque. This affects the angular velocity without affecting the linear velocity of the center of mass

Now, we are going to make a controllable body within our Box2D simulation. In particular we will be using the "ApplyForce" function to move a circle shape whenever the player presses a keyboard button. The ApplyForce function takes in a Newtonian force vector and a position where that force is to be applied. In this case, we want to apply the force directly to the center of mass of the player body, otherwise a torque effect will be produced. The player body has a density of 0.1 kg/mē so notice that if this value was higher we would need a greater force in order to move the body. Also take note that I've set a linear damping of 2 for the player body so that it decelerates nicely when the keys are released. For rendering the the simulation, I have used a few function from the last physics tutorial.

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

player = world:NewBody ( "dynamicBody", 0, 0, 0, 2, 0, false, false, false, false )
player:NewCircle ( 1, 0, 0, 0.1, 0.5, 0.5, false )
player:OnCreate ( )

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