In this tutorial, I will show you how to create a collect-the-pieces game using the Corona SDK and the Lua programming language. We will explore how to use touch controls and events, create shapes using the graphics API, and we will also make use of physics collisions and timers. Let's get started.
1. Application Overview
In this game, the player will be able to control a puck on the screen to collect other pieces with the same color. The player has only a limited time to collect as many pieces as possible. If the player touches a piece from another color, the game is over.
In this project you will learn the following skills:
- physics collisions
- create text fields
- user timers and images
- how to use touch controls and events
- create shapes using the new Graphics API
2. Target Device
The first thing we have to do is select the platform we want to run our application on. This enables us to choose the correct sizes for the artwork that we'll use.
The iOS platform has the following requirements:
- iPad 1/2/Mini: 1024px x 768px, 132 PPI
- iPad Retina: 2048px x 1536px, 264 PPI
- iPhone/iPod Touch: 320px x 480px, 163 PPI
- iPhone/iPod Retina: 960px x 640px, 326 PPI
- iPhone 5/iPod Touch: 1136px x 640px, 326 PPI
Since Android is a more open platform, there are many devices and possible resolutions. A few of the more common ones are listed below:
- Asus Nexus 7 Tablet: 800px x 1280px, 216 PPI
- Motorola Droid X: 854px x 480px, 228 PPI
- Samsung Galaxy SIII: 720px x 1280px, 306 PPI
In this tutorial, we'll be focusing on the iOS platform and the iPhone and iPod Touch in particular. However, the code used in this tutorial can also be used for the Android platform.
3. Interface
We'll use a simple and friendly interface with multiple shapes, buttons, bitmaps and more. The artwork for the interface can be found in the source files of this tutorial.
4. Export Graphics
Depending on the device you've selected, you may need to convert the graphics to the recommended resolution (PPI), which you can do in your favorite image editor. I used the Adjust Size… option in the Tools menu of the Preview application on OS X. Remember to give the images a descriptive name and save them in your project folder.
5. Application Configuration
We'll use a configuration file, config.lua
, to make the application go full screen across devices. The configuration file shows the original screen size and the method used to scale the content in case the application is run on a different resolution.
application = { content = { width = 320, height = 480, scale = 'letterbox' }, }
6. main.lua
Let's write the actual application. Open your preferred Lua editor. Any plain text editor will work, but it is recommended to use a text editor that has syntax highlighting. Create a new file and save it as main.lua
in your project folder.
7. Code Structure
We'll structure our code as if it were a class. If you're familiar with ActionScript or Java, you should find the project structure familiar.
Necessary Classes Variables and Constants Declare Functions constructor (Main function) class methods (other functions) call Main function
8. Hide Status Bar
display.setStatusBar(display.HiddenStatusBar)
This code snippet hides the status bar. The status bar is the bar at the top of the device's screen that shows the time, signal, and other indicators.
9. Default Anchors
Setting the display's default anchors is useful if you're porting an application from the previous Graphics library, that is, the projects you've created with previous version of the Corona SDK. Since the release of the Graphics 2.0 library, the reference point of every image has changed from its top-left to its center. To change this in every image that you use in your project, we can invoke the setDefault
method and pass a value from 0.0
to 1.0
, with 0.0
being the left if you change the x
anchor and the top if you change the y
anchor.
display.setDefault('anchorX', 0) display.setDefault('anchorY', 0)
10. Import Physics
We'll be using the physics library to handle collisions. Import and start the library using the code snippet shown below.
-- Physics local physics = require('physics') physics.start()
11. Background
A simple background for the application's user interface. The code snippet below draws the background to the screen.
-- Graphics -- [Background] local gameBg = display.newImage('gameBg.png')
12. Title View
This is the title view. It's the first interactive screen to appear in our game. These variables store its components.
-- [Title View] local title local playBtn local creditsBtn local titleView
13. Credits View
The credits view shows the credits and copyright of the application. The creditsView
variable is used to store it.
-- [CreditsView] local creditsView
14. Score Text Field
We'll create a text field for showing the player's score a bit later in this tutorial. We store a reference to this text field in the scoreTF
variable.
-- Score TextField local scoreTF
15. Pucks
The pucks in the game are created and distributed randomly on the stage. The pucks
group will be used to group them so that we can manipulate them easily.
-- Pucks local pucks
16. Alert
The alertView
variable keeps a reference to the alert view that is displayed when a puck of the wrong color is touched by the player. The alert view will show the player the game is over.
-- Alert local alertView
17. Sounds
We'll use sound effects to give the game extra character. The sounds that I've used in this project were obtained from as3sfxr. You can find the background music on Play On Loop.
-- Sounds local bgMusic = audio.loadStream('POL-sky-sanctuary-short.mp3') local blip = audio.loadSound('blip.wav') local wrong = audio.loadSound('lose.wav')
18. Variables
The next code snippet lists the variables that we'll use in the game. The totalPucks
variable stores the number of pucks that are placed on the stage, timerSrc
keeps a reference to the game's timer, and time
references the rectangle that shows the remaining time.
-- Variables local totalPucks = 20 local timerSrc local time
19. Declare Functions
We declare the functions as local
at the very beginning. In Lua, you can forward declare a function by declaring its name before implementing the function's body. This makes it easier to keep track of the various functions that we'll use in this project.
-- Functions local Main = {} local startButtonListeners = {} local showCredits = {} local hideCredits = {} local showGameView = {} local gameListeners = {} local createPuck = {} local movePuck = {} local reduceTime = {} local alert = {}
20. Constructor
Let's start by creating a stub implementation for the function that will initialize the game logic, the Main
function.
function Main() end
21. Add Title View
Next, we draw the title view to the screen and add a tap listener to each button. The newImage
method is used to load the images and display them on the screen using the positions passed to the function. We also create a group named titleView
that serves as a container for the newly created elements.
function Main() titleBg = display.newImage('titleBg.png') playBtn = display.newImage('playBtn.png', 220, 168) creditsBtn = display.newImage('creditsBtn.png', 204, 230) titleView = display.newGroup(titleBg, playBtn, creditsBtn) startButtonListeners('add') end
22. Start Button Listeners
In startButtonListeners
, we add the event listeners to the title view's buttons. When the play button is tapped, we show and start the game. When the credits button is tapped, we show the game's credits.
function startButtonListeners(action) if(action == 'add') then playBtn:addEventListener('tap', showGameView) creditsBtn:addEventListener('tap', showCredits) else playBtn:removeEventListener('tap', showGameView) creditsBtn:removeEventListener('tap', showCredits) end end
23. Show Credits
In showCredits
, we hide the buttons, display the credits, and add a tap listener to hide the credits when the player taps the credits.
function showCredits:tap(e) playBtn.isVisible = false creditsBtn.isVisible = false creditsView = display.newImage('credits.png', -110, display.contentHeight-80) transition.to(creditsView, {time = 300, x = 0, onComplete = function() creditsView:addEventListener('tap', hideCredits) end}) end
24. Hide Credits
When the player taps the credits, the view is tweened from the stage and removed.
function hideCredits:tap(e) playBtn.isVisible = true creditsBtn.isVisible = true transition.to(creditsView, {time = 300, y = display.contentHeight+creditsView.height, onComplete = function() creditsView:removeEventListener('tap', hideCredits) display.remove(creditsView) creditsView = nil end}) end
25. Show Game View
When the player taps the play button to start a new game, the title view is tweened from the stage and hidden. This shows the game view. The game view is the heart of the game. Let's break the rest of the showGameView
step by step.
function showGameView:tap(e) transition.to(titleView, {time = 300, x = -titleView.height, onComplete = function() startButtonListeners('rmv') display.remove(titleView) titleView = nil end}) end
26. Score Text Field
We start by creating the score text field as shown below.
function showGameView:tap(e) transition.to(titleView, {time = 300, x = -titleView.height, onComplete = function() startButtonListeners('rmv') display.remove(titleView) titleView = nil end}) -- TextFields scoreTF = display.newText('0', 25, 18, 'Courier', 15) scoreTF:setFillColor(15, 223, 245) end
The newText
method accept a number of arguments.
- The initial text of the text field.
- The text field's
x
coordinate. - The text field's
y
coordinate. - The text field's font and font size.
On iOS, you have access to a wide range of fonts. On Android there are only three fonts available; Droid Sans, Droid Serif, and Droid Sans Mono.
27. Timer
In the next step, we create the timer's rectangle shape using Corona built-in vector graphics library. The newRect
function creates a rectangle that is 20pt wide, 6pt tall, and it places it in the top-right of the stage. The default color of new shapes is white so we need to change the rectangle's color by invoking setFillColor
and passing in an RGB value. The Graphics 2.0 library doesn't use values ranging from 0
to 255
for RGB values. Instead an RGB value is expected to range from 0.0
to 1.0
.
function showGameView:tap(e) transition.to(titleView, {time = 300, x = -titleView.height, onComplete = function() startButtonListeners('rmv') display.remove(titleView) titleView = nil end}) -- TextFields scoreTF = display.newText('0', 25, 18, 'Courier', 15) scoreTF:setFillColor(15, 223, 245) -- Timer time = display.newRect(450, 20, 20, 6) time:setFillColor(0.05, 0.87, 0.96) end
A quick and easy approach for converting old RGB value to new values is by dividing the old value by 255
. For example, 123
becomes 123/255
, which translates to 0.48
.
28. Pucks
The pucks
group stores all the pucks so that we can manipulate them all at once.
function showGameView:tap(e) transition.to(titleView, {time = 300, x = -titleView.height, onComplete = function() startButtonListeners('rmv') display.remove(titleView) titleView = nil end}) -- TextFields scoreTF = display.newText('0', 25, 18, 'Courier', 15) scoreTF:setFillColor(15, 223, 245) -- Timer time = display.newRect(450, 20, 20, 6) time:setFillColor(0.05, 0.87, 0.96) -- Pucks pucks = display.newGroup() end
29. Start Game
To finish the showGameViewMethod
, we install the game listeners and start the background music. By setting the loops
parameter to -1
, the background music will loop until we tell it to stop.
function showGameView:tap(e) transition.to(titleView, {time = 300, x = -titleView.height, onComplete = function() startButtonListeners('rmv') display.remove(titleView) titleView = nil end}) -- TextFields scoreTF = display.newText('0', 25, 18, 'Courier', 15) scoreTF:setFillColor(15, 223, 245) -- Timer time = display.newRect(450, 20, 20, 6) time:setFillColor(0.05, 0.87, 0.96) -- Pucks pucks = display.newGroup() gameListeners('add') audio.play(bgMusic, {loops = -1, channel = 1}) end
30. Game Listeners
In showGameView
, we call the gameListeners
function in which the pucks are created by invoking the createPucks
function. We also create a rectangle for displaying the game's time.
function gameListeners(action) if(action == 'add') then createPucks() timerSrc = timer.performWithDelay(1000, reduceTime, 20) else for i = 1, pucks.numChildren do pucks[i]:removeEventListener('touch', movePuck) end timer.cancel(timerSrc) timerSrc = nil end end
31. Create Pucks
In createPucks
, we use a for
loop to instantiate the pucks. The number of pucks is stored in totalPucks
, which we set to 20
a bit earlier in this tutorial.
A random position is calculated for each puck using math.random
. We also use math.random
to help us load a different color from the project's images. How does this work? We generate a random integer between 1
and 4
, and add the result to the name of the image that we want to load. For example, if the random number is equal to 3
, the game loads an image named puck3.png.
function createPucks() for i = 1, totalPucks do local p local rnd = math.floor(math.random() * 4) + 1 p = display.newImage('puck' .. tostring(rnd) .. '.png', math.floor(math.random() * display.contentWidth), math.floor(math.random() * display.contentHeight)) p.name = 'p' .. tostring(rnd) p:addEventListener('touch', movePuck) -- Physics physics.addBody(p, 'dynamic', {radius = 12}) p.isSensor = true pucks:insert(p) end end
32. Move Function
We use touch events to let the player move the pucks. When the player touches a puck, it is aligned with the position of the touch, the player's finger, and is then moved by updating its position. We also add a collision
listener to the active puck to detect when the puck collides with another puck. This event listener is removed when the player releases the puck by lifting their finger from the screen.
function movePuck(e) if(e.phase == 'began') then -- Collision e.target.x = e.x - e.target.width/2 e.target.y = e.y - e.target.height/2 e.target:addEventListener('collision', onCollision) end if(e.phase == 'moved') then e.target.x = e.x - e.target.width/2 e.target.y = e.y- e.target.height/2 end if(e.phase == 'ended') then e.target:addEventListener('collision', onCollision) end end
33. Timer
The reduceTimer
function is in charge of the timer rectangle we created earlier. Every second, the width of the shape is reduced by setting its xScale
property and it is removed from the stage when it's no longer visible. An alert view is shown to the player when the time's up.
function reduceTime(e) time.xScale = time.xScale - 0.05 time.x = 450 if(time.xScale <= 0.2) then display.remove(time) alert() end end
34. Handling Collisions
The onCollision
function is in charge of handling collisions between pucks.
function onCollision(e) if(e.other ~= nil) then if(e.other.name == e.target.name) then audio.play(blip) -- Local Score local score = display.newText('50', e.other.x, e.other.y, 'Courier New Bold', 14) transition.to(score, {time = 500, xScale = 1.5, yScale = 1.5, y = score.y - 20, onComplete = function() display.remove(score) score = nil end }) -- Remove display.remove(e.other) e.other = nil -- Total Score scoreTF.text = tostring(tonumber(scoreTF.text) + 50) scoreTF.x = 15 else audio.play(wrong) alert('lose') end end end
This is what happens when a collision occurs:
- We first check whether the names of the pucks are the same to see if their color matches.
- If they have the same color, we play a sound effect.
- The player's score is increased by
50
points and the score text field is updated. - If the color of the pucks don't match, a different sound effect is played and the game over alert view is shown to the player.
35. Alert
The alert
function is a simple helper function to show an alert view and animate it. We stop the audio after 700 milliseconds to make sure that we can play a sound effect. The game listeners are removed to end the game and we show an appropriate image to the player.
function alert(action) timer.performWithDelay(700, function() audio.stop(1) audio.dispose(bgMusic) bgMusic = nil end, 1) gameListeners('rmv') if(action == 'lose') then alertView = display.newImage('gameOver.png', 155, 125) else alertView = display.newImage('timeUp.png', 155, 125) local score = display.newText(scoreTF.text, 225, 160, 'Courier New Bold', 20) score:setFillColor(255, 255, 255) end transition.from(alertView, {time = 300, xScale = 0.5, yScale = 0.5}) -- Wait 100 ms to stop physics timer.performWithDelay(10, function() physics.stop() end, 1) end
36. Call Main
To start the game, we invoke the Main
function as shown below.
Main()
37. Loading Screen
On the iOS platform, the file named Default.png is displayed while the application is launching. Add this image to your project's source folder, and it will be automatically added by the Corona compiler.
38. Icon
Using the graphics you created earlier, you can now create a nice icon. The dimensions of the icon size for a non-retina iPhone are 57px x 57px, while the retina version needs to be 114px x 114px. The artwork for iTunes is required to be 1024px x 1024px. I suggest creating the iTunes artwork first and then creating the smaller sized images by scaling the iTunes artwork down to the correct dimensions. There is no need to make the application icon glossy or add rounded corners as this is taken care of by the operating system.
39. Testing in the Simulator
It's time to test our application in the simulator. Open the Corona Simulator, browse to your project folder, and click Open. If everything works as expected, you're ready for the final step.
40. Build
In the Corona Simulator, go to File > Build and select the target device. Fill out the required fields and click Build. Wait a few seconds and your application is ready to test on a device and/or to be submitted for distribution.
Conclusion
In this tutorial we've learned about touch listeners, collision detection, and physics, as well as a few other skills that can be useful in a wide number of games. Experiment with the final result and try to modify the game to create your own version. I hope you liked this tutorial and found it helpful. Thank you for reading.