Wiki

Clone wiki

Core / PhysicsLab

Back to Codea's Example Projects


ProjectPhysics-Small.png Physics Lab by John Millard

Introduction


Physics Lab is one of the example projects supplied with Codea. It shows a user how to use the Physics API to create dynamic simulations. A user can run through each of the tests to see examples of features, and browse the associated code. By taking a copy of the example project, a user can add their own tests to the examples provided.

The project runs in display mode STANDARD and supports any landscape orientation. Two parameters are used: test_number sets the test from 1 to 9 (see below); and use_accelerometer is a off-on switch (0 or 1) that determines whether or not the iPad's accelerometer is used to determine the 'gravity' affecting bodies.

Under the hood, Codea's Physics API makes use of Box2D, an independent open source C++ engine for simulating rigid bodies in 2D developed by Erin Catto. Box2D has its own manual, which may help in understanding Codea's in-app reference.

Tabs in the project


In addition to Main the project comprises the following tabs:

TabComment
PhysicsDebugDrawEstablishes the class PhysicsDebugDraw.
Test1Establishes the class Test1. This is a basic introduction to bodies.
Test2Establishes the class Test2. This is a basic introduction to joints.
Test3Establishes the class Test3. This is an introduction to gravity scale.
Test4Establishes the class Test4. This is an introduction to sensors.
Test5Establishes the class Test5. This is an introduction to filtering.
Test6Establishes the class Test6. This is an introduction to motors.
Test7Establishes the class Test7. This is an introduction to (1) raycastings and (2) aabb queries.
Test8Establishes the class Test8. This is an introduction to the collision engine.
Test9Establishes the class Test9. This is an introduction to edge chains and edges.

Main tab


The Main tab implements four callback functions (which are explained further later in this section):

setup()          -- called once
draw()           -- called up to 60 times a second
touched(touch)   -- called whenever a touch on screen begins, ends or moves   
collide(contact) -- called any time two bodies collide with each other

The Main tab also implements the following functions:

setTest(t)  -- Cleans up the current test and then sets it to test t
nextTest()  -- Advances to the next test, cycling back to the first if necessary (not used)
cleanup()   -- Cleans up, generally

The Main tab also implements a number of functions that return bodies:

createCircle(x, y, r)  -- Creates a circle, centred on (x, y)
createBox(x, y, w, h)  -- Creates a box, centred on (x, y)
createGround()         -- Creates the ground (see below) 
createRandPoly(x, y)   -- Creates a random polygon with up to 10 sides

These create functions all follow a similar pattern, for example:

function createCircle(x, y, r)
    local circle = physics.body(CIRCLE, ...) -- Create a body, with relevant arguments
    ...                                      -- Set attributes accordingly
    debugDraw:addBody(circle)                -- Add the body to debugDraw
    return circle                            -- Return the body created 
end

setup()

The setup() function is called once by Codea and does a number of important things to set up the application:

function setup()
    ...
    debugDraw = PhysicsDebugDraw() -- Create an instance of the PhysicsDebugDraw class.
                                   -- This class keeps track of bodies, joints etc so that
                                   -- they can be rendered in the viewer.                             
    ...
    tests = {                      -- Create an array that contains an instance of each of
        Test1(),                   -- the classes for tests 1 to 9. Each test class provides
        Test2(),                   -- a 'setup' and a 'draw' function. Some tests also provide
        Test3(),                   -- a 'touched' or 'collide' function.
        Test4(),
        Test5(),
        Test6(),
        Test7(),
        Test8(),
        Test9()
    }
    ...
    currentTestIndex = 1          -- This holds the index to the current test (1 to 9)
    currentTest = nil             -- This holds a reference to the current test
    setTest(currentTestIndex)     -- This sets up the test with the given index
  
    defaultGravity = physics.gravity()
    ...
end

Note that physics.gravity() is only preserved in the global variable defaultGravity after the currentTestIndex (tests[1]) has been setup. This means that if tests[1]:setup() affects gravity, it will affect the default for all tests. This can be corrected in a copy of the example project by modifying the order in which gravity is preserved and the test is setup:

function setup()
    ...
    debugDraw = PhysicsDebugDraw()
    defaultGravity = physics.gravity() -- Preserve gravity first ...
    ...
    tests = {                          -- ...then initialise the tests ...
        Test1(),
        ...
        Test9()
    }
    ...
    currentTestIndex = 1
    currentTest = nil
    setTest(currentTestIndex)          -- ... or setup the current test
    ...
end

The setup() function also establishes the two sliders that are used by the application:

iparameter("test_number", 1, #tests)  -- An integer from 1 to the number of tests
iparameter("use_accelerometer", 0, 1) -- Either 0 or 1

The variables set by these sliders are inspected in the draw() function.

draw()

The draw() function is called by Codea up to 60 times a second, and does the following things each time:

function draw()
    if test_number ~= currentTestIndex then -- Check to see if the slider has changed the
        setTest(test_number)                -- value of the 'test_number' variable. If it has 
    end                                     -- (re)set the test accordingly. 
    
    currentTest:draw()                      -- Draw the current test
    debugDraw:draw()                        -- Draw the bodies, joints etc
    ...
    if use_accelerometer == 1 then          -- Check the 'use_accelerometer' slider and
        physics.gravity(Gravity)            -- either use the built-in value referenced by the
    else                                    -- 'Gravity' global variable or use the default
        physics.gravity(defaultGravity)     -- gravity established during 'setup()'.
    end
end

The following modification to a copy of the example project allows a test to modify the default defaultGravity by setting self.defaultGravity:

function draw()
    ...
    if use_accelerometer == 1 then
        physics.gravity(Gravity)
    else
        physics.gravity(currentTest.defaultGravity or defaultGravity)
    end
end

The draw() function also renders in the viewer information about the current test:

local str = string.format("Test %d - %s", currentTestIndex, currentTest.title)
font("MyriadPro-Bold")
fontSize(22)
fill(255, 255, 255, 255)        -- Opaque white, equivalent to fill(255)
                                -- textMode(CENTER) by default
text(str, WIDTH/2, HEIGHT - 20) -- Centred at middle of viewer, 20 below the top

touched(touch)

This function passes touch to debugDraw:touched() to handle. If it is not handled there, touch is passed to currentTest:touched() to handle:

function touched(touch)
    if debugDraw:touched(touch) == false then
        currentTest:touched(touch)
    end
end

collide(contact)

This function passes contact to debugDraw:collide() to handle. It then passes it to currentTest:collide() to handle, if that function exists:

function collide(contact)
    debugDraw:collide(contact)
    if currentTest.collide then
        currentTest:collide(contact) -- Only called if collide is not nil
    end
end

createCircle(x, y, r)

This creates a dynamic body that is a circle of radius r.

In the co-ordinates of the local space of the body (those relative to its centre of mass and not taking into account the rotation of the body), the centre of the circle is at 0, 0.

In the co-ordinates of the viewer, the centre of mass is set to x, y.

function createCircle(x, y, r)
    local circle = physics.body(CIRCLE, r)
                                    -- circle.type is DYNAMIC, by default
    circle.interpolate = true       -- Used to smooth out motion, but introduces a slight lag
    circle.x = x
    circle.y = y
    circle.restitution = 0.25       -- The body has some 'bounce'
    circle.sleepingAllowed = false  -- The body is not allowed to 'sleep', if it comes to rest
                                    -- circle.density is 1, by default
                                    -- circle.friction is 0.2, by default
                                    -- circle.gravityScale is 1, by default
    debugDraw:addBody(circle)
    return circle
end

createBox(x, y, w, h)

This creates a dynamic body that is a rectangular box (that is, a polygon) of width w and height h}.

In the co-ordinates of the local space of the body (those relative to its centre of mass and not taking into account the rotation of the body), the centre of the box is at 0, 0.

In the co-ordinates of the viewer, the centre of mass is set to x, y.

function createBox(x, y, w, h)
    local box = physics.body(POLYGON,
        vec2(-w/2, h/2),         -- Four points, specified in a counter-clockwise order.
        vec2(-w/2, -h/2),        -- Here: top left, bottom left, bottom right and top right.
        vec2(w/2, -h/2),
        vec2(w/2, h/2))
                                 -- box.type is DYNAMIC, by default
    box.interpolate = true       -- Used to smooth out motion, but introduces a slight lag
    box.x = x
    box.y = y
    box.restitutions = 0.25      -- The body has some 'bounce'
    box.sleepingAllowed = false  -- The body is not allowed to 'sleep', if it comes to rest
                                 -- box.density is 1, by default
                                 -- box.friction is 0.2, by default
                                 -- box.gravityScale is 1, by default
    debugDraw:addBody(box)
    return box
end

createRandPoly(x, y)

This creates a dynamic body that is a polygon with a random number of sides (between 3 and 10, inclusive).

In the co-ordinates of the local space of the body (those relative to its centre of mass and not taking into account the rotation of the body), the centre of the polygon is at 0, 0.

In the co-ordinates of the viewer, the centre of mass is set to x, y.

The function calculates the location of each of the vertices of the polygon and holds them in a table (an array) (points):

function createRandPoly(x,y)
    local count = math.random(3, 10)  -- Random number of sides/vertices, between 3 and 10.
    local r = math.random(25, 75)     -- Random 'radius', between 25 and 75.
    local a = 0                       -- Set the angle 'a' to 0.
    local d = 2 * math.pi / count     -- Set the angular sweep to a full circle (2 pi, in
                                      -- radians), divided by the number of sides (in count).
    local points = {}                 -- Create an empty table to hold the points.
    
    for i = 1, count do               -- Iterate through the vertices of the polygon.
        local v = vec2(r,0):rotate(a) -- Rotate a horizontal vector of length 'r',
                                      -- counter-clockwise through angle 'a'.
            + vec2(                   -- Add a random vector, with the x and y coordinates
                math.random(-10, 10), -- each a random integer between -10 and 10.
                math.random(-10,10))
        a = a + d                     -- Add the sweep 'd' to angle 'a'.
        table.insert(points, v)       -- Add the point held in 'v' to the table 'points'.
    end
    ...
end

The function then unpacks the array points to create the polygon body:

function createRandPoly(x, y)
    ...
    local poly = physics.body(POLYGON, unpack(points))
                                 -- poly.type is DYNAMIC, by default
    poly.x = x
    poly.y = y
    poly.sleepingAllowed = false -- The body is not allowed to 'sleep', if it comes to rest
    poly.restitution = 0.25      -- The body has some 'bounce'
                                 -- poly.density is 1, by default
                                 -- poly.friction is 0.2, by default
                                 -- poly.gravityScale is 1, by default
    debugDraw:addBody(poly)
    return poly
end

createGround()

This creates a static body that is a rectangular slab (that is, a polygon) with a width that of the viewer (WIDTH) and a height of 20}.

In the co-ordinates of the local space of the body (those relative to its centre of mass and not taking into account the rotation of the body), the centre of the slab is not at 0, 0 but at WIDTH/2, 10. However, because the slab is static it does not matter that it is 'not physical' (that is, that its centre of mass is located in the bottom-left corner).

In the co-ordinates of the viewer, the centre of mass is set to 0, 0, by default.

function createGround()
    local ground = physics.body(POLYGON,
        vec2(0, 20),         -- Four points, specified in a counter-clockwise order.
        vec2(0, 0),          -- Here: top left, bottom left, bottom right and top right.
        vec2(WIDTH, 0),
        vec2(WIDTH,20))
    ground.type = STATIC     -- Otherwise would have been DYNAMIC, by default
                             -- ground.friction is 0.2, by default
                             -- ground.restitution is 0, by default
                             -- Density has no meaning for an 'infinitely massive' static body
    debugDraw:addBody(ground)
    return ground
end

In this case, ground.restitution is 0, by default. However, it is possible for static bodies to have some 'bounce' (as in the case of dynamic bodies).

PhysicsDebugDraw tab


The PhysicsDebugDraw tab implements PhysicsDebugDraw as a class, defining the following functions:

PhysicsDebugDraw:init()
PhysicsDebugDraw:addBody()
PhysicsDebugDraw:addJoint()
PhysicsDebugDraw:clear()
PhysicsDebugDraw:draw()
PhysicsDebugDraw:touched(touch)
PhysicsDebugDraw:collide(contact)

The class keeps track of added bodies and joints; handles, and keeps track of, touches that begin in dynamic bodies; handles, and keeps track of, contacts from collisions; draws bodies, joints etc; and updates the effect of touches that began in dynamic bodies. It also allows information to be cleared in order to start afresh.

PhysicsDebugDraw:init()

The init() function creates four fields referring to empty tables:

function PhysicsDebugDraw:init()
    self.bodies   = {} -- An array of the bodies
    self.joints   = {} -- An array of the joints
    self.touchMap = {} -- A table of entries for certain touches, indexed by touch.id
    self.contacts = {} -- A table of contacts from collisions, indexed by contact.id
end

The entries in the touchMap are tables, with fields tp (the location of the touch in the viewer), body (the dynamic body where the touch began) and anchor (the location where the touch began on the body).

PhysicsDebugDraw:addBody(body)

The addBody() function adds the body in question to the end of the bodies table:

function PhysicsDebugDraw:addBody(body)
    table.insert(self.bodies, body)
end

PhysicsDebugDraw:addJoint(joint)

The addJoint() function adds the joint in question to the end of the joints table:

function PhysicsDebugDraw:addJoint(joint)
    table.insert(self.joints,joint)
end

PhysicsDebugDraw:clear()

The clear() function destroys the bodies and joints and then causes the four fields to refer to empty tables:

function PhysicsDebugDraw:clear()
    for i,body in ipairs(self.bodies) do  -- Destroy all the bodies
        body:destroy()
    end
    for i,joint in ipairs(self.joints) do -- Destroy all the joints
        joint:destroy()
    end      
    self.bodies = {}
    self.joints = {}
    self.contacts = {}
    self.touchMap = {}
end

PhysicsDebugDraw:draw()

This is the function that renders all of the bodies, joints etc that the instance of the class has been keeping track of:

function PhysicsDebugDraw:draw()
    ...
    for k, v in pairs(self.touchMap) do
        ... -- Process the touchMap
    end
    ...
    for k, joint in pairs(self.joints) do
        ... -- Process the joints
    end
    ...
    for i, body in ipairs(self.bodies) do
        ... -- Process the bodies
    end 
    ...
    for k, v in pairs(self.contacts) do -- Process the contacts
        for m, n in ipairs(v.points) do -- Work through the points for each contact
            ellipse(n.x, n.y, 10, 10)   -- Draw a small circle of diameter 10
        end
    end
    ...
end

Entries in the touchMap are rendered by purple lines from the current location of the touch on the viewer to the location on the body (in terms of viewer coordinates):

strokeWidth(5)      -- Width of 5
stroke(128, 0, 128) -- Colour purple (a = 255, by default)
...
-- v is an entry in the touchMap
local worldAnchor = v.body:getWorldPoint(v.anchor)
local touchPoint = v.tp
...
line(touchPoint.x, touchPoint.y, worldAnchor.x, worldAnchor.y)

Entries in the joints are rendered by bright green lines, from one anchor to another:

stroke(0, 255, 0, 255) -- Colour green (the final 255 is not required)
strokeWidth(5)         -- Width of 5
...
local a = joint.anchorA
local b = joint.anchorB
line(a.x, a.y, b.x, b.y)

Entries in the bodies are rendered in different colours, depending on the type of the body:

if body.type == STATIC then
    stroke(255,255,255,255)         -- White for STATIC
elseif body.type == DYNAMIC then
    stroke(150,255,150,255)         -- Pale green for DYNAMIC
elseif body.type == KINEMATIC then
    stroke(150,150,255,255)         -- Light blue for KINEMATIC
end

The co-ordinates returned by myBody.points are in the local space of the body. That is, they are relative to the body's centre of mass and do not take into account its rotation relative to the viewer. Consequently, each body is drawn by first translating the viewer co-ordinates to the location of the body's centre of mass and rotating the viewer co-ordinates to the body's rotation:

pushMatrix()              -- Preserve settings
translate(body.x, body.y) -- Translate origin of viewer co-ordinates over
                          -- the body's centre of mass
rotate(body.angle)        -- Rotate the viewer co-ordinates to align with the body's rotation
...                       -- Render body, based on co-ordinates in the local space of the body
popMatrix()               -- Restore settings  

How each body is rendered depends on its shapeType. For example, the ellipse() function is used for the circle:

elseif body.shapeType == CIRCLE then
    strokeWidth(5.0)           -- Width of 5
    line(0,0,body.radius-3,0)  -- Horizontal line from centre to 3 short of the radius
    strokeWidth(2.5)           -- Width of 2.5
    ellipse(0,0,body.radius*2) -- Circle of diameter twice the radius
end

Visually, a stroke width of 2.5 for ellipse() has the same 'weight' as a stroke width of 5 for line.

In the case of an edge or a chain, line() is used for each segment:

elseif body.shapeType == CHAIN or body.shapeType == EDGE then
    strokeWidth(5.0)                 -- Width of 5
    local points = body.points       -- Get the points, in local co-ordinates
        for j = 1,#points-1 do       -- Iterate from first to penultimate point
            a = points[j]            -- 'a' holds this point
            b = points[j+1]          -- 'b' holds the next point
            line(a.x, a.y, b.x, b.y) -- Draw the line segment
        end

In the case of a (closed) polygon, the code draws a line from the last point to the first:

if body.shapeType == POLYGON then
    strokeWidth(5.0)
    local points = body.points
        for j = 1,#points do              -- Iterate from first to last point
            a = points[j]                 -- 'a' holds this point
            b = points[(j % #points) + 1] -- 'b' holds the next point:
                                          -- if j < #points, then j % #points is j
                                          -- if j = #points, then j % #points is 0
            line(a.x, a.y, b.x, b.y)      -- Draw the line segment
        end

In the case of entries in the touchMap, the PhysicsDebugDraw:draw() function does more than simply render the information in the viewer. It also updates the effect of the touch on the dynamic body:

local gain = 2.0
local damp = 0.5
...
-- v is an entry in the touchMap
local worldAnchor = v.body:getWorldPoint(v.anchor) -- Translate into viewer co-ordinates
local touchPoint = v.tp                            -- Recorded in terms of viewer co-ordinates
local diff = touchPoint - worldAnchor              -- From the anchor to the current location

local vel = v.body:getLinearVelocityFromWorldPoint(worldAnchor)   -- See below
v.body:applyForce( (1/1) * diff * gain - vel * damp, worldAnchor) -- See below

The function myBody:GetLinearVelocityFromWorldPoint(worldPoint) returns the velocity of the point on (or outside of) the body (where the point is specified in terms of world co-ordinates). If the body is not spinning, then the velocity will be the velocity of its centre of mass. If the body is spinning (and the point is not the point the body is spinning around) then the velocity will be determined by the velocity of its centre of mass and the effect of the spinning.

The function myBody:applyForce(force, worldPoint) applies the force to the body at the point worldPoint (specified in terms of world co-ordinates). The force that is applied is made up of, and can be analysed into, two components (the multiplication of the first component by (1/1), or 1, has no effect):

-- v is an entry in the touchMap
v.body:applyForce(diff * gain, worldAnchor) -- One in the direction of 'diff'
v.body:applyForce(-vel * damp, worldAnchor) -- One in the opposite direction of 'vel'

The first component is proportional to the vector from the anchor to the current location of the touch (diff), where the proportionality is determined by local variable gain. This component of the force will cause the body to accelerate towards the current location of the touch.

The second component is proportion to the velocity of the anchor (vel), but in the opposite direction, where the proportionality is determined by the local variable damp. This component of the force will act to dampen the effect of the first component. When the body is at rest, the second component is zero.

If the current location (or locations) of the touch (or touches) affecting the body remain fixed then the body will eventually come to rest and the net forces on it (from touches and from gravity) will be zero.

PhysicsDebugDraw:touched(touch)

This is the function that processes touches that began inside a dynamic body. If the touch is processed then the function returns true, otherwise it returns false.

function PhysicsDebugDraw:touched(touch)
    local touchPoint = vec2(touch.x, touch.y) -- Store the location as a vector
    if touch.state == BEGAN then
        for i, body in ipairs(self.bodies) do -- Check each body
            if body.type == DYNAMIC and body:testPoint(touchPoint) then
                ... -- Process the hit
                return true -- Exit when the first hit has been processed
            end
        end
    elseif touch.state == MOVING and self.touchMap[touch.id] then
        ...          -- Process an existing touch in the touchMap
        return true  -- Exit
    elseif touch.state == ENDED and self.touchMap[touch.id] then
        ...          -- Process an existing touch in the touchMap
        return true; -- Exit
    end
    return false     -- Exit without having processed the touch
end

For touches that began inside a dynamic body, an entry is added to the touchMap, indexed by the touch.id that identifies the touch:

if body.type == DYNAMIC and body:testPoint(touchPoint) then
    self.touchMap[touch.id] = {
        tp = touchPoint,                        -- The location of the touch on the screen
        body = body,                            -- The body touched 
        anchor = body:getLocalPoint(touchPoint) -- The location of the touch on the body
    } 
    return true
end

As tracked touches move, the location of the touch on the screen is updated in the touchMap:

elseif touch.state == MOVING and self.touchMap[touch.id] then
    self.touchMap[touch.id].tp = touchPoint
    return true

As tracked touches end, the entry in the touchMap is cleared:

elseif touch.state == ENDED and self.touchMap[touch.id] then
    self.touchMap[touch.id] = nil
    return true;
end

PhysicsDebugDraw:collide(contact)

This is the function that processes contacts, its actions depending on whether the contact.state records that the contact has begun, is moving or has ended. Contracts are recorded in contacts, indexed by the contact.id that identifies the contact:

function PhysicsDebugDraw:collide(contact)
    if contact.state == BEGAN then
        self.contacts[contact.id] = contact -- Store the contact in contacts
        sound(SOUND_HIT, 2643)              -- Make a sound
 
    elseif contact.state == MOVING then
        self.contacts[contact.id] = contact -- Update the contact in contacts

    elseif contact.state == ENDED then
        self.contacts[contact.id] = nil     -- Remove the entry in contacts

    end
end

Test1 to Test9 tabs


Each of the Test1 to Test9 tabs implements a class, for example the Test1 tab implements Test1 as a class.

Each of the class implementations follow a similar function, defining the following functions:

Tab:init():setup():draw():touched(touch):collide(contact):cleanup()
RequiredRequiredRequiredRequiredOptionalOptional
Test1YesYesYes - does nothingYesNoNo
Test2YesYesYes - does nothingYes - does nothingNoNo
Test3YesYesYes - does nothingYes - does nothingNoNo
Test4YesYesYes - does nothingYes - does nothingYesNo
Test5YesYesYes - does nothingYes - does nothingNoNo
Test6YesYesYes - does nothingYesNoYes
Test7YesYesYesYes - Does nothingNoYes - does nothing
Test8YesYesYesYesNoYes
Test9YesYesYes - does nothingYes - does nothingNoYes

myTest:init(), myTest:setup(), myTest:draw() and myTest:touched(touch) are required - they are called when the instance of the class is created, in setTest(), in draw() and in touched(touch respectively.

However, although myTest:draw() and myTest:touched(touch) must exist, they need not do anything. debugDraw:draw() can handle all the drawing and all touches not handled by debugDraw:touched(touch) can be ignored.

myTest:collide(contact) is optional: collide(contact) tests to see if the function exists before calling it if it does:

function collide(contact)
    ...
    if currentTest.collide then      -- Is collide not nil (a function exists)?
        currentTest:collide(contact) -- If yes, then call it
    end
end

myTest:cleanup() is also optional:

function setTest(t)
    ...
    if currentTest.cleanup then  -- Is cleanup not nil (a function exists)?
        currentTest:cleanup()    -- If yes, then call it
    end
    ...
end

With one exception, the myTest:init() function does nothing more than set a title for the test. For example (Test1):

self.title = "basic bodies (tap to create)"

The exception is Test7, which uses myTest:init() to set five other fields.

Test1 tab


The Test1 tab implements:

Test1:init()         -- Sets title "basic bodies (tap to create)"
Test1:setup()
Test1:draw()         -- Does nothing
Test1:touched(touch)

Test1:setup()

The setup() function creates the ground, a box, a circle (to its right) and a random polygon (further to its right):

createGround()
createBox(WIDTH/2, 100, 30, 30)     -- 30 x 30
createCircle(WIDTH/2 + 50, 110, 30) -- radius 30
createRandPoly(WIDTH/2 + 150, 120)

Each of these bodies will be added to debugDraw.bodies in the function that creates it.

Test1:touched(touch)

This function does what the title suggests: when the touch has begun, a random polygon is created at the location of the touch:

function Test1:touched(touch)
    if touch.state == BEGAN then
        createRandPoly(touch.x, touch.y, 25, 25) -- Last two arguments do nothing
    end
end

Perhaps confusingly, the function calls createRandPoly(x, y) with two additional arguments that have no function in the example project (25, 25).

Test2 tab


The Test2 tab implements:

Test2:init()         -- Sets title "basic joints"
Test2:setup()
Test2:draw()         -- Does nothing
Test2:touched(touch) -- Does nothing

Test2:setup()

The setup() function creates the ground, two boxes (box and box2), and two circles (circle, which is static, and circle2). More interestingly, it creates three joints:

Joint typeJoining
REVOLUTEthe static circle and the dynamic box
DISTANCEbox and dynamic circle2
PRISMATICthe static circle and the dynamic box2

Test2 does not illustrate the fourth type of joint that exists in the Physics API, WELD.

Each of these joints is added to debugDraw.joints after it has been created.

A REVOLUTE joint rivets two bodies together but allows them to rotate around the point of the (invisible) rivet. In this case, the position of the rivet is the centre of the static circle:

local joint = physics.joint(REVOLUTE, circle, box, circle.position)
debugDraw:addJoint(joint)

A DISTANCE joint joins two bodies together by an (invisible) rod but allows them to rotate around the points at which the rod is attached. In this case, the rod joins the centre of the box and the centre of the dynamic circle:

local distJoint = physics.joint(DISTANCE, box, circle2, box.position, circle2.position)
debugDraw:addJoint(distJoint)

A PRISMATIC joint joins two bodies together by an (invisible) rod but allows them to slide along the rod (but not rotate around the points at which the rod is attached). That is, it allows for relative translation of two bodies along a specified axis and prevents relative rotation. In this case, the axis runs horizontally through the centre of the second box and to the right (vec2(1, 0)):

local sliderJoint = physics.joint(PRISMATIC, circle, box2, box2.position, vec2(1,0))
sliderJoint.enableLimit = true -- Enable limits for the joint
                               -- sliderJoint.lowerLimit is 0, by default
sliderJoint.upperLimit = 50    -- Set the upper limit to 50
debugDraw:addJoint(sliderJoint)

In this case, the initial distance between the bodies can not reduce (sliderJoint.lowerLimit is 0) but it can increase in the direction of the axis by 50. This allows the dynamic box to move to the right, away from the static circle.

Test3 tab


The Test3 tab implements:

Test3:init()         -- Sets title "gravity scale"
Test3:setup()
Test3:draw()         -- Does nothing
Test3:touched(touch) -- Does nothing

Test3:setup()

The setup() function creates the ground, and three circles, each with a different gravityScale:

                            -- circle1.gravityScale is 1, by default
circle2.gravityScale = 0.5
circle3.gravityScale = 0.25

The gravityScale of a body controls the influence of gravity on it. Running the test, the acceleration of the second circle in the direction of the ground is lower than that of the first circle, and the acceleration of the third circle is lower still.

Test4 tab


The Test4 tab implements:

Test4:init()           -- Sets title "sensors"
Test4:setup()
Test4:draw()           -- Does nothing
Test4:touched(touch)   -- Does nothing
Test4:collide(contact)

Test4:setup()

The setup() function creates the ground, a dynamic box (above the middle of the viewer) and a static circle (in the middle of the viewer). The circle is set as a sensor:

self.sensor = createCircle(WIDTH/2, HEIGHT/2, 25) -- Returns the body (a circle)
self.sensor.sensor = true                         -- Set the body as a sensor  
self.sensor.type = STATIC                         -- Set the body as static

The box falls down, and through the circle. Collision information is generated for sensors in the Codea Physics API, but they do not physically affect the scene.

Test4:collide(contact)

The collide(contact) function tests to see if one of the bodies involved in the contact (bodyA or bodyB is a sensor. If it is, then a message is printed according to the state of the contact (BEGAN, MOVING or other (ENDED):

function Test4:collide(contact)
    if contact.bodyA.sensor or contact.bodyB.sensor then
        if contact.state == BEGAN then       -- By default, BEGAN is 0
            print("Sensor BEGAN")
        elseif contact.state == MOVING then  -- By default, MOVING is 1
            print("Sensor PERSIST")
        else
            print("Sensor ENDED")            -- By default, ENDED is 2 (but it is not used here)
        end
    end
end

Codea's Editor suggests 'PERSIST' in its autocomplete function, but it does not exist as a built-in global variable (print(PERSIST == nil) outputs true). It is not a substitute for MOVING.

Test5 tab


The Test5 tab implements:

Test5:init()           -- Sets title "filtering"
Test5:setup()
Test5:draw()           -- Does nothing
Test5:touched(touch)   -- Does nothing

Test5:setup()

Test 5 illustrates a feature of the Codea Physics API that is not documented in the in-app reference: filtering. Box2D, and Codea, supports 16 categories of filtering (0 to 15).

The setup() function creates the ground, and (in order of height above the ground in the middle of the viewer) a square box, a circle and a long thin box (a slab, box2).

The square is put in category 1, the circle in 2 and the slab in 3:

                        -- The categories of the ground is {0}, by default.
box.categories    = {1}
circle.categories = {2}
box2.categories   = {3}

The categories field of a body is a table (an array) that lists the categories of which the body is a member.

Each of the dynamic bodies has a mask set that determines which other categories of body the body will interact with:

BodyIn categoriesWill interact with categories (mask)Comment
ground00, 1, ..., 15Will interact with everything, by default
square10, 3Will not interact with circle
circle20, 3Will not interact with square
slab30, 1, 2Will interact with everything created in the test
                        -- The mask of ground is {0, 1, ..., 15}, by default
box.mask    = {0, 3}
circle.mask = {0, 3}
box2.mask   = {0, 1, 2}

Each category has to be set explicitly; a body will not interact with other bodies in its category unless the mask is set. Two bodies will only interact if each is in a category that the other will interact with.

Test6 tab


Test 6 illustrates a feature of the Codea Physics API that is documented in the in-app reference under physics.joint: motors.

The Test6 tab implements:

Test6:init()           -- Sets title "motors (hold on left or right of screen to move car)"
Test6:setup()
Test6:draw()           -- Does nothing
Test6:touched(touch)   
Test6:cleanup()        -- Sets the 'leftJoint' and 'rightJoint' to nil (see 'setup()')

Test6: setup()

The setup() function creates the ground, a polygon (car), two circles as wheels (leftWheel of radius 20 and rightWheel of radius 15) and four square boxes stacked on top of each other:

local ground = createGround()
...
local car = physics.body(POLYGON,
    vec2(-50, 10),  -- Eight points, listed in a counter-clockwise order
    vec2(-50, -10),
    vec2(50, -10),
    vec2(50, 10),
    vec2(30, 10),
    vec2(25, 25),
    vec2(-25, 25),
    vec2(-30, 10))
...
local leftWheel = createCircle(leftPos.x, leftPos.y, 20)
...
local rightWheel = createCircle(rightPos.x, rightPos.y, 15)
...
for y = 25, 70, 15 do -- Steps through 25, 40, 55 and 75, in increments of 15
    createBox(WIDTH/2 + 150, y, 15, 15) -- 15 x 15 square box
end

For bodies, density is 1 by default. That is doubled for car:

car.density = 2.0

For bodies, friction is 0.2 by default. That is increased to 1 for the ground and the two wheels:

ground.friction = 1
leftWheel.friction = 1  
rightWheel.friction = 1

Each of the wheels is joined to car with a REVOLUTE join. As introduced in test 2, a REVOLUTE joint rivets two bodies together but allows them to rotate around the point of the (invisible) rivet. In this case, the position of the rivet is the centre of the wheel in question:

self.leftJoint = physics.joint(REVOLUTE, car, leftWheel, leftWheel.position)
self.rightJoint = physics.joint(REVOLUTE, car, rightWheel, rightWheel.position)

The properties of the leftJoint are set so that the amount of torque the motor can apply to reach the desired motor speed is set to 10:

self.leftJoint.maxMotorTorque = 10

Test6:touched(touch)

The touched(touch) function checks the touch.state. If it is not ENDED the motor of leftJoint is enabled and its speed set to -1000 (if the touch is to the right of the viewer) or 1000 otherwise. If the touch.state is ENDED then the motorSpeed is set to 0:

function Test6:touched(touch)
    if touch.state == BEGAN or touch.state == MOVING then
        self.leftJoint.enableMotor = true     -- Enable the motor
        if touch.x > WIDTH/2 then             -- Touch to right of mid-point?
            self.leftJoint.motorSpeed = -1000 -- Set a negative speed
        else                                  -- Touch to the left of mid-point?
            self.leftJoint.motorSpeed = 1000  -- Set a positive speed
        end

    elseif touch.state == ENDED then          -- Touch ended?
        self.leftJoint.motorSpeed = 0         -- Set speed to 0
    end
end

A negative speed is clockwise (causing the wheel to turn clockwise relative to the car and driving the car to the right) and a positive speed is counter-clockwise (causing the wheel to turn counter-clockwise relative to the car and driving the car to the left).

As the car is massive, its momentum continued to carry it on even after the motor speed of the leftJoint is set to 0.

Test7 tab


The Test7 tab implements:

Test7:init()          -- Sets title "raycasting and aabb queries" and more (see below)
Test7:setup()
Test7:draw()           
Test7:touched(touch)  -- Does nothing   
Test6:cleanup()       -- Does nothing

Test7:init()

In addition to setting the title for test 7, the init() function uses fields point1 to point4 to record four points (as vec2 vectors) and field aabb to record a square (as a table (an array) recording its bottom-left and top-right corners):

self.point1 = vec2(WIDTH/2, 0)
self.point2 = vec2(WIDTH/2, HEIGHT/4)         -- Above point1    
self.point3 = vec2(0, 30)
self.point4 = vec2(WIDTH/2 - 10, 30)          -- To the right of point3
self.aabb   = {vec2(500, 25), vec2(600, 125)} -- 100 x 100 rectangle

Test7:setup()

The setup() function creates the ground, a small square box (near the top of the viewer) and two circles (slightly above the top surface of the ground).

Test7:draw()

The draw() function, illustrates three things: (1) a ray cast from point1 to point2, with a single result (a table); (2) a ray cast from point3 to point4, with multiple results (an array (a table) of tables); and (3) a query returning all bodies within a bounding box (AABB) (an array (a table) of bodies).

In the case of the vertical ray cast from point1 to point2, a vertical force is applied to any body that cuts the ray:

result = physics.raycast(self.point1, self.point2)
if result then
    ...
    result.body:applyForce(vec2(0, 25))

The test also draws the ray on the viewer. If a result is returned, then its point field holds where the ray hit the body:

stroke(0, 255, 0, 255) -- Colour is bright green
strokeWidth(5)         -- Width is 5
if result then    
    line(self.point1.x, self.point1.y, result.point.x, result.point.y)
    ...
else
    line(self.point1.x, self.point1.y, self.point2.x, self.point2.y) -- Full ray if no result
end

In the case of the horizontal ray cast from point3 to point4, a small horizontal force is applied to all the bodies that cut the ray. A small red circle is also drawn to mark the point at which the ray hits:

stroke(0, 255, 0, 255) -- Colour of stroke is bright green
strokeWidth(5)         -- Width is 5
...
line(self.point3.x, self.point3.y, self.point4.x, self.point4.y) -- Draw ray
fill(255, 0, 0, 255)   -- Colour of fill is bright red
results = physics.raycastAll(self.point3, self.point4)
for k, v in ipairs(results) do            -- Iterate through results
    ellipse(v.point.x, v.point.y, 10, 10) -- Draw circle of radius 5 for the hit. As the
                                          -- radius corresponds to the stroke width of 5,
                                          -- there is no need to set noStroke(); the circle
                                          -- is drawn as if no stroke were set.
    v.body:applyForce(vec2(2, 0))         -- Apply force to body
end

In the case of the query to test the contents within a bounding box, the stroke colour depends on whether the array returned has any entries:

strokeWidth(5)
noFill()
-- Query the bounding box and count the number of entries in the array returned
if #physics.queryAABB(self.aabb[1], self.aabb[2]) > 0 then -- More than none?
    stroke(255, 0, 255, 255)   -- Colour is purple
else                                                       -- None?
    stroke(194, 194, 194, 128) -- Colour is transparent light grey
end
rectMode(CORNERS)
-- Draw the bounding box
rect(self.aabb[1].x, self.aabb[1].y, self.aabb[2].x, self.aabb[2].y)

Test8 tab


The Test8 tab implements:

Test8:init()          -- Sets title "collision engine"
Test8:setup()         -- Pauses physics simulation and more (see below)
Test8:draw()           
Test8:touched(touch)  -- Does nothing   
Test8:cleanup()       -- Resumes physics simulation and sets 'box' and 'circle' to nil

Test8:setup()

The setup() function pauses the simulation of physics:

physics.pause()

(The simulation is resumed as part of the cleanup() of test 8.)

It then creates a box and a circle.

Test8:touched(touch)

For those touches not handled by debugDraw:touched(touch) (which does nothing, as the simulation of physics has been paused in Test8:setup()), the position of the box follows the location of the touch:

if touch.state == BEGAN or touch.state == MOVING then
    self.box.position = vec2(touch.x, touch.y)
end

Test8:draw()

The draw() function changes the title of the test, depending on whether or not the box and the circle overlap:

if self.box:testOverlap(self.circle) then
    self.title = "collision engine (overlapping = true)"
else
    self.title = "collision engine (overlapping = false)"
end

The myBody:testOverlap(otherBody) function is documented in Codea's in-app reference. It returns true if the bodies intersect, false otherwise.

Test9 tab


In version 1.4.1 of Codea, moving to test 9 from another test using the slider can cause the Codea application to exit (crash). This is believed to be due to a bug in the Codea Physics API, which is under investigation. One theory is that the bug manifests itself when an edge or chain is created after one or more bodies have been destroyed.

A work-around of sorts is to change the test_number slider so that it defaults to test 9:

function setup()
    ...
    iparameter("test_number", 1, #tests, 9) -- Was: iparameter("test_number", 1, #tests)
    ...
end

Test 9 should be used with the use_accelerometer slider set to 1 (for 'on'). Tilting the iPad will then allow you to use the force of gravity to cause a two-wheeled carriage (or chaise) to roll along the undulating ground - until it comes up against a vertical edge, blocking its path.

The Test9 tab implements:

Test9:init()          -- Sets title "edge chains & edges"
Test9:setup()         
Test9:draw()          -- Does nothing           
Test9:touched(touch)  -- Does nothing   
Test9:cleanup()       -- Sets 'leftJ' only to nil (see 'setup'())

Test9:setup()

The setup function creates a carriage or chaise (chase, a square box) and two wheels (two circles) and joins the wheels to the chaise using REVOLUTE joins:

local chase = createBox(WIDTH/2, 150, 50, 50) -- Create chaise (or carriage)
local leftWheel  = createCircle(chase.x - 15, chase.y - 25, 10) -- Create one wheel...
local rightWheel = createCircle(chase.x + 15, chase.y - 25, 10) -- ...and another
        
self.leftJ  = physics.joint(REVOLUTE, chase, leftWheel,  leftWheel.position)
self.rightJ = physics.joint(REVOLUTE, chase, rightWheel, rightWheel.position)

The chaise is created a little way above the undulating ground (at height 150). The force of gravity will cause it to fall down to the ground when the test begins (if the iPad is held upright).

The setup() function also creates an undulating ground made up of a (static) chain (of edges):

local points = {} -- A table for the points in the chain
for i = 0, WIDTH, WIDTH/30 do -- Step across the width of the viewer in 30 increments
    -- The height of each point in the chain depends on a sin function
    table.insert(points, vec2(i, math.sin(i * 4) * 20 + 60))
end

local ground = physics.body(CHAIN, false, unpack(points)) -- Create body of type CHAIN
debugDraw:addBody(ground)                                 -- Add to bodies

As the math.sin() function returns a value between -1 and 1, the height of the ground will undulate between 40 and 80.

Finally, the setup() function creates a single vertical (static) edge, that intersects the undulating ground, placed at a point three-quarters the width of the viewer:

local edge = physics.body(EDGE,
    vec2(WIDTH*0.75, 0),
    vec2(WIDTH*0.75, 250))
debugDraw:addBody(edge)

Updated