Difference between revisions of "Article Tutorials/Square Dodge"
(3 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
+ | __TOC__ | ||
== Displaying Images == | == Displaying Images == | ||
+ | It’s time to put all of your knowledge together and actually make something worthwhile. We’ll start off with a few functions and build it up to a fully-fledged, fun game! | ||
− | + | Create a new project. I called mine “Square Dodge”. | |
− | + | Download the code and all resources for this post from '''[https://github.com/plicatibu/gideros_external_tutorials/tree/master/bluebilby.com_2013-05-08_gideros-mobile-tutorial-creating-your-first-game here]''' | |
− | + | Square Dodge source code. (3.8 Mb) | |
− | + | Don’t forget to import your audio and image assets! | |
− | + | [[File:Jason-Oakley-Creating-Your-First-Game-Square-Dodge-project.png|thumb|center]] | |
− | |||
− | |||
− | [[File:Jason-Oakley-Creating-Your-First-Game-Square-Dodge-project.png|thumb|center]] | ||
− | |||
− | |||
− | |||
+ | The code below is heavily commented to help you understand how each part works. | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
-- Square Dodge by Jason Oakley (C) 2013 Blue Bilby | -- Square Dodge by Jason Oakley (C) 2013 Blue Bilby | ||
Line 84: | Line 81: | ||
-- Player image touched function | -- Player image touched function | ||
local function imagetouch(playerImage, event) | local function imagetouch(playerImage, event) | ||
− | if not isGameRunning then | + | if not isGameRunning then return end |
− | |||
− | |||
-- Is the player touching the player sprite? | -- Is the player touching the player sprite? | ||
if playerImage:hitTestPoint(event.touch.x, event.touch.y) then | if playerImage:hitTestPoint(event.touch.x, event.touch.y) then | ||
Line 92: | Line 87: | ||
player.y = event.touch.y - (player.height / 2) | player.y = event.touch.y - (player.height / 2) | ||
-- Make sure they don't drag it offscreen | -- Make sure they don't drag it offscreen | ||
− | if player.x > 286 then | + | if player.x > 286 then player.x = 286 end |
− | + | if player.x < 0 then player.x = 0 end | |
− | + | if player.y < 0 then player.y = 0 end | |
− | if player.x < 0 then | + | if player.y > 446 then player.y = 446 end |
− | |||
− | |||
− | if player.y < 0 then | ||
− | |||
− | |||
− | if player.y > 446 then | ||
− | |||
− | |||
player:setPosition(player.x, player.y) | player:setPosition(player.x, player.y) | ||
end | end | ||
Line 152: | Line 139: | ||
-- Pick a direction for the enemies to move | -- Pick a direction for the enemies to move | ||
xdir, ydir = rnd(2) - 1, rnd(2) - 1 | xdir, ydir = rnd(2) - 1, rnd(2) - 1 | ||
− | if xdir == 0 then | + | if xdir == 0 then xdir = -1 end |
− | + | if ydir == 0 then ydir = -1 end | |
− | |||
− | if ydir == 0 then | ||
− | |||
− | |||
enemyShape[i].xdir, enemyShape[i].ydir = xdir, ydir | enemyShape[i].xdir, enemyShape[i].ydir = xdir, ydir | ||
end | end | ||
Line 169: | Line 152: | ||
xRand = xRand + 100 | xRand = xRand + 100 | ||
yRand = yRand + 100 | yRand = yRand + 100 | ||
− | if xRand > 270 then | + | if xRand > 270 then xRand = xRand - 200 end |
− | + | if yRand > 420 then yRand = yRand - 200 end | |
− | |||
− | if yRand > 420 then | ||
− | |||
− | |||
end | end | ||
numberOfEnemies = numberOfEnemies + 1 | numberOfEnemies = numberOfEnemies + 1 | ||
Line 215: | Line 194: | ||
rectB.width, | rectB.width, | ||
rectB.height | rectB.height | ||
− | if | + | if (y2 >= y1 and y1 + h1 >= y2) or (y2 + h2 >= y1 and y1 + h1 >= y2 + h2) or (y1 >= y2 and y2 + h2 >= y1) or |
− | + | (y1 + h1 >= y2 and y2 + h2 >= y1 + h1) then | |
− | (y1 + h1 >= y2 and y2 + h2 >= y1 + h1) | + | if x2 >= x1 and x1 + w1 >= x2 then collided = true end |
− | + | if x2 + w2 >= x1 and x1 + w1 >= x2 + w2 then collided = true end | |
− | if x2 >= x1 and x1 + w1 >= x2 then | + | if x1 >= x2 and x2 + w2 >= x1 then collided = true end |
− | + | if x1 + w1 >= x2 and x2 + w2 >= x1 + w1 then collided = true end | |
− | |||
− | if x2 + w2 >= x1 and x1 + w1 >= x2 + w2 then | ||
− | |||
− | |||
− | if x1 >= x2 and x2 + w2 >= x1 then | ||
− | |||
− | |||
− | if x1 + w1 >= x2 and x2 + w2 >= x1 + w1 then | ||
− | |||
− | |||
end | end | ||
return collided | return collided | ||
Line 313: | Line 282: | ||
local function updateAll() | local function updateAll() | ||
-- Only update if the game is still going | -- Only update if the game is still going | ||
− | if not isGameRunning then | + | if not isGameRunning then return end |
− | |||
− | |||
scoresUpdate() | scoresUpdate() | ||
enemiesUpdate() | enemiesUpdate() | ||
Line 325: | Line 292: | ||
-- This executes "updateAll" each frame (constantly) | -- This executes "updateAll" each frame (constantly) | ||
stage:addEventListener(Event.ENTER_FRAME, updateAll) | stage:addEventListener(Event.ENTER_FRAME, updateAll) | ||
− | </syntaxhighlight | + | </syntaxhighlight> |
+ | Wow! That’s a lot of code! Here’s a screenshot of the game in action: | ||
− | + | [[File:Jason-Oakley-Creating-Your-First-Game-Square-Dodge-screenshot.png|thumb|center]] | |
− | |||
− | + | Let’s analyse our code. | |
− | |||
− | Let’s analyse our code. | ||
− | |||
− | |||
+ | In the below code, we are assigning our variables as ‘local’ so they don’t use too much RAM by being global. This is a good practise for all of your apps, so start doing it now. ‘enemyShape’ is a table to hold all the enemy objects. | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
-- Define our variables and tables as local to save memory | -- Define our variables and tables as local to save memory | ||
Line 352: | Line 316: | ||
local MAXCOUNTDOWN = 500 | local MAXCOUNTDOWN = 500 | ||
local gameoverImg, startbuttonImg | local gameoverImg, startbuttonImg | ||
− | </syntaxhighlight | + | </syntaxhighlight> |
− | |||
− | |||
+ | Assigning ‘math.random’ to ‘rnd’ will cache the function and make it faster to access. This is a standard in Lua, particularly for math functions. | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
-- Cache math.random to make access faster | -- Cache math.random to make access faster | ||
local rnd = math.random | local rnd = math.random | ||
− | </syntaxhighlight | + | </syntaxhighlight> |
− | This is our code for loading and saving the hiScore variable between games. This way, whenever the player plays our game, they will see their highest score to beat. | + | This is our code for loading and saving the hiScore variable between games. This way, whenever the player plays our game, they will see their highest score to beat. |
− | |||
+ | For loading, we first try to open the file. If it fails because it is the first time, we set the hiScore to ‘0’. | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
-- Load high score if it exists | -- Load high score if it exists | ||
Line 375: | Line 338: | ||
end | end | ||
end | end | ||
− | </syntaxhighlight | + | </syntaxhighlight> |
− | |||
− | |||
+ | Save the hiScore so we can later retrieve it. | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
-- Save high score | -- Save high score | ||
Line 386: | Line 348: | ||
file:close() | file:close() | ||
end | end | ||
− | </syntaxhighlight | + | </syntaxhighlight> |
− | |||
− | |||
+ | Initialise the score variable and set up the text at the top of the screen to display the score and hiScore. | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
-- Initialise scores and text | -- Initialise scores and text | ||
Line 401: | Line 362: | ||
stage:addChild(hiScoreText) | stage:addChild(hiScoreText) | ||
end | end | ||
− | </syntaxhighlight | + | </syntaxhighlight> |
− | |||
− | |||
− | |||
+ | Update the score. If ‘score’ is bigger than ‘hiScore’, make them the same. | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
-- Update all scores | -- Update all scores | ||
Line 416: | Line 375: | ||
end | end | ||
end | end | ||
− | </syntaxhighlight | + | </syntaxhighlight> |
− | |||
− | |||
− | |||
+ | Load our tune and set it to repeat forever. Load the sound effect for Game Over and play that effect. | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
-- Play tune | -- Play tune | ||
Line 433: | Line 390: | ||
explodefx:play() | explodefx:play() | ||
end | end | ||
− | </syntaxhighlight | + | </syntaxhighlight> |
− | |||
− | |||
− | |||
+ | We need to set up a touch function so that when the player touches the player image, it will be draggable around the screen. We also ensure the image stays within the boundaries of the screen by limiting the ‘x’ and ‘y’ values. | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
-- Player image touched function | -- Player image touched function | ||
Line 449: | Line 404: | ||
player.x = event.touch.x-(player.width/2) | player.x = event.touch.x-(player.width/2) | ||
player.y = event.touch.y-(player.height/2) | player.y = event.touch.y-(player.height/2) | ||
− | |||
-- Make sure they don't drag it offscreen | -- Make sure they don't drag it offscreen | ||
− | if player.x > 286 then | + | if player.x > 286 then player.x = 286 end |
− | + | if player.x < 0 then player.x = 0 end | |
− | + | if player.y < 0 then player.y = 0 end | |
− | + | if player.y > 446 then player.y = 446 end | |
− | if player.x < 0 then | ||
− | |||
− | |||
− | |||
− | if player.y < 0 then | ||
− | |||
− | |||
− | |||
− | if player.y > 446 then | ||
− | |||
− | |||
− | |||
player:setPosition(player.x, player.y) | player:setPosition(player.x, player.y) | ||
end | end | ||
end | end | ||
− | </syntaxhighlight | + | </syntaxhighlight> |
− | |||
− | |||
+ | Load the player image and set its initial ‘x’ and ‘y’ co-ordinates. Configure the Event Listener for when the player touches the image. | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
-- Set up the player object | -- Set up the player object | ||
Line 487: | Line 428: | ||
player:addEventListener(Event.TOUCHES_MOVE, imagetouch, player) | player:addEventListener(Event.TOUCHES_MOVE, imagetouch, player) | ||
end | end | ||
− | </syntaxhighlight | + | </syntaxhighlight> |
− | This one is quite long. We are creating all the enemies required, but we only add the first 6 to the active display. The rest are kept outside the display area until the timer adds them in. This way, the game slowly gets more difficult as more enemies appear. | + | This one is quite long. We are creating all the enemies required, but we only add the first 6 to the active display. The rest are kept outside the display area until the timer adds them in. This way, the game slowly gets more difficult as more enemies appear. |
− | |||
+ | We also make sure the initial enemies do not appear too close to the player’s initial position in the middle of the screen, otherwise it’ll be Game Over too quickly. | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
-- Set up the enemies | -- Set up the enemies | ||
Line 530: | Line 471: | ||
-- Pick a direction for the enemies to move | -- Pick a direction for the enemies to move | ||
local xdir, ydir = rnd(2)-1, rnd(2)-1 | local xdir, ydir = rnd(2)-1, rnd(2)-1 | ||
− | if xdir == 0 then | + | if xdir == 0 then xdir = -1 end |
− | + | if ydir == 0 then ydir = -1 end | |
− | |||
− | |||
− | if ydir == 0 then | ||
− | |||
− | |||
− | |||
enemyShape[i].xdir, enemyShape[i].ydir = xdir, ydir | enemyShape[i].xdir, enemyShape[i].ydir = xdir, ydir | ||
end | end | ||
− | + | enemyCountdown=MAXCOUNTDOWN | |
end | end | ||
− | </syntaxhighlight | + | </syntaxhighlight> |
− | |||
− | |||
− | |||
+ | Every now and again, we bring one of the enemies hiding off-screen onto the active screen and set them bouncing around. We also make sure these don’t spawn too close to the player’s current position. | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
-- Spawn a new enemy | -- Spawn a new enemy | ||
Line 555: | Line 488: | ||
xRand = xRand + 100 | xRand = xRand + 100 | ||
yRand = yRand + 100 | yRand = yRand + 100 | ||
− | if xRand > 270 then | + | if xRand > 270 then xRand = xRand - 200 end |
− | + | if yRand > 420 then yRand = yRand - 200 end | |
− | + | end | |
− | if yRand > 420 then | ||
− | |||
− | |||
− | end | ||
numberOfEnemies = numberOfEnemies + 1 | numberOfEnemies = numberOfEnemies + 1 | ||
enemyShape[numberOfEnemies].x, enemyShape[numberOfEnemies].y = xRand, yRand | enemyShape[numberOfEnemies].x, enemyShape[numberOfEnemies].y = xRand, yRand | ||
Line 567: | Line 496: | ||
enemyCountdown=MAXCOUNTDOWN | enemyCountdown=MAXCOUNTDOWN | ||
end | end | ||
− | </syntaxhighlight | + | </syntaxhighlight> |
− | |||
− | |||
− | |||
+ | This function will move the enemies on the active screen and bounce them around if they hit the borders of the screen. It decrements the counter and if it’s time, spawns new enemies. | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
-- Update the enemies | -- Update the enemies | ||
Line 600: | Line 527: | ||
end | end | ||
end | end | ||
− | </syntaxhighlight | + | </syntaxhighlight> |
− | |||
− | |||
− | |||
− | |||
+ | This is a simple collision test function. You can find example code for this on the internet with a quick search. Basically, it checks the co-ordinates of the player object compared to the enemy object specified. If they overlap, it returns ‘true’. | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
-- Simple collision test | -- Simple collision test | ||
Line 612: | Line 536: | ||
local x1,y1,w1,h1,x2,y2,w2,h2 = rectA.x, rectA.y, rectA.width, rectA.height, rectB.x, rectB.y, rectB.width, rectB.height | local x1,y1,w1,h1,x2,y2,w2,h2 = rectA.x, rectA.y, rectA.width, rectA.height, rectB.x, rectB.y, rectB.width, rectB.height | ||
if (y2 >= y1 and y1 + h1 >= y2) or (y2 + h2 >= y1 and y1 + h1 >= y2 + h2) or (y1 >= y2 and y2 + h2 >= y1) or (y1 + h1 >= y2 and y2 + h2 >= y1 + h1) then | if (y2 >= y1 and y1 + h1 >= y2) or (y2 + h2 >= y1 and y1 + h1 >= y2 + h2) or (y1 >= y2 and y2 + h2 >= y1) or (y1 + h1 >= y2 and y2 + h2 >= y1 + h1) then | ||
− | if x2 >= x1 and x1 + w1 >= x2 then | + | if x2 >= x1 and x1 + w1 >= x2 then collided = true end |
− | + | if x2 + w2 >= x1 and x1 + w1 >= x2 + w2 then collided = true end | |
− | + | if x1 >= x2 and x2 + w2 >= x1 then collided = true end | |
− | + | if x1 + w1 >= x2 and x2 + w2 >= x1 + w1 then collided = true end | |
− | if x2 + w2 >= x1 and x1 + w1 >= x2 + w2 then | ||
− | |||
− | |||
− | |||
− | if x1 >= x2 and x2 + w2 >= x1 then | ||
− | |||
− | |||
− | |||
− | if x1 + w1 >= x2 and x2 + w2 >= x1 + w1 then | ||
− | |||
− | |||
end | end | ||
return collided | return collided | ||
end | end | ||
− | </syntaxhighlight | + | </syntaxhighlight> |
− | |||
− | |||
+ | Set up the enemies and player object, load the score and set up the text. | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
-- Initialise the game | -- Initialise the game | ||
Line 643: | Line 555: | ||
initScores() | initScores() | ||
end | end | ||
− | </syntaxhighlight | + | </syntaxhighlight> |
− | |||
− | |||
+ | Set up the logo and game Start button to be touchable. | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
-- Start button touch handler | -- Start button touch handler | ||
Line 659: | Line 570: | ||
end | end | ||
end | end | ||
− | </syntaxhighlight | + | </syntaxhighlight> |
− | |||
− | |||
+ | Create a start button object and add it to the screen. | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
-- Start game. Display START button and logo | -- Start game. Display START button and logo | ||
Line 674: | Line 584: | ||
startbuttonImg:addEventListener(Event.TOUCHES_BEGIN, startTouch, startbuttonImg) | startbuttonImg:addEventListener(Event.TOUCHES_BEGIN, startTouch, startbuttonImg) | ||
end | end | ||
− | </syntaxhighlight | + | </syntaxhighlight> |
− | |||
− | |||
+ | Create a Game Over object and make it touchable. Remove the player and enemy objects and text at the top of the screen. | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
local function goTouch(gameOverImage, event) | local function goTouch(gameOverImage, event) | ||
Line 702: | Line 611: | ||
end | end | ||
end | end | ||
− | </syntaxhighlight | + | </syntaxhighlight> |
− | |||
− | |||
+ | When the player collides with an enemy object, this code is executed. Save the current hiScore, play the ‘game over’ sound effect and display the Game Over image. | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
-- Game over handling | -- Game over handling | ||
Line 727: | Line 635: | ||
gameoverImg:addEventListener(Event.TOUCHES_BEGIN, goTouch, gameoverImg) | gameoverImg:addEventListener(Event.TOUCHES_BEGIN, goTouch, gameoverImg) | ||
end | end | ||
− | </syntaxhighlight | + | </syntaxhighlight> |
− | |||
− | |||
+ | Supply the player and enemy objects to test if any collided. If they did, go to the Game Over function. | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
-- See if any collisions occurred | -- See if any collisions occurred | ||
Line 743: | Line 650: | ||
end | end | ||
end | end | ||
− | </syntaxhighlight | + | </syntaxhighlight> |
− | |||
− | |||
+ | This is the code to run every frame of the game. This means it constantly is run by the application. Sometimes, it’s good to check the game is actually running so they code does not accidentally get called elsewhere. It’s a fail-safe. | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
-- Update everything | -- Update everything | ||
local function updateAll() | local function updateAll() | ||
-- Only update if the game is still going | -- Only update if the game is still going | ||
− | if not isGameRunning then | + | if not isGameRunning then return end |
− | |||
− | |||
scoresUpdate() | scoresUpdate() | ||
Line 759: | Line 663: | ||
checkCollisions() | checkCollisions() | ||
end | end | ||
− | </syntaxhighlight | + | </syntaxhighlight> |
− | |||
− | |||
+ | Start the tune playing and start the game off. | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
-- Start it all up! | -- Start it all up! | ||
playTune() | playTune() | ||
startGame() | startGame() | ||
− | </syntaxhighlight | + | </syntaxhighlight> |
− | |||
− | |||
+ | This tells the application to call the ‘updateAll’ function constantly to keep things running. | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
-- This executes "updateAll" each frame (constantly) | -- This executes "updateAll" each frame (constantly) | ||
stage:addEventListener(Event.ENTER_FRAME, updateAll) | stage:addEventListener(Event.ENTER_FRAME, updateAll) | ||
− | </syntaxhighlight> | + | </syntaxhighlight> |
+ | |||
+ | And that’s how you can make a simple game. You’ve learned enough to get out there and get something started. | ||
+ | |||
+ | Next, we’ll go into more complex programming and explore some third-party applications to help make your game developing easier. | ||
+ | |||
− | + | '''Note: This tutorial was written by [http://bluebilby.com/author/waulokadmin/ Jason Oakley] and was originally available here: http://bluebilby.com/2013/05/08/gideros-mobile-tutorial-creating-your-first-game/''' | |
− | |||
− | ''' | + | '''[[Written Tutorials]]''' |
+ | {{GIDEROS IMPORTANT LINKS}} |
Latest revision as of 10:54, 26 August 2024
Displaying Images
It’s time to put all of your knowledge together and actually make something worthwhile. We’ll start off with a few functions and build it up to a fully-fledged, fun game!
Create a new project. I called mine “Square Dodge”.
Download the code and all resources for this post from here
Square Dodge source code. (3.8 Mb)
Don’t forget to import your audio and image assets!
The code below is heavily commented to help you understand how each part works.
-- Square Dodge by Jason Oakley (C) 2013 Blue Bilby
-- Define our variables and tables as local to save memory
local score
local hiScore
local enemyShape = {}
local player
local isGameRunning
local scoreText
local hiScoreText
local MINNUMBEROFENEMIES = 7
local MAXNUMBEROFENEMIES = 101
local numberOfEnemies = MINNUMBEROFENEMIES
local enemyCountdown
local MAXCOUNTDOWN = 500
local gameoverImg
-- Cache math.random to make access faster
local rnd = math.random
-- Functions begin here
-- Load high score if it exists
local function loadScore()
local file = io.open("|D|settings.txt", "r")
if not file then
hiScore = 0
else
hiScore = tonumber(file:read("*line"))
file:close()
end
print('')
end
-- Save high score
local function saveScore()
local file = io.open("|D|settings.txt", "w+")
file:write(hiScore .. "\n")
file:close()
end
-- Initialise scores and text
local function initScores()
score = 0
scoreText = TextField.new(nil, "Score: " .. score)
scoreText:setPosition(10, 10)
stage:addChild(scoreText)
hiScoreText = TextField.new(nil, "Hi Score: " .. hiScore)
hiScoreText:setPosition(200, 10)
stage:addChild(hiScoreText)
end
-- Update all scores
local function scoresUpdate()
score = score + 1
scoreText:setText("Score: " .. score)
if score > hiScore then
hiScore = score
hiScoreText:setText("Hi Score: " .. hiScore)
end
end
-- Play tune
local function playTune()
local gametune = Sound.new("audio/DST-909Dreams.mp3")
gametune:play(0, true)
end
-- Play sound effects
local function playEffect()
local explodefx = Sound.new("audio/gameover.wav")
explodefx:play()
end
-- Player image touched function
local function imagetouch(playerImage, event)
if not isGameRunning then return end
-- Is the player touching the player sprite?
if playerImage:hitTestPoint(event.touch.x, event.touch.y) then
player.x = event.touch.x - (player.width / 2)
player.y = event.touch.y - (player.height / 2)
-- Make sure they don't drag it offscreen
if player.x > 286 then player.x = 286 end
if player.x < 0 then player.x = 0 end
if player.y < 0 then player.y = 0 end
if player.y > 446 then player.y = 446 end
player:setPosition(player.x, player.y)
end
end
-- Set up the player sprite
local function initPlayer()
-- Create the player object
player = Bitmap.new(Texture.new("images/player.png"))
player.x, player.y = 160, 240
player.width, player.height = 32, 32
player:setPosition(player.x, player.y)
stage:addChild(player)
-- Make it touchable
player:addEventListener(Event.TOUCHES_MOVE, imagetouch, player)
end
-- Set up the enemies
local function initEnemies()
-- Create enemy objects
local i
for i = 1, MAXNUMBEROFENEMIES do
enemyShape[i] = Shape.new()
enemyShape[i]:setLineStyle(1, 0x000066, 0.25)
enemyShape[i]:setFillStyle(Shape.SOLID, 0xaaaaff)
enemyShape[i]:beginPath()
enemyShape[i]:moveTo(1, 1)
enemyShape[i]:lineTo(19, 1)
enemyShape[i]:lineTo(19, 19)
enemyShape[i]:lineTo(1, 19)
enemyShape[i]:lineTo(1, 1)
enemyShape[i]:endPath()
enemyShape[i].width, enemyShape[i].height = 20, 20
-- Set up enemy positions
if i < MINNUMBEROFENEMIES then
-- Add randomly to the screen
local xRand, yRand = rnd(270) + 20, rnd(400) + 20
-- Make sure enemies do not appear on top of player
if xRand > 100 and xRand < 200 and yRand > 200 and yRand < 300 then
xRand = xRand + 100
yRand = yRand + 100
end
enemyShape[i].x, enemyShape[i].y = xRand, yRand
else
-- Hide extra enemies offscreen until needed
enemyShape[i].x, enemyShape[i].y = -100, -100
end
enemyShape[i]:setPosition(enemyShape[i].x, enemyShape[i].y)
stage:addChild(enemyShape[i])
-- Pick a direction for the enemies to move
xdir, ydir = rnd(2) - 1, rnd(2) - 1
if xdir == 0 then xdir = -1 end
if ydir == 0 then ydir = -1 end
enemyShape[i].xdir, enemyShape[i].ydir = xdir, ydir
end
enemyCountdown = MAXCOUNTDOWN
end
-- Spawn a new enemy
local function spawnEnemy()
local xRand, yRand = rnd(250) + 20, rnd(400) + 20
-- Make sure enemies do not appear on top of player
if xRand > player.x - 40 and xRand < player.x + 40 and yRand > player.y - 40 and yRand < player.y + 40 then
xRand = xRand + 100
yRand = yRand + 100
if xRand > 270 then xRand = xRand - 200 end
if yRand > 420 then yRand = yRand - 200 end
end
numberOfEnemies = numberOfEnemies + 1
enemyShape[numberOfEnemies].x, enemyShape[numberOfEnemies].y = xRand, yRand
enemyShape[numberOfEnemies]:setPosition(enemyShape[numberOfEnemies].x, enemyShape[numberOfEnemies].y)
enemyCountdown = MAXCOUNTDOWN
end
-- Update the enemies
local function enemiesUpdate()
for i = 1, numberOfEnemies do
enemyShape[i].x = enemyShape[i].x + enemyShape[i].xdir
enemyShape[i].y = enemyShape[i].y + enemyShape[i].ydir
-- Check if we hit a wall
if enemyShape[i].x < 1 or enemyShape[i].x > 299 then
enemyShape[i].xdir = -enemyShape[i].xdir
end
if enemyShape[i].y < 1 or enemyShape[i].y > 459 then
enemyShape[i].ydir = -enemyShape[i].ydir
end
enemyShape[i]:setPosition(enemyShape[i].x, enemyShape[i].y)
end
-- Simple way of doing a timer before spawning more enemies
enemyCountdown = enemyCountdown - 1
if enemyCountdown == 0 then
if numberOfEnemies < MAXNUMBEROFENEMIES then
spawnEnemy()
end
end
end
-- Simple collision test
function collisionTest(rectA, rectB)
local collided = false
x1, y1, w1, h1, x2, y2, w2, h2 =
rectA.x,
rectA.y,
rectA.width,
rectA.height,
rectB.x,
rectB.y,
rectB.width,
rectB.height
if (y2 >= y1 and y1 + h1 >= y2) or (y2 + h2 >= y1 and y1 + h1 >= y2 + h2) or (y1 >= y2 and y2 + h2 >= y1) or
(y1 + h1 >= y2 and y2 + h2 >= y1 + h1) then
if x2 >= x1 and x1 + w1 >= x2 then collided = true end
if x2 + w2 >= x1 and x1 + w1 >= x2 + w2 then collided = true end
if x1 >= x2 and x2 + w2 >= x1 then collided = true end
if x1 + w1 >= x2 and x2 + w2 >= x1 + w1 then collided = true end
end
return collided
end
-- Initialise the game
local function initGame()
isGameRunning = true
initEnemies()
initPlayer()
loadScore()
initScores()
end
-- Start button touch handler
local function startTouch(startbuttonImage, event)
-- See if the Game Over object was touched
if startbuttonImage:hitTestPoint(event.touch.x, event.touch.y) then
startbuttonImage:removeEventListener(Event.TOUCHES_END, startTouch)
-- Clean up our objects
stage:removeChild(startbuttonImage)
startbuttonImage = nil
initGame()
end
end
-- Start game. Display START button and logo
local function startGame()
-- Create a Game Over object and display it
startbuttonImg = Bitmap.new(Texture.new("images/squaredodge.png"))
startbuttonImg.x, startbuttonImg.y = 0, 200
startbuttonImg:setPosition(startbuttonImg.x, startbuttonImg.y)
stage:addChild(startbuttonImg)
-- Make the Game Over object touchable
startbuttonImg:addEventListener(Event.TOUCHES_BEGIN, startTouch, startbuttonImg)
end
local function goTouch(gameOverImage, event)
-- See if the Game Over object was touched
if gameOverImage:hitTestPoint(event.touch.x, event.touch.y) then
gameoverImg:removeEventListener(Event.TOUCHES_END, goTouch)
-- Clean up our objects
stage:removeChild(gameoverImg)
gameoverImg = nil
for i = 1, MAXNUMBEROFENEMIES do
stage:removeChild(enemyShape[i])
enemyShape[i] = nil
end
stage:removeChild(player)
player = nil
stage:removeChild(hiScoreText)
hiScoreText = nil
stage:removeChild(scoreText)
scoreText = nil
-- Restart the game
startGame()
end
end
-- Game over handling
local function gameOver()
-- Save the current hiScore
saveScore()
-- Play explosion
playEffect()
-- Remove the listener from the player object
player:removeEventListener(Event.TOUCHES_MOVE, imagetouch)
-- Create a Game Over object and display it
gameoverImg = Bitmap.new(Texture.new("images/gameover.png"))
gameoverImg.x, gameoverImg.y = 0, 200
gameoverImg:setPosition(gameoverImg.x, gameoverImg.y)
stage:addChild(gameoverImg)
-- Make the Game Over object touchable
gameoverImg:addEventListener(Event.TOUCHES_BEGIN, goTouch, gameoverImg)
end
-- See if any collisions occurred
local function checkCollisions()
for i = 1, numberOfEnemies do
if collisionTest(player, enemyShape[i]) == true then
isGameRunning = false
gameOver()
return
end
end
end
-- Update everything
local function updateAll()
-- Only update if the game is still going
if not isGameRunning then return end
scoresUpdate()
enemiesUpdate()
checkCollisions()
end
-- Start it all up!
playTune()
startGame()
-- This executes "updateAll" each frame (constantly)
stage:addEventListener(Event.ENTER_FRAME, updateAll)
Wow! That’s a lot of code! Here’s a screenshot of the game in action:
Let’s analyse our code.
In the below code, we are assigning our variables as ‘local’ so they don’t use too much RAM by being global. This is a good practise for all of your apps, so start doing it now. ‘enemyShape’ is a table to hold all the enemy objects.
-- Define our variables and tables as local to save memory
local score
local hiScore
local enemyShape = {}
local player
local isGameRunning
local scoreText
local hiScoreText
local MINNUMBEROFENEMIES = 7
local MAXNUMBEROFENEMIES = 101
local numberOfEnemies = MINNUMBEROFENEMIES
local enemyCountdown
local MAXCOUNTDOWN = 500
local gameoverImg, startbuttonImg
Assigning ‘math.random’ to ‘rnd’ will cache the function and make it faster to access. This is a standard in Lua, particularly for math functions.
-- Cache math.random to make access faster
local rnd = math.random
This is our code for loading and saving the hiScore variable between games. This way, whenever the player plays our game, they will see their highest score to beat.
For loading, we first try to open the file. If it fails because it is the first time, we set the hiScore to ‘0’.
-- Load high score if it exists
local function loadScore()
local file = io.open("|D|settings.txt","r")
if not file then
hiScore = 0
else
hiScore=tonumber(file:read("*line"))
file:close()
end
end
Save the hiScore so we can later retrieve it.
-- Save high score
local function saveScore()
local file=io.open("|D|settings.txt","w+")
file:write(hiScore .. "\n")
file:close()
end
Initialise the score variable and set up the text at the top of the screen to display the score and hiScore.
-- Initialise scores and text
local function initScores()
score = 0
scoreText = TextField.new(nil, "Score: " .. score)
scoreText:setPosition(10,10)
stage:addChild(scoreText)
hiScoreText = TextField.new(nil, "Hi Score: " .. hiScore)
hiScoreText:setPosition(200,10)
stage:addChild(hiScoreText)
end
Update the score. If ‘score’ is bigger than ‘hiScore’, make them the same.
-- Update all scores
local function scoresUpdate()
score = score + 1
scoreText:setText("Score: " .. score)
if score > hiScore then
hiScore = score
hiScoreText:setText("Hi Score: " .. hiScore)
end
end
Load our tune and set it to repeat forever. Load the sound effect for Game Over and play that effect.
-- Play tune
local function playTune()
local gametune = Sound.new("audio/DST-909Dreams.mp3")
gametune:play(100,math.huge)
end
-- Play sound effects
local function playEffect()
local explodefx = Sound.new("audio/gameover.wav")
explodefx:play()
end
We need to set up a touch function so that when the player touches the player image, it will be draggable around the screen. We also ensure the image stays within the boundaries of the screen by limiting the ‘x’ and ‘y’ values.
-- Player image touched function
local function imagetouch(playerImage, event)
if not isGameRunning then
return
end
-- Is the player touching the player object?
if playerImage:hitTestPoint(event.touch.x, event.touch.y) then
player.x = event.touch.x-(player.width/2)
player.y = event.touch.y-(player.height/2)
-- Make sure they don't drag it offscreen
if player.x > 286 then player.x = 286 end
if player.x < 0 then player.x = 0 end
if player.y < 0 then player.y = 0 end
if player.y > 446 then player.y = 446 end
player:setPosition(player.x, player.y)
end
end
Load the player image and set its initial ‘x’ and ‘y’ co-ordinates. Configure the Event Listener for when the player touches the image.
-- Set up the player object
local function initPlayer()
-- Create the player object
player = Bitmap.new(Texture.new("images/player.png"))
player.x, player.y = 160,240
player.width, player.height = 32, 32
player:setPosition(player.x, player.y)
stage:addChild(player)
-- Make it touchable
player:addEventListener(Event.TOUCHES_MOVE, imagetouch, player)
end
This one is quite long. We are creating all the enemies required, but we only add the first 6 to the active display. The rest are kept outside the display area until the timer adds them in. This way, the game slowly gets more difficult as more enemies appear.
We also make sure the initial enemies do not appear too close to the player’s initial position in the middle of the screen, otherwise it’ll be Game Over too quickly.
-- Set up the enemies
local function initEnemies()
-- Create enemy objects
for i = 1,MAXNUMBEROFENEMIES do
enemyShape[i] = Shape.new()
enemyShape[i]:setLineStyle(1, 0x000066, 0.25)
enemyShape[i]:setFillStyle(Shape.SOLID, 0xaaaaff)
enemyShape[i]:beginPath()
enemyShape[i]:moveTo(1,1)
enemyShape[i]:lineTo(19,1)
enemyShape[i]:lineTo(19,19)
enemyShape[i]:lineTo(1,19)
enemyShape[i]:lineTo(1,1)
enemyShape[i]:endPath()
enemyShape[i].width, enemyShape[i].height = 20, 20
-- Set up enemy positions
local i
if i < MINNUMBEROFENEMIES then
-- Add randomly to the screen
local xRand, yRand = rnd(270)+20,rnd(400)+20
-- Make sure enemies do not appear on top of player
if xRand > 100 and xRand < 200 and yRand > 200 and yRand < 300 then
xRand = xRand + 100
yRand = yRand + 100
end
enemyShape[i].x, enemyShape[i].y = xRand, yRand
else
-- Hide extra enemies offscreen until needed
enemyShape[i].x, enemyShape[i].y = -100,-100
end
enemyShape[i]:setPosition(enemyShape[i].x,enemyShape[i].y)
stage:addChild(enemyShape[i])
-- Pick a direction for the enemies to move
local xdir, ydir = rnd(2)-1, rnd(2)-1
if xdir == 0 then xdir = -1 end
if ydir == 0 then ydir = -1 end
enemyShape[i].xdir, enemyShape[i].ydir = xdir, ydir
end
enemyCountdown=MAXCOUNTDOWN
end
Every now and again, we bring one of the enemies hiding off-screen onto the active screen and set them bouncing around. We also make sure these don’t spawn too close to the player’s current position.
-- Spawn a new enemy
local function spawnEnemy()
local xRand, yRand = rnd(250)+20,rnd(400)+20
-- Make sure enemies do not appear on top of player
if xRand > player.x-40 and xRand < player.x+40 and yRand > player.y-40 and yRand < player.y+40 then
xRand = xRand + 100
yRand = yRand + 100
if xRand > 270 then xRand = xRand - 200 end
if yRand > 420 then yRand = yRand - 200 end
end
numberOfEnemies = numberOfEnemies + 1
enemyShape[numberOfEnemies].x, enemyShape[numberOfEnemies].y = xRand, yRand
enemyShape[numberOfEnemies]:setPosition(enemyShape[numberOfEnemies].x, enemyShape[numberOfEnemies].y)
enemyCountdown=MAXCOUNTDOWN
end
This function will move the enemies on the active screen and bounce them around if they hit the borders of the screen. It decrements the counter and if it’s time, spawns new enemies.
-- Update the enemies
local function enemiesUpdate()
local i
for i=1,numberOfEnemies do
enemyShape[i].x=enemyShape[i].x+enemyShape[i].xdir
enemyShape[i].y=enemyShape[i].y+enemyShape[i].ydir
-- Check if we hit a wall
if enemyShape[i].x < 1 or enemyShape[i].x > 299 then
enemyShape[i].xdir = -enemyShape[i].xdir
end
if enemyShape[i].y < 1 or enemyShape[i].y > 459 then
enemyShape[i].ydir = -enemyShape[i].ydir
end
enemyShape[i]:setPosition(enemyShape[i].x,enemyShape[i].y)
end
-- Simple way of doing a timer before spawning more enemies
enemyCountdown = enemyCountdown - 1
if enemyCountdown == 0 then
if numberOfEnemies < MAXNUMBEROFENEMIES then
spawnEnemy()
end
end
end
This is a simple collision test function. You can find example code for this on the internet with a quick search. Basically, it checks the co-ordinates of the player object compared to the enemy object specified. If they overlap, it returns ‘true’.
-- Simple collision test
function collisionTest(rectA, rectB)
local collided = false
local x1,y1,w1,h1,x2,y2,w2,h2 = rectA.x, rectA.y, rectA.width, rectA.height, rectB.x, rectB.y, rectB.width, rectB.height
if (y2 >= y1 and y1 + h1 >= y2) or (y2 + h2 >= y1 and y1 + h1 >= y2 + h2) or (y1 >= y2 and y2 + h2 >= y1) or (y1 + h1 >= y2 and y2 + h2 >= y1 + h1) then
if x2 >= x1 and x1 + w1 >= x2 then collided = true end
if x2 + w2 >= x1 and x1 + w1 >= x2 + w2 then collided = true end
if x1 >= x2 and x2 + w2 >= x1 then collided = true end
if x1 + w1 >= x2 and x2 + w2 >= x1 + w1 then collided = true end
end
return collided
end
Set up the enemies and player object, load the score and set up the text.
-- Initialise the game
local function initGame()
isGameRunning = true
initEnemies()
initPlayer()
loadScore()
initScores()
end
Set up the logo and game Start button to be touchable.
-- Start button touch handler
local function startTouch(startbuttonImage, event)
-- See if the Game Start object was touched
if startbuttonImage:hitTestPoint(event.touch.x, event.touch.y) then
startbuttonImage:removeEventListener(Event.TOUCHES_END, startTouch)
-- Clean up our objects
stage:removeChild(startbuttonImage)
startbuttonImage=nil
initGame()
end
end
Create a start button object and add it to the screen.
-- Start game. Display START button and logo
local function startGame()
-- Create a Start Game object and display it
startbuttonImg = Bitmap.new(Texture.new("images/squaredodge.png"))
startbuttonImg.x, startbuttonImg.y = 0,200
startbuttonImg:setPosition(startbuttonImg.x, startbuttonImg.y)
stage:addChild(startbuttonImg)
-- Make the Game Over object touchable
startbuttonImg:addEventListener(Event.TOUCHES_BEGIN, startTouch, startbuttonImg)
end
Create a Game Over object and make it touchable. Remove the player and enemy objects and text at the top of the screen.
local function goTouch(gameOverImage, event)
-- See if the Game Over object was touched
if gameOverImage:hitTestPoint(event.touch.x, event.touch.y) then
gameoverImg:removeEventListener(Event.TOUCHES_END, goTouch)
-- Clean up our objects
stage:removeChild(gameoverImg)
gameoverImg=nil
local i
for i = 1,MAXNUMBEROFENEMIES do
stage:removeChild(enemyShape[i])
enemyShape[i]=nil
end
stage:removeChild(player)
player=nil
stage:removeChild(hiScoreText)
hiScoreText=nil
stage:removeChild(scoreText)
scoreText=nil
-- Restart the game
startGame()
end
end
When the player collides with an enemy object, this code is executed. Save the current hiScore, play the ‘game over’ sound effect and display the Game Over image.
-- Game over handling
local function gameOver()
-- Save the current hiScore
saveScore()
-- Play explosion
playEffect()
-- Remove the listener from the player object
player:removeEventListener(Event.TOUCHES_MOVE, imagetouch)
-- Create a Game Over object and display it
gameoverImg = Bitmap.new(Texture.new("images/gameover.png"))
gameoverImg.x, gameoverImg.y = 0,200
gameoverImg:setPosition(gameoverImg.x, gameoverImg.y)
stage:addChild(gameoverImg)
-- Make the Game Over object touchable
gameoverImg:addEventListener(Event.TOUCHES_BEGIN, goTouch, gameoverImg)
end
Supply the player and enemy objects to test if any collided. If they did, go to the Game Over function.
-- See if any collisions occurred
local function checkCollisions()
local i
for i=1,numberOfEnemies do
if collisionTest (player, enemyShape[i]) == true then
isGameRunning = false
gameOver()
return
end
end
end
This is the code to run every frame of the game. This means it constantly is run by the application. Sometimes, it’s good to check the game is actually running so they code does not accidentally get called elsewhere. It’s a fail-safe.
-- Update everything
local function updateAll()
-- Only update if the game is still going
if not isGameRunning then return end
scoresUpdate()
enemiesUpdate()
checkCollisions()
end
Start the tune playing and start the game off.
-- Start it all up!
playTune()
startGame()
This tells the application to call the ‘updateAll’ function constantly to keep things running.
-- This executes "updateAll" each frame (constantly)
stage:addEventListener(Event.ENTER_FRAME, updateAll)
And that’s how you can make a simple game. You’ve learned enough to get out there and get something started.
Next, we’ll go into more complex programming and explore some third-party applications to help make your game developing easier.
Note: This tutorial was written by Jason Oakley and was originally available here: http://bluebilby.com/2013/05/08/gideros-mobile-tutorial-creating-your-first-game/