Boa: a Physics Lab experiment with linearDamping

The code below is intended to be added as a new tab to a copy of the Physics Lab example project and TestBoa() as the first test in the list of tests in the Main tab’s setup() function. It makes use of the linearDamping property of a physics.body, that is not currently documented in the in-app reference. As the test ‘turns off defaultGravity’, it is important that it is the first test.

An example of the test running is below:


--
-- Boa, a Physics Lab test
-- Add TestBoa() to tests = {TestBoa(), ...} list in Main setup()
--
TestBoa = class()

local HEAD, BODY, TAIL = 1, 2, 3
local size = 10 -- Sets the scale of the snake

function TestBoa:init()
    self.title="Boa - Touch and drag head to move"
    local linkPoints = {}
    linkPoints[HEAD] = {
        vec2(1, 2),
        vec2(0.5, 4),
        vec2(-0.5, 4),
        vec2(-1, 2),
        vec2(-1, -3),
        vec2(0, -4),
        vec2(1, -3)}
    linkPoints[BODY] = {
        vec2(1, 3),
        vec2(0, 4),
        vec2(-1, 3),
        vec2(-1, -3),
        vec2(0, -4),
        vec2(1, -3)}
    linkPoints[TAIL] = {
        vec2(1, 3),
        vec2(0, 4),
        vec2(-1, 3),
        vec2(-1, 2),
        vec2(0, -4),
        vec2(1, 2)}
    for k, v in ipairs(linkPoints) do
        for i = 1, #v do
            v[i] = v[i] * size
        end
    end
    self.createLink = function(centre, type)
        local points = linkPoints[type]
        local link = physics.body(POLYGON, unpack(points))
        link.x = centre.x
        link.y = centre.y
        if type ~= HEAD then link.linearDamping = 1 end
        debugDraw:addBody(link)
        return link
    end
end

function TestBoa:setup()
    self.links = {}
    self.joints = {}
    local nLinks = 10 -- Sets the length of the snake
    self.links[1] = self.createLink(vec2(WIDTH/2, HEIGHT * 4/5), HEAD)
    for i = 2, nLinks do
        local type = BODY
        if i == nLinks then type = TAIL end
        local n = #self.links
        local ol = self.links[n]
        local oc = ol.position
        local nc = oc + vec2(0, -6 * size)
        local nl = self.createLink(nc, type)
        self.links[n + 1] = nl
        local joint = physics.joint(REVOLUTE, nl, ol, (oc + nc) / 2)
        debugDraw:addJoint(joint)
    end
    -- Turn off defaultGravity (if first test setup by Physics Lab)
    physics.gravity(0, 0)
end

function TestBoa:cleanup()
    self.links = nil
    self.joints = nil
end

function TestBoa:draw()
    local head = self.links[1]
    local hPos = head.position
    local hAng = head.angle
    pushStyle()
    pushMatrix()
    translate(hPos.x, hPos.y)
    rotate(hAng)
    -- Draw a forked tongue
    lineCapMode(ROUND)
    stroke(255, 0, 0)
    strokeWidth(10)
    line(0, 4 * size, 0, 5 * size)
    strokeWidth(7)
    line(0, 5 * size, -0.3 * size, 5.5 * size)
    line(0, 5 * size, 0.3 * size, 5.5 * size)
    -- Draw two eyes
    noStroke()
    fill(255)
    ellipse(-0.7 * size, 2 * size, 0.8 * size)
    ellipse(0.7 * size, 2 * size, 0.8 * size)
    popMatrix()
    popStyle()
end 

function TestBoa:touched(touch)
end 

I get error on line 77.

Show me your main and I’ll try it.

Nice one @mpilgrem.

@wrmichael - you add it as a new class, TestBoa ,to the physics lab example then modify line 11 in Main to include the new class. It should now read

    tests = {Test1(), Test2(), Test3(), Test4(), Test5(), Test6(), Test7(), Test8(), TestBoa()}

Cool. But my Bo just drops and falls off the screen.

@wrmichael You might want to try turning off gravity ;).

I see the problem @wrmichael. It lies in setup() in Main, which preserves defaultGravity only after having initialised all the tests and having setup the first test. It also lies in draw(), that repeatedly resets gravity to defaultGravity.

The quick fix is to make sure that TestBoa() is the first test listed in tests = {TestBoa(), ... }.

Hello @John. I have added the following suggested changes, to the Physics Lab example project, to the Issue Tracker:


A. Preserve defaultGravity before the tests are initialised and, importantly, before tests[1] is set up through setTest(currentTestIndex). That is, move the following line in the existing setup() function immediately after creating debugDraw = PhysicsDebugDraw():

defaultGravity = physics.gravity()

B. Allow a test to override the default defaultGravity by changing the logic at the end of the existing draw() function as follows:


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


If I make the changes above to a copy of the Physics Lab example project, in the TestBoa:setup() function I need to replace the final line:

physics.gravity(0, 0)

with

self.defaultGravity = vec2()

The position that TestBoa() appears in tests = {...} then does not matter.

The following replacement for TestBoa:draw() colours in the snake’s body. The helper function is required because triangulate() requires points to be in the opposite order (clockwise) to that required by physics.body(POLYGON, ...) (anti-clockwise).


function TestBoa:draw()
    local head = self.links[1]
    pushStyle()
    pushMatrix()
    -- Colour in the body yellow-green
    for i = 1, #self.links do
        local link = self.links[i]
        local m = mesh()
        m.vertices = triangulate(reverse(link.points))
        m:setColors(196, 255, 0)
        resetMatrix()
        translate(link.x, link.y)
        rotate(link.angle)
        m:draw()
    end
    resetMatrix()
    translate(head.x, head.y)
    rotate(head.angle)
    -- Draw a forked red tongue
    lineCapMode(SQUARE)
    stroke(255, 0, 0)
    strokeWidth(10)
    line(0, 4 * size, 0, 5 * size)
    lineCapMode(ROUND)
    strokeWidth(7)
    line(0, 5 * size, -0.3 * size, 5.5 * size)
    line(0, 5 * size, 0.3 * size, 5.5 * size)
    -- Draw two black eyes
    noStroke()
    fill(0)
    ellipse(-0.7 * size, 2 * size, 0.8 * size)
    ellipse(0.7 * size, 2 * size, 0.8 * size)
    popMatrix()
    popStyle()
end

-- Helper function
function reverse(p)
    local n = #p
    local rp = {}
    for i = 1, n do
        rp[i] = p[n - i + 1]
    end
    return rp
end

The code below is a variation on the same theme, showing how one snake can interact with another. It assumes the changes to a copy of the Physics Lab example project in my post above.


--
-- Box of Boas, a Physics Lab test
-- Add TestBoa() to tests = {TestBoa(), ...} list in Main setup()
--
TestBoa = class()

local HEAD, BODY, TAIL = 1, 2, 3

Boa = class()

function Boa:createLink(centre, type)
    local points = self.linkPoints[type]
    local link = physics.body(POLYGON, unpack(points))
    link.sleepingAllowed = false
    link.x = centre.x
    link.y = centre.y
    link.linearDamping = 1
    if type == BODY then link.linearDamping = 3 end
    debugDraw:addBody(link)
    return link
end

function Boa:init(hPos, length, size, colour)
    self.size = size
    self.colour = colour
    self.linkPoints = {}
    self.linkPoints[HEAD] = {
        vec2(1, 2),
        vec2(0.5, 4),
        vec2(-0.5, 4),
        vec2(-1, 2),
        vec2(-1, -3),
        vec2(0, -4),
        vec2(1, -3)}
    self.linkPoints[BODY] = {
        vec2(1, 3),
        vec2(0, 4),
        vec2(-1, 3),
        vec2(-1, -3),
        vec2(0, -4),
        vec2(1, -3)}
    self.linkPoints[TAIL] = {
        vec2(1, 3),
        vec2(0, 4),
        vec2(-1, 3),
        vec2(-1, 2),
        vec2(0, -4),
        vec2(1, 2)}
    for k, v in ipairs(self.linkPoints) do
        for i = 1, #v do
            v[i] = v[i] * size
        end
    end
    self.links = {}
    self.joints = {}
    self.meshes = {}
    self.links[1] = self:createLink(hPos, HEAD)
    for i = 2, length do
        local type = BODY
        if i == length then type = TAIL end
        local n = #self.links
        local ol = self.links[n]
        local oc = ol.position
        local nc = oc + vec2(0, -6 * size)
        local nl = self:createLink(nc, type)
        self.links[n + 1] = nl
        local joint = physics.joint(REVOLUTE, nl, ol, (oc + nc) / 2)
        debugDraw:addJoint(joint)
    end
    for i = 1, length do
        local link = self.links[i]
        local m = mesh()
        m.vertices = triangulate(reverse(link.points))
        m:setColors(colour)
        self.meshes[i] = m
    end
end

function Boa:draw()
    local size = self.size
    local colour = self.colour
    local head = self.links[1]
    pushStyle()
    pushMatrix()
    -- Colour in body of snake
    for i = 1, #self.links do
        local link = self.links[i]
        resetMatrix()
        translate(link.x, link.y)
        rotate(link.angle)
        self.meshes[i]:draw()
    end
    resetMatrix()
    translate(head.x, head.y)
    rotate(head.angle)
    -- Draw red forked tongue
    lineCapMode(SQUARE)
    stroke(255, 0, 0)
    strokeWidth(10)
    line(0, 4 * size, 0, 5 * size)
    lineCapMode(ROUND)
    strokeWidth(7)
    line(0, 5 * size, -0.3 * size, 5.5 * size)
    line(0, 5 * size, 0.3 * size, 5.5 * size)
    -- Draw two black eyes
    noStroke()
    fill(0)
    ellipse(-0.7 * size, 2 * size, 0.8 * size)
    ellipse(0.7 * size, 2 * size, 0.8 * size)
    popMatrix()
    popStyle()
end

function TestBoa:init()
    self.title="Box of Boas - Touch and drag heads to move"
end

function TestBoa:setup()
    self.defaultGravity = vec2()
    local n = 5
    self.snakes = {}
    for i = 1, n do
        local l = math.random(8, 12)
        local s = math.random(20 - l, 22 - l)
        local c = color(math.random(128, 255), math.random(128, 255),
            math.random(64))
        self.snakes[i] =
            Boa(vec2(WIDTH/(n + 1) * i, HEIGHT * 4/5), l, s, c)
    end
end

function TestBoa:cleanup()
    self.snakes = nil
end

function TestBoa:draw()
    for i = 1, #self.snakes do
        self.snakes[i]:draw()
    end
end 

function TestBoa:touched(touch)
end

-- Helper function
function reverse(p)
    local n = #p
    local rp = {}
    for i = 1, n do
        rp[i] = p[n - i + 1]
    end
    return rp
end

Hello @John. My experiments with linearDamping and angularDamping have identified a minor bug in their implementation, which I have added to the Issue Tracker and the wiki.

Setting is fine but, when getting,myBody.linearDamping and myBody.angularDamping return booleans, rather than the numerical value set for the property of the body. This is because, in Lget in body.mm, lua_pushboolean has been used instead of lua_pushnumber.

Cheers, I’ll fix those right away