Building a Platform game with Corona and Lime
**Update** - Added Corona Project Minus Lime Source Code
You can download the project I will be working with here. This does not include the Lime source code, you will have to buy that yourself here.
Note: I have been coding in lua for about a week now. Please let me know if you find anything incorrect or bad practice.
In this tutorial I will be walking you through the code needed to create a platform game. I will be using the Corona SDK and Lime. Lime is a nice library for use with Corona that makes it easy to create tile based games with Tiled. If you don’t know what Tiled is, check it out.
Here is a demo video of the game in action!
If you load the map.tmx from the project into Tiled you will see two layers. The top layer is called “Physics”. This is the layer that will define all our objects. The bottom layer is called “Background” and will hold all the tiles that create the visual aspect of the map.
First thing to do is set up the main entry point for the game. In the main.lua file I have the following code. This will do a bit of setup for the app and create our platform world.
--Hide the status bar
display.setStatusBar( display.HiddenStatusBar )
--Enable multitouch
system.activate( "multitouch" )
--Include platform.lua
local platform = require("platform")
--Call the loadMap function in platform.lua
platform.loadMap("map.tmx")
Next is the initial setup of our platform.lua file.
--platform.lua is defined as a module
module(..., package.seeall)
--Include physics and start the physics simulation
require("physics")
physics.start()
--Load lime and assign to a variable
local lime = require("lime")
function loadMap(tmx)
--Load the map into our instance of lime
map = lime.loadMap(tmx)
--Add a listener so we know when the player object has been loaded from the map
map:addObjectListener("Player", onPlayerLoaded)
--Tell lime to create the visual aspect of the world
visual = lime.createVisual(map)
--Tell lime to create the physics bodies needed for the world
local physical = lime.buildPhysical(map)
end
Now that the map is loaded we can move on to the listener function referenced in the loadMap function. This method will get passed the player object defined in Tiled. Most of the properties are simply stored for use later. The object.sheet property is used to load the image used to represent the main character.
function onPlayerLoaded(object)
--Store properties for later use
topSpeed = tonumber(object.topSpeed)
floatForce = tonumber(object.floatForce)
walkForce = tonumber(object.walkForce)
jumpForce = tonumber(object.jumpForce)
jumpDrag = tonumber(object.jumpDrag)
wallJumpPower = tonumber(object.wallJumpPower)
wallDrag = tonumber(object.wallDrag)
--Create our player display object
player = display.newImage(object.sheet)
--Grab the layer display object and add the player to it
local layer = map:getObjectLayer("Physics")
layer.group:insert(player)
--Add a collision listener and set the initial position of the player
player:addEventListener( "collision", onPlayerCollision )
player.x = object.x
player.y = object.y
--Create a physics body to represent the player and stop it from rotating
physics.addBody(player, { density = object.density, friction = object.friction, bounce = object.bounce })
player.isFixedRotation = true
--Player is ready, setup the joystick
setupJoystick()
end
Before I get into the collision detection I would like to step back and go over touch events and the main loop. Here is how those event listeners are defined.
Runtime:addEventListener("touch", onTouch)
Runtime:addEventListener("enterFrame", onPlatformLoop)
The first is the “touch” event. What I am going to do is use a joystick for left and right movement and a tap anywhere else on screen for the jump. The onTouch Method simply sets a variable to indicate whether the user has touched the screen anywhere. When a touch begins I am also calling a resetJump method. More on that part later though.
local function onTouch(event) if event.phase == "began" then touchDown = true resetJump() elseif event.phase == "ended" then touchDown = false end end
Next up is the main loop. This gets called once every frame, so we can use it for updating our map position among other things.
local function onPlatformLoop(event) --If the player has been created set the map to center around the player if player then map:setPosition(player.x, player.y) end --If the user is touching the screen I will call this method if touchDown then onTouchIsDown() end --For the sake of clean code all player updates will be in another function updatePlayer() end
Next step is to get the joystick controls in. For this I found a nice class for use in Corona SDK here. We setup the joystick when our player is loaded and ready to control. You can consult the joystick class for details on how to use it. I have a simple setup here since it is only used for left and right movement.
--Include joystick.lua
local joystickClass = require( "joystick" )
function setupJoystick()
joystick = joystickClass.newJoystick{
outerImage = "joystickOuter.png",
outerAlpha = 0.0,
innerImage = "joystickInner.png",
innerAlpha = 1.0,
position_x = 0,
position_y = display.contentHeight - (display.contentHeight/2),
onMove = onWalk
}
end
One thing to note about the joystick is the onMove property points to the onWalk function. The onWalk function will test if the player can walk and apply a force to make the player move.
function onWalk( event ) --Make sure the joystick is not in the neutral position if event.joyX ~= false then --Get the current velocity of the player vx, vy = player:getLinearVelocity() --Make sure we don't keep speeding up past the topSpeed local belowSpeed = vx > -topSpeed and vx < topSpeed; --Set a different speed for horizontal movement in the air local force = playerOnGround and walkForce or floatForce --Apply force to move the player if belowSpeed then player:applyForce( event.joyX * force, vy, player.x, player.y) end end end
Back in our game loop we called a function named onTouchIsDown. Right now all this does is call the updateJump function. It may do more in the future but that’s for another day.
function onTouchIsDown() updateJump() end function updateJump() --Set the current force for the jump, will reset when user taps currForce = currForce - jumpDrag --Make sure the force is never negative if currForce < 0 then currForce = 0 end --If on the ground apply a horizontal force if playerOnGround then player:applyForce( 0, -currForce, player.x, player.y ) end --If the player is on the wall make him jump off if playerOnGround == false and playerOnWall then wallJump() end end function resetJump() --Set current force back to its initial value currForce = jumpForce end
In our game loop I was also calling a function named “updatePlayer”. This is meant to adjust movement in different directions based on the state of the player.
function updatePlayer() -- Stop movement on the x axis in the air if joystick.joyX == false and playerOnGround == false then vx, vy = player:getLinearVelocity() player:setLinearVelocity(0, vy) end --Slide down walls slowly when pushing towards the wall if joystick.joyX ~= false and playerOnWall and wallJumping == false then player:setLinearVelocity(0, wallDrag) end end
The wall jump function will allow our player to jump of walls. This is a fun feature in my opinion. I like wall jumping to reach high places.
function wallJump() --Only wall jump if joystick is not in the neutral position if joystick.joyX then --Don't try to wall jump if I am already wall jumping if wallJumping ~= true then --Get the side the wall is on to set our jump direction local xPower = wallOnLeft and wallJumpPower or -wallJumpPower wallJumping = true --Make the player jump player:setLinearVelocity(xPower, -(wallJumpPower*2)) end end end
Last up is collision detection. The physics engine in Corona handles all the real collision detection. What it won’t tell us is what side of an object the player hit. This is important if we want to wall jump. We need to know if we are hitting the ground or a wall. The following function is the listener that was set up when the player was created. In this function event.other refers to the physics body that our player has collided with. If the body has the property IsGround (which was set in the map) then we call hitGround or offGround based on whether the collision began or ended. We pass the collision object along since we will need to know its location and size later.
function onPlayerCollision(event) if event.phase == "began" then if event.other.IsGround then hitGround(event.other) end end if event.phase == "ended" then if event.other.IsGround then offGround(event.other) end end end
When we call the hitGround function we need to test what side was hit and whether the collision was valid.
function hitGround(ground) --The player is no longer wall jumping if it hit a new ground object wallJumping = false --Check which side of the ground the player hit local hitSide = getCollisionSide(ground, player) --activeWall tracks the wall currently in contact with the player --If not already touching a wall test for a wall if activeWall == nil then playerOnWall = hitSide == 3 or hitSide == 4 --If the player is on the wall set the active wall if playerOnWall then activeWall = ground wallOnLeft = hitSide == 3 end end --activeGround tracks the ground currently in contact with the player --If not already touching the ground test for the ground if activeGround == nil then playerOnGround = hitSide == 2 --If the player is on the ground then set the active ground and unset wall if playerOnGround then activeGround = ground activeWall = nil playerOnWall = false end end end
The offGround function is more simple. If a collision ended and the wall is active then deactivate the wall. the same goes for the ground.
function offGround(ground) if ground == activeWall then activeWall = nil playerOnWall = false end if ground == activeGround then activeGround = nil playerOnGround = false end end
Here is the method that checks which side of the ground object the player collided with. This is just a simple rectangle rectangle collision detection. The varaible names should make it self explanatory.
function getCollisionSide(ground, player) local groundX, groundY = ground.x - (ground.width / 2), ground.y - (ground.height / 2) local playerX, playerY = player.x - (player.width / 2), player.y - (player.height / 2) local groundLeft = groundX local playerLeft = playerX local groundRight = groundX+ground.width local playerRight = playerX+player.width local groundTop = groundY local playerTop = playerY local groundBottom = groundY+ground.height local playerBottom = playerY+player.height if groundBottom < playerTop then return 1 end if groundTop > playerBottom then return 2 end if groundRight < playerLeft then return 3 end if groundLeft > playerRight then return 4 end end
That about wraps it up. As always please feel free to ask questions.
I am very impressed with how easy it was to build this simple platformer. I really like Lime, which was created by @MonkeyDead. There is definitely room for improvement, but it has a solid foundation to work off of. The developer of Lime has been very responsive and helpful. With the Lime Corona combo I feel like I can focus more on making games instead of writing code.