Saturday, August 1, 2015

Building a physic-based game with Corona SDK in about 100 lines of code



We will be making is called Apple Rescue. It is a physics-based game where you try to bring the apple to the ground by removing blocks beneath it. Each time the apple falls and hits something, it is damaged, and the game is lost when the apple takes on too much damage. To win the game the you have remove the blocks in the right order.
 

Setting Up the Project

First go to Corona SDK website and download the latest version. Once installed, launch Corona Simulator and click on "NEW PROJECT". In the dialog that appears, enter "Apple Rescue" as your application name. For project template, choose "Blank"



 If you look in the project folder, you will see a bunch of generated files. main.lua is the main file for our game.


Source Code Structure

For simplicity, we will be only adding code to main.lua. For bigger projects, we will need to better organize the code. For now, adding these lines to your main.lua


display.setStatusBar(display.HiddenStatusBar)

local physics = require( "physics" )
physics.start()

local STATE = {wait=0, running=1, won=2, lost=3}

local state = STATE.wait
local doGround, doBackground, doApple, doDamageText

local function gameLost()
end

local function gameWon()
end

local function setupScene()
end

local function makeBlock(image, density, friction, bounce)
end

local function setupBlock(blocks)
end

local function setupApple(apple)
end


local function setupLevel(level)
    setupScene()
    setupBlock(level.blocks)
    setupApple(level.apple)    
end

-- main --

local level1 = {
    blocks = {
        {"block12x1.png", 1, 0.8, 0.1, 195, 295},    
        {"wood4x4.png", 0.5, 0.5, 0.3, 150, 320},
        {"wood4x4.png", 0.5, 0.5, 0.3, 225, 320},
        {"foam4x4.png", 0.2, 0.5, 0.7, 170, 360},
        {"foam4x4.png", 0.2, 0.5, 0.7, 210, 360},
        {"wood4x4.png", 0.5, 0.5, 0.3, 190, 400},
        {"wood4x4.png", 0.5, 0.5, 0.3, 190, 440},
    },
    apple = {
        maxDamage = 5,
        x = 190,
        y = 280
    }

}

setupLevel(level1)
state = STATE.running


This is the skeleton of the game source code. The first line tells the SDK to hide the status bar as we want our game to play in full-screen. Then the physics engine is imported and started. The physics engine must be started before we can use any physics features.

The constants and variables are declared in the next few lines. The variable doGround, doBackground, doApple, and doDamageText will be used to reference the "display objects". Display object is the term used by Corona SDK for any object that can be displayed on screen.

There are 7 functions in this game. gameLost() and gameWon() are called when the player lose or win the game respectively. setupBlock(), setupScene(), and setupApple() are called at the beginning of the game to setup the game level and put display objects on screen.

A level configuration is defined at the bottom. It is a table contains list of block configurations and an apple configuration. Only one level is defined here, but you can add as many levels as you want. To start the level, simply call setupLevel(..) with the level configuration as a sole parameter.
If you wish, you can run current code in Corona Simulator, but you will only see the a blank screen. Let's add background.


function setupScene()

Modify the function setupScene() so it looks like this. The explanation follows in a bit.

local function setupScene()
    doBackground = display.newRect(0, 0, display.contentWidth, display.contentHeight)
    doBackground.anchorX, doBackground.anchorY = 0, 0
    doBackground:setFillColor(177/255, 222/255, 255/255)
    doDamageText = display.newText("damage : 0%",
                    display.contentCenterX, 50, native.systemFontBold, 12 )
    doGround = display.newImage("images/ground.png")
    doGround.kind = "ground"
    doGround.x, doGround.y = display.contentCenterX, 480
    physics.addBody(doGround, "static", { density=1.0, friction=0.3, bounce=0.2 })
end

The blackground is just a blue rectangle. The function display.newRect(..) creates a rectangle display object. Since it need to completely fill the screen, the width and height are set to screen width and height. The constants display.contentWidth and display.contentHeight are just two of many display properties that Corona SDK provides. See here for the completed list.

Notice the proterties anchorX and anchorY? They control the anchor point of the display objects. Their value can be anything from 0 to 1. By default, they are both set to 0.5, which set the anchor point at the very center of the display object. It is very useful when you want to rotate the object as the anchor point is the center of rotation. However, the anchor point also controls where a display object are located when it is first put on screen. In this case, if left at 0.5, the center of our background rectangle will be located at (0,0), which is not what we want. We want the top-left corner to be at (0,0). This is why anchorX and anchorY are both set to 0. Can you guess what would happen if anchorX and anchorY are both set to 1? (answer: the bottom-right of the background rectangle will be at (0,0), so most of the background will be off-screen).

To create a text display object, we call display.newText(..). The anchor point for this text is left at default, this way, when its X axis is at display.contentCenterX the text will always be center-aligned relative to the width of the screen no matter how long the text is.

Adding Physics body

The ground is created next. It will be our first physics-enabled display object. In Corona SDK, attaching physics body to a display object is done by calling physics.addBody(..). This function will automatically determine the width and height of the given display object, and attach appropriate physics body to that object. By default, the physic body will be a rectangle just big enough to contain the given display object, but you can create custom physic body shape if needed, as you will see later in this tutorial.
There are a few types of physic bodies. The default is "dynamic", which means that it will interact with gravity and other forces. However, since we want our ground to stay put, we specify its body type as "static".

      physics.addBody(doGround, "static", { density=1.0, friction=0.3, bounce=0.2 })

A static body type will not interact with gravity. It can still collide with other physics bodies though, which is very useful because we want to know when the apple hits the ground. See here for details on body types.
So far we have added the background and the ground object to the level, it's time to add some blocks.

function setupBlock() and makeBlock()

Modify setupBlock() to looks like this:

local function setupBlock(blocks)
    for i=1,#blocks do
        local block = blocks[i]
        local blockObj = makeBlock("images/" .. block[1], block[2], block[3], block[4])
        blockObj.x, blockObj.y = block[5], block[6]
    end
end

As you can see, setupBlock() just takes the list of block configurations and feed each entry to a function makeBlock(...). Let's define that function now.
 
local function makeBlock(image, density, friction, bounce)
    local rect = display.newImage(image)
    rect.kind = "block"
    physics.addBody( rect, { density=density, friction=friction, bounce=bounce } )
    rect:addEventListener("tap", function(e)
        rect:removeSelf()
    end)
    return rect
end

Function makeBlock(..) expects 4 parameters. They are used to create a display object and an associated physics body. Now, look back at the block configuration list, you should see that different types of blocks have different properties. For example, the bounce value for "foam4x4.png" is much higher that the same values for "wood4x4.png" or "block12x1.png". This is because we want a foam block to be quite bouncy compared to others. See the difference between foam block and wood block in the image below. You can change these values to represent different types of materials. For example, if you want to add a icy block, you can set its friction value to a very low number so it becomes very slippery.


Detecting touches

We want the block to disappear when tapped on. For this, we add the "tap" event listener to each block we create.

    rect:addEventListener("tap", function(e)
        rect:removeSelf()
    end)
The tap event is generated when the player tap on the blocks on screen. The second parameter to addEventListerner() is the callback function, called when the event is generated. When that happens, we simply remove the block from screen by calling removeSelf(), which also remove the physics body attached the block. If you remove blocks at the bottom, expect everything else to fall down!

function setupApple()

Modify setupApple() as follow:

local function setupApple(apple)
    local appleShape = { -5.5,10, 0,11, 5.5,10, 10,1, 7,-6, 0,-7.5, -7,-6, -10,1 }
    doApple = display.newImage("images/apple.png")
    doApple.x, doApple.y = apple.x, apple.y
    doApple.damage = 0
    doApple.maxDamage = apple.maxDamage
    physics.addBody(doApple, { density=1.0, friction=0.3, bounce=0.2, shape=appleShape })
    doApple:addEventListener("postCollision", function(event)
        if event.force < 0.5 or state ~= STATE.running then return end
        doApple.damage = doApple.damage + event.force
        local percent = math.floor(doApple.damage / doApple.maxDamage * 100)
        doDamageText.text = "damage : " .. percent .. "%"
        if percent > 100 then
            gameLost()
            return
        end
        if event.other.kind == "ground" then
            gameWon()
        end
    end)
end
 
Remember about a custom shape for physic body? We need that for the apple. The custom shape is defined in appleShape list, which is a list of coordinates relative to the middle of the apple.

 


Note that a custom shape may have at most 8 points, but you can combine several shapes together to create complex physic body. However, 8 points are enough for this very apple.


Collision Detection

Next step is to detect when the collisions occur. The apple may collide with the blocks or the ground, both will cause damage to the apple. To detect such collisions, the event listener is added to the apple to respond to "postCollision" event. There are many useful properties included in each postCollision event. The one we are interested in is event.force, which basically tell us how hard the apple hits something. Note that the postCollision event is generated even for the smallest collisions, such as when the apple is rolling slowly on the blocks or on the ground. For this game, we ignore the postCollision events when the collision force is too small.

         if event.force < 0.5 or state ~= STATE.running then return end

Note that we also check to make sure that the game is running, since we don't want the apple to take on more damage when the game is over. On another hand, If the force is large enough, it is added to the apple's overall damage.

         apple.damage = apple.damage + event.force

The maximum amount of damage that the apple can take is defined in apple.maxDamage, which is in turn taken from the level's apple configuration. We use this value to calculate the percentage of the damage accumulated so far. The resulted value is then display on screen.

         damageText.text = "damage : " .. percent .. "%"

This value is also used to determine if the damage is too great. If that's the case, the player loses the game.

         if percent > 100 then
            gameLost()
            return
         end
 
Next we check if the object that gets hit by the apple is the ground object. If it is, then the player wins the game.

        if event.other.kind == "ground" then
            gameWon()
        end
 
With this function done, we are almost finish the game. The last two functions are just to inform the player when the game ends.

function gameWin() and function gameLost()

They look like this

local function gameLost()
    state = STATE.lost
    apple:removeSelf()
    local skull = display.newImage("images/skull.png")
    skull.x, skull.y = apple.x, apple.y
    transition.to(skull, {time=2000, y=skull.y - 200, alpha=0})
    damageText.text = "You Lost!\n" .. damageText.text
end

local function gameWon()
    state = STATE.won
    damageText.text = "You Won!\n" .. damageText.text
end
 
When called, they change the state of the game to STATE.lost and STATE.won respectively, so that the apple will not take any more damage. The text also changes to reflect the state of the game.

Display Object Transition

Out of these two functions, the interesting one would be gameLost(). It is there to show how easy to animate the display objects manually in Corona SDK. When the game is lost, the apple is removed from screen and replaced with the image of a skull. It would be boring if the skull doesn't do anything, so we move it up the screen and make it fading out. We do this by apply the transition the the skull.

    transition.to(skull, {time=2000, y=skull.y - 200, alpha=0})

This function takes the display object, and modify its property as specified in the second parameter. In this case, for the next 2 seconds, we want the skull to move 200 pixel up the screen, and gradually reducing its alpha value at the same time. The result is the skull floating up and disappear.
The transition.to (and its sister, transition.from) function can manipulate any property of the display objects. So you can use it to rotate, scale, and most anything you want your display objects to do to make your game interesting. See here for details.

Conclusion

Well, that's it! With Corona SDK, we just create a game in just over 100 lines! It has only one level, but it is easy to add more. If you add features such as level selector or leader chart, you can get pretty close to actually publish this game. For an indie developer, to be able to publish an app in a record time, it is a very good thing. Imagine if you have to do it in Java for Android, and repeat it all over again in Objective-C for iOS devices. Who has time for that?

You can download the whole source code and graphics on GitHub

If you are interested in what else I'm doing with Corona SDK, visit my portfolio at http://www.chupamobile.com/author/chatudom

No comments:

Post a Comment