Share Your Code

Billy Cart in Corona

Posted by MarkHenryC, Posted on February 11, 2011, Last updated August 24, 2011

GitHub URL: 
https://github.com/MarkHenryC/BillyCart

For Corona users getting started with physics this project skeleton may be useful. Items of of interest:

Wheel-powered vehicle
Auto-generate physics landscapes
Vector drawing
Custom physics shapes
Camera tracking
Physics collision callbacks

Get the code from github rather than here, as an additional config file is required.

-----------------------------------------------------------------------
-- main.lua
-----------------------------------------------------------------------
 
--[[
 
         Mark H Carolan 2010
        
         Demo of rudimentary cart object with powered wheels
           responding to physics-based terrain.
        
         Intended for Corona beginners. Demonstrates a few
         basic ways of handling sidescrolling with physics.
        
         Illustrates:
        
           Tracking moving object with "camera".
           Re-using side-scrolling surfaces.
           Using piston and motorised pivot joints.
           Creating physics objects using shape tables.
           Using Corona's vector lines to illustrate shapes.
           Creating modular game components in functions.
           Using timer to call function at a later date.
           Using Lua closures to pass parameter data to a listener.
           Handling collisions with specific objects.
          
          Also serves as a general introduction to using Lua 
                tables and Corona group display objects.
          
          NOTE: This doesn't represent best-practice in Lua
            programming or using the Corona SDK. Hopefully it
            simply offers some insights into getting started
            with Corona from my perspective as a relative
            newcomer to both Lua and Corona. If any of my comments
            etc. are incorrect, please let me know.
        
--]]
 
require "physics"
physics.start()
 
-----------------------------------------------------------------------
-- There appears to be an odd side effect with
-- the (debug) drawing of joints. You can't add
-- joints to a display group, so they appear to break away
-- from the visual object that contains
-- the physics bodies they're attached to.
-- Not a big deal, as it only affects the debug and hybrid
-- drawing of the joints, but a little disconcerting at first.
 
-- physics.setDrawMode("hybrid")
-----------------------------------------------------------------------
 
local JUMP_POWER = 7
local CAMERA_OFFSET = 100
 
local function offset(x, y)
        return x, y - display.contentHeight
end
 
-----------------------------------------------------------------------
-- Utility function:
-- Create ground shapes and display as lines
-----------------------------------------------------------------------
 
local function newContour(params)
        local physicsShape = {}
        
        local coords = params.coords
        local bodyType = params.bodyType
        
        local contour = display.newLine(coords[1], coords[2], coords[3], coords[4])
        
        physicsShape[1], physicsShape[2] = offset(coords[1], coords[2])
        physicsShape[3], physicsShape[4] = offset(coords[3], coords[4])
        
        if params.color then
                contour:setColor(params.color[1], params.color[2], params.color[3])
        else
                contour:setColor(255, 0, 0, 255)
        end
        contour.width = 8
        
        for i = 5, #coords, 2 do
                contour:append(coords[i], coords[i+1])
                physicsShape[i], physicsShape[i+1] = offset(coords[i], coords[i+1])
        end
 
        -----------------------------------------------------
        -- Visuals must be closed by repeating startpoint
        -- but not physics shapes, which will crash simulator.
        -----------------------------------------------------
        
        contour:append(coords[1], coords[2])
        
        physics.addBody(contour, bodyType or "static", 
                { friction = 1, bounce = 0.0, shape=physicsShape}       )
        
        return contour
end
 
----------------------------------------------------------------------- 
-- Simple slider for speed control
-----------------------------------------------------------------------
 
local function newSlider(params)
        local p = params or {}
        local g = display.newGroup()
        
        local back = display.newRect(0, 0, p.width or 120, p.height or 32)
        g:insert(back)
        
        back:setFillColor(0, 0, 200)
        back.strokeWidth = 3
        back:setStrokeColor(128, 128, 128)
        
        local button = display.newRect(0, 0, p.buttonWidth or 32, p.buttonHeight or 34) 
        g:insert(button)
        
        button:setFillColor(0, 200, 0)
        button.strokeWidth = 3  
        button:setStrokeColor(200, 200, 200)
        
        local leftLimit = back.x-back.width/2
        local rightLimit = back.x+back.width/2
        
        button.x = leftLimit
        
        g.x = params.x or 160
        g.y = p.y or 280
        
        button.y = button.y-1
        local currentXReading = leftLimit
        
        local callbackFunc = p.callbackFunc
        local callbackObject = p.callbackObject
        
        button:addEventListener("touch", g)
        
        ------------------------------------------------
        -- Nothing much to see here. Just a slider made
        -- with a backing strip and a square button.
        -- the slider is passed an object and a function
        -- to call on that object when the button is moved.
        -- for simplicity, output values are 0.0 to 1.0
        ------------------------------------------------
        
        function g:touch(event)
                if event.phase == "began" then
                        display.getCurrentStage():setFocus(button)
                elseif event.phase == "moved" then
                        local movementX = event.x - event.xStart
                        local posX = currentXReading + movementX
                        event.target.x = posX 
                        if event.target.x < leftLimit then
                                event.target.x = leftLimit
                        elseif event.target.x > rightLimit then
                                event.target.x = rightLimit
                        end
                        
                        local val = event.target.x
                
                        -----------------------------
                        -- Output value 0.0 .. 1.0
                        -----------------------------
                        
                        callbackFunc(callbackObject, val / back.width) 
                elseif event.phase == "ended" then
                        currentXReading = button.x
                        display.getCurrentStage():setFocus(nil)
                end
                return true
        end
        
        function g:set(f) -- 0.0 .. 1.0
                button.x = f * button.width
                currentXReading = leftLimit + f * back.width
        end
        
        return g
end
 
-----------------------------------------------------------------------
-- Utility function:
-- Create a circular polygon
-----------------------------------------------------------------------
 
local function createNGon(radius, sides)
        local points = {}
        local rad = math.rad(360/sides)
        
        ------------------------------------------------
        -- Create anything up to an octagon (the max sides
        -- allowed by Corona in a vector line) based on
        -- radius of an enclosing circle.
        ------------------------------------------------
        
        for i = 1, sides do
                local radAngle = rad*(i-1)
                
                local rSin = math.sin(radAngle)
                local rCos = math.cos(radAngle)
                                
                points[#points+1] = rSin * radius
                points[#points+1] = rCos * radius
                
        end
        
        ------------------------------------------------
        -- For poly lines, define the first and second xy
        -- coords, creating a single line, then add points
        -- from there to create poly line. For closed polygon
        -- simply repeat the first xy coord at the end.
        ------------------------------------------------
        
        local poly = display.newLine(points[1], points[2], points[3], points[4])
        for i = 5, #points, 2 do
                poly:append(points[i], points[i+1])
        end 
        
        poly:append(points[1], points[2])
        
        poly:setColor(255, 255, 255)
        poly.width = 2
        
        local g = display.newGroup()
        g:insert(poly)
        display.setDefault("fillColor", 0, 255, 0, 200)
        g:insert(display.newCircle(0, 0, radius+2))
        
        --------------------------------------------
        -- It's necessary to put this into a group.
        -- It seems that the registration point, when
        -- a physics object is attached, is at the start
        -- point of the multi-segment line. Setting
        -- registration point doesn't fix this. Putting
        -- the line into a group does. Also, the wheel is 
        -- just for looks. The wheel could just as easily 
        -- be square, as it's only the physics body that
        -- determines the behaviour.
        ----------------------------------------------
        
        return g
end
 
-----------------------------------------------------------------------
-- Utility function:
-- Create the Billy Cart with a box and 2 circles
-----------------------------------------------------------------------
 
local function newBillyCart(params)
        local cart = display.newGroup()
 
        local originX = params.originX or 0
        local originY = params.originY or 0
        
        local body = params.body or display.newRect(0, 0, 102, 20)      
        body:setFillColor(0, 0, 255)            
 
        -----------------------------------------
        -- Shaped vector wheel (to show rotation)
        -----------------------------------------
        
        display.setDefault("fillColor", 0, 255, 0, 200)
        
        local wheelLowerLeft = createNGon(16, 5)
        local wheelLowerRight = createNGon(16, 5)               
        
        -----------------------------------------
        -- Anchor for shock absorber
        -----------------------------------------
        
        display.setDefault("fillColor", 255, 0, 0, 200)
                
        local shockBaseLeft = display.newRect(0, 0, 14, 14)
        local shockBaseRight = display.newRect(0, 0, 14, 14)    
        
        -----------------------------------------
        -- Shock absorber bar
        -----------------------------------------
        
        display.setDefault("fillColor", 255, 255, 0, 200)
        
        local lowerShockLeft = display.newRect(0, 0, 8, 28)             
        local lowerShockRight = display.newRect(0, 0, 8, 28)    
        
        -----------------------------------------
        -- Get everything in the right place
        -----------------------------------------
        
        function cart:setOrigin(originX, originY)
                body.x = originX
                body.y = originY+10     
                wheelLowerLeft.x = originX-40
                wheelLowerLeft.y = originY+20   
                wheelLowerRight.x = originX+40
                wheelLowerRight.y = originY+20
                shockBaseLeft.x = originX-40
                shockBaseLeft.y = originY+16
                shockBaseRight.x = originX+40
                shockBaseRight.y = originY+16
                lowerShockLeft.x = originX-40
                lowerShockLeft.y = originY+24
                lowerShockRight.x = originX+40          
                lowerShockRight.y = originY+24  
        end
        
        cart:setOrigin(originX, originY)
        
        -----------------------------------------
        -- put all components into single group
        -----------------------------------------
        
        cart:insert(body)
        cart:insert(wheelLowerLeft)
        cart:insert(wheelLowerRight)
        cart:insert(shockBaseLeft)
        cart:insert(shockBaseRight)
        cart:insert(lowerShockLeft)
        cart:insert(lowerShockRight)
        
        -----------------------------------------
        -- Make into physics objects
        -----------------------------------------
        
        physics.addBody(body, {density=0.5, bounce = 0, friction = 0})
        physics.addBody(wheelLowerLeft, {density=0.9, bounce = 0.0, friction = 4.0, radius=16})
        physics.addBody(wheelLowerRight, {density=0.9, bounce = 0.0, friction = 4.0, radius=16})
        physics.addBody(shockBaseLeft, {density=0.05, bounce = 0, friction = 0})
        physics.addBody(shockBaseRight, {density=0.05, bounce = 0, friction = 0})
        physics.addBody(lowerShockLeft, {density=0.02, bounce = 0, friction = 0})
        physics.addBody(lowerShockRight, {density=0.02, bounce = 0, friction = 0})
        
        -----------------------------------------
        -- Create joints to define behaviour
        -----------------------------------------
        
        local shockBaseLeftJoint = physics.newJoint ("weld", body, 
                shockBaseLeft, shockBaseLeft.x, shockBaseLeft.y)
        
        local shockBaseRightJoint = physics.newJoint ("weld", body, 
                shockBaseRight, shockBaseRight.x, shockBaseRight.y)
 
        local lowerShockLeftJoint = physics.newJoint ("piston", 
                shockBaseLeft, lowerShockLeft, shockBaseLeft.x, shockBaseLeft.y, 0, 10)
                
        local lowerShockRightJoint = physics.newJoint ("piston", 
                shockBaseRight, lowerShockRight, shockBaseRight.x, shockBaseRight.y, 0, 10)
        
        local lowerConnectorLeftJoint = physics.newJoint ("weld", 
                shockBaseLeft, lowerShockLeft, shockBaseLeft.x, shockBaseLeft.y)
                
        local lowerConnectorRightJoint = physics.newJoint ("weld", 
                shockBaseRight, lowerShockRight, shockBaseRight.x, shockBaseRight.y)
                                
        local wheelLowerLeftJoint = physics.newJoint ("pivot", 
                lowerShockLeft, wheelLowerLeft, wheelLowerLeft.x, wheelLowerLeft.y)
        
        local wheelLowerRightJoint = physics.newJoint ("pivot", 
                lowerShockRight, wheelLowerRight, wheelLowerRight.x, wheelLowerRight.y)
                
        local wheelConnectorJoint = physics.newJoint("distance", 
                wheelLowerLeft, wheelLowerRight, wheelLowerLeft.x, wheelLowerLeft.y, 
                wheelLowerRight.x, wheelLowerRight.y)
        
        ------------------------------------------------
        -- Turn the pivot joints into motors.
        ------------------------------------------------
        
        wheelLowerLeftJoint.isMotorEnabled = true
        wheelLowerLeftJoint.motorSpeed = 0
        wheelLowerLeftJoint.maxMotorTorque = 100000
 
        wheelLowerRightJoint.isMotorEnabled = true
        wheelLowerRightJoint.motorSpeed = 0
        wheelLowerRightJoint.maxMotorTorque = 100000
 
        ------------------------------------------------
        -- Try to stop wild movements. These figures
        -- probably need a lot of tweaking.
        ------------------------------------------------
        
        body.angularDamping = 50
 
        shockBaseLeft.linearDamping = 10
        shockBaseRight.linearDamping = 10
        
        lowerShockLeft.angularDamping = 50
        lowerShockRight.angularDamping = 50
 
        lowerShockLeft.linearDamping = 50
        lowerShockRight.linearDamping = 50
        
        -----------------------------------------
        -- Set spin of pivot joints to power cart
        -----------------------------------------
        
        function cart:setSpeed(s)
                wheelLowerLeftJoint.motorSpeed = s
                wheelLowerRightJoint.motorSpeed = s
        end
 
        -----------------------------------------
        -- Jump by puttin upward impulse on wheels
        -----------------------------------------
        
        function cart:lift()
                wheelLowerLeft:applyLinearImpulse(0, -JUMP_POWER, 
                wheelLowerLeft.x, wheelLowerLeft.y)
                wheelLowerRight:applyLinearImpulse(0, -JUMP_POWER, 
                wheelLowerRight.x, wheelLowerRight.y)
        end
        
        -----------------------------------------
        -- Reset after a prang, maintaining x position
        -----------------------------------------
        
        function cart:reset()
                body.rotation = 0
                self:setOrigin(body.x, body.y - 20)
                self:setSpeed(0)
        end
 
        -----------------------------------------
        -- Change back to normal color after
        -- embarrassed blush from hitting something.
        -- NOTE: 'self' is handled manually because this
        -- is a listener function (no implicit self, as
        -- it's not called by the timer routine with 
        -- the ':' syntax)
        -----------------------------------------
        
        function cart.revert(self)
                body:setFillColor(0, 0, 255)
        end
        
        -----------------------------------------
        -- Handle collision with other objects.
        -- If object is a known type 
        -- (like "b" for boulder) respond accordingly.
        -----------------------------------------
        
        function cart:collision(event)
                if event.phase == "ended" then
                        if event.other.id and event.other.id == "b" then
                                body:setFillColor(255, 0, 0)
        
                                -----------------------------------------
                                -- Use Lua closure to pass 'self' to listener.
                                -----------------------------------------
                                
                                local doRevert = function() return self.revert(self) end
                                
                                -----------------------------------------
                                -- Switch off hit status after 2 seconds
                                -----------------------------------------
                                
                                timer.performWithDelay(2000, doRevert)
                        end
                end
        end
        
        wheelLowerLeft:addEventListener("collision", cart)
        wheelLowerRight:addEventListener("collision", cart)
        
        -----------------------------------------
        -- Body needs to be accessible from outside
        -----------------------------------------
        
        cart.body = body
        
        return cart
end
 
-----------------------------------------------------------------------
-- Main game object
-----------------------------------------------------------------------
 
function newBillyCartGame(params)
        local camera = display.newGroup()
        
        local p = params or {}
 
        -----------------------------------------
        -- Our home-made, slightly ricketty 
        -- Billy Cart.
        -----------------------------------------
        
        local cart = newBillyCart
        {
                originX = 100,
                originY = 160,
 
        }               
        
        camera:insert(cart)
        
        -----------------------------------------
        -- Obstacle
        -----------------------------------------
        
        local boulder = display.newCircle(0, 0, 12)
        boulder:setFillColor(255, 255, 0)
        physics.addBody(boulder, {density=0.5, bounce = 0.1, friction = 1.0})
        camera:insert(boulder)
        boulder.x = -display.contentWidth -- drop behind
        boulder.y = 0
        boulder.id = "b" -- for collision detection
        
        -----------------------------------------
        -- Specify function for slider to call
        -----------------------------------------
        
        local function setSpeed(self, amt)
                local speed = amt * 2000
                cart:setSpeed(speed)    
        end
 
        -----------------------------------------
        -- Create slider control
        -----------------------------------------
        
        local slider = newSlider
        {
                callbackFunc = setSpeed,
                callbackObject = camera,
        }
        
        -----------------------------------------
        -- Slope definition with variable peak
        -----------------------------------------
        
        local function newSlope(peakY)
                local slope = 
                {
                        0, 320,
                        0, 260,
                        200, peakY,
                        280, peakY,
                        480, 260, 
                        480, 320
                }
                return slope
        end
        
        -----------------------------------------
        -- Generate landscape shapes
        -----------------------------------------
        
        local peaks = { 259, 150, 100, 50, 249, 239, 10, 219, 70, 239, 150, 50 }
                
        local lastXPos = 0
        local shapeStartIndex = camera.numChildren+1
        local lastIndex = shapeStartIndex
        local shapeEndIndex = shapeStartIndex + #peaks-1
        
        local panels = {}
        
        local colors = 
        {
                { 255, 0, 0 },
                { 0, 0, 255 },
                { 0, 255, 255 },
                { 255, 255, 0 },
                { 0, 255, 0 }
        }
        
        for i = 1, #peaks do
                local ground = newContour
                {
                        coords = newSlope(peaks[i]),
                        color = colors[i % #colors]
                }
                ground.x = ground.x + (i-1) * 480
                ground.index = i
                lastXPos = ground.x
                camera:insert(ground)
                panels[#panels+1] = ground
        end
        
        -----------------------------------------
        -- Camera tracking.
        -- Offscreen landscape creation.
        -----------------------------------------
        
        function camera:enterFrame(event)       
 
                -----------------------------------------
                -- Reposition "camera"
                -----------------------------------------
                
                self.x = -cart.body.x + CAMERA_OFFSET
                self.y = -cart.body.y + CAMERA_OFFSET
                
                -----------------------------------------
                -- Reuse physics ground shapes
                -----------------------------------------
                
                if cart.body.x > lastXPos then          
                        lastXPos = lastXPos + display.contentWidth
                        
                        local slope = self[lastIndex]
                        slope.x = lastXPos
                        lastIndex = lastIndex + 1
                        if lastIndex > shapeEndIndex then
                                lastIndex = shapeStartIndex
                        end
                        
                end
                
                -----------------------------------------
                -- Reset if tipped over
                -----------------------------------------
                
                if cart.body.rotation < -135 
                        or cart.body.rotation > 135 then
                        cart:reset()
                        slider:set(0)
                end
 
                -----------------------------------------
                -- Drop boulder ahead of cart if it's 
                -- offscreen left
                -----------------------------------------
                
                if boulder.x < cart.body.x - display.contentWidth then  
                        boulder.x = cart.body.x + display.contentWidth
                        boulder.y = 0
                end
                
        end
        
        --------------------------------
        -- Jump
        --------------------------------
        function camera:touch(event)
                if event.phase == "began" then
                        cart:lift()
                end
        end
        
        function camera:start()
                Runtime:addEventListener("enterFrame", self)
                Runtime:addEventListener("touch", self)
        end
        
        return camera
end
 
-----------------------------------------------------------------------
-- Here's where it starts:
-----------------------------------------------------------------------
 
local game = newBillyCartGame()
 
game:start()

Compatibility: 
Corona Game Edition


Replies

thegwill
User offline. Last seen 35 weeks 5 days ago. Offline
Joined: 21 Aug 2010

Thanks for sharing - some really useful stuff in there :)

ChrisSlee
User offline. Last seen 2 days 8 hours ago. Offline
Joined: 11 Feb 2011

Mark, I really appreciate this. Thanks!

Rob2
User offline. Last seen 11 weeks 4 days ago. Offline
Joined: 14 Apr 2010

Totally fantastic! Thanks very much, surely destined to become a classic!!

diginutscorp
User offline. Last seen 2 years 13 weeks ago. Offline
Joined: 24 Feb 2011

Can anyone tell me how I can replace the custom shapes(slopes) with png images in Billy Cart code

Hendrix000007
User offline. Last seen 3 weeks 2 days ago. Offline
Joined: 26 Feb 2011

awesome Mark!
thankz for sharing this usefull stuff
...clean n nice code 2!

MarkHenryC
User offline. Last seen 1 year 4 weeks ago. Offline
Joined: 11 Nov 2009

@ diginutscorp:

Modify newContour() so that you pass an image as the "contour" display object, instead of it being generated by creating lines. Just make sure the image you pass in has the same general shape as the "coords" param.

Sorry such a late reply. I don't visit here so regularly.

cheetahdvdburner
User offline. Last seen 1 year 44 weeks ago. Offline
Joined: 7 Apr 2011

Awesome! that is a good looking cart! Thank you

diginutscorp
User offline. Last seen 2 years 13 weeks ago. Offline
Joined: 24 Feb 2011

great code, thank but can you tell me how do simply make the slider . I have tried to modify the code to simply use the slider.

Please any help to simply make the slider is very much appreciated

thank you mark

mrbarry
User offline. Last seen 1 year 25 weeks ago. Offline
Joined: 25 Apr 2011

Thanks for the code Mark. Do you or anybody else know how to place an object on top of the cart without it compressing the suspension completely?

I've tried letting a block (with minimal density) fall on top of the cart and the suspension immediately compresses all the way making the cart behave erratically.

Thank you.

diginutscorp
User offline. Last seen 2 years 13 weeks ago. Offline
Joined: 24 Feb 2011

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
local function newSlider(params)
        local p = params or {}
       -- local g = display.newGroup()
        
      back = display.newRect(0, 0, p.width or 20, p.height or 300)
        g:insert(back)
       -- back.isVisible = false
        back:setFillColor(225,140, 0)
        back.strokeWidth = 1
        back:setStrokeColor(128, 128, 128)
          back.y = 8
       button = display.newRect(0, 0, p.buttonWidth or 20, p.buttonHeight or 32) 
        g:insert(button)   
         
        button:setFillColor(139, 0, 0)
        button.strokeWidth = 1 
        button:setStrokeColor(128, 128, 128)
        physics.addBody(button,"kinematic", {density=0, bounce = 0.1, friction = 1})
      
        
      local leftLimit = back.y + back.width/2
       --   local leftLimit =   100
        
        
        local rightLimit = back.y-back.width/2
        
        button.y = leftLimit
        
        g.x = params.x or 10 -- x  co-ordinate
        g.y = p.y or  100  --  y  co- donate for button
        
        button.x = button.x -1
       button.y = button.y + 200
       local currentXReading =    100
        
       -- local callbackFunc = p.callbackFunc
       -- local callbackObject = p.callbackObject
        
       button:addEventListener("touch", g)
        
        ------------------------------------------------
        -- Nothing much to see here. Just a slider made
        -- with a backing strip and a square button.
        -- the slider is passed an object and a function
        -- to call on that object when the button is moved.
        -- for simplicity, output values are 0.0 to 1.0
        ------------------------------------------------
        
         function g:touch(event)
                if event.phase == "began" then
                        display.getCurrentStage():setFocus(button)
                elseif event.phase == "moved" then
                        local movementX = event.y - event.yStart
                      local posX = currentXReading + movementX
                        event.target.y = posX 
                        if event.target.y > leftLimit then
                                event.target.y = leftLimit
                        elseif event.target.y < rightLimit then
                                event.target.y = rightLimit
                        end
                        
                        local val = event.target.y
                
                        -----------------------------
                        -- Output value 0.0 .. 1.0
                        -----------------------------
                        
                      --  callbackFunc(callbackObject, val / back.width) 
                elseif event.phase == "ended" then
                        currentXReading = button.y
                        display.getCurrentStage():setFocus(nil)
                end
                return true
        end
        
        function g:set(f) -- 0.0 .. 1.0
                button.x = f * button.width
                currentXReading = leftLimit + f * back.width
        end
        
        return g
end
 
 
 local slider = newSlider
        {
               -- callbackFunc = setSpeed,
              --  callbackObject = camera,
        }
        
       
can anyone help me with this. I would like to be able to scroll the button up and down. I can't get it to work. 
Any help is appreciated