Wiki
Clone wikiCore / PhysicsLab
Back to Codea's Example Projects
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:
Tab | Comment |
---|---|
PhysicsDebugDraw | Establishes the class PhysicsDebugDraw . |
Test1 | Establishes the class Test1 . This is a basic introduction to bodies. |
Test2 | Establishes the class Test2 . This is a basic introduction to joints. |
Test3 | Establishes the class Test3 . This is an introduction to gravity scale. |
Test4 | Establishes the class Test4 . This is an introduction to sensors. |
Test5 | Establishes the class Test5 . This is an introduction to filtering. |
Test6 | Establishes the class Test6 . This is an introduction to motors. |
Test7 | Establishes the class Test7 . This is an introduction to (1) raycastings and (2) aabb queries. |
Test8 | Establishes the class Test8 . This is an introduction to the collision engine. |
Test9 | Establishes 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() |
---|---|---|---|---|---|---|
Required | Required | Required | Required | Optional | Optional | |
Test1 | Yes | Yes | Yes - does nothing | Yes | No | No |
Test2 | Yes | Yes | Yes - does nothing | Yes - does nothing | No | No |
Test3 | Yes | Yes | Yes - does nothing | Yes - does nothing | No | No |
Test4 | Yes | Yes | Yes - does nothing | Yes - does nothing | Yes | No |
Test5 | Yes | Yes | Yes - does nothing | Yes - does nothing | No | No |
Test6 | Yes | Yes | Yes - does nothing | Yes | No | Yes |
Test7 | Yes | Yes | Yes | Yes - Does nothing | No | Yes - does nothing |
Test8 | Yes | Yes | Yes | Yes | No | Yes |
Test9 | Yes | Yes | Yes - does nothing | Yes - does nothing | No | Yes |
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 type | Joining |
---|---|
REVOLUTE | the static circle and the dynamic box |
DISTANCE | box and dynamic circle2 |
PRISMATIC | the 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:
Body | In categories | Will interact with categories (mask) | Comment |
---|---|---|---|
ground | 0 | 0, 1, ..., 15 | Will interact with everything, by default |
square | 1 | 0, 3 | Will not interact with circle |
circle | 2 | 0, 3 | Will not interact with square |
slab | 3 | 0, 1, 2 | Will 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