Difference between revisions of "Article Tutorials/Square Dodge"

From GiderosMobile
m (formatting)
 
(2 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!
 
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”.
 
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]
+
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)
 
Square Dodge source code. (3.8 Mb)
Line 14: Line 14:
  
 
The code below is heavily commented to help you understand how each part works.
 
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 82: 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
        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 90: 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
            player.x = 286
+
         if player.x < 0 then player.x = 0 end
        end
+
         if player.y < 0 then player.y = 0 end
         if player.x < 0 then
+
         if player.y > 446 then player.y = 446 end
            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)
 
         player:setPosition(player.x, player.y)
 
     end
 
     end
Line 150: 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
            xdir = -1
+
         if ydir == 0 then ydir = -1 end
        end
 
         if ydir == 0 then
 
            ydir = -1
 
        end
 
 
         enemyShape[i].xdir, enemyShape[i].ydir = xdir, ydir
 
         enemyShape[i].xdir, enemyShape[i].ydir = xdir, ydir
 
     end
 
     end
Line 167: 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
            xRand = xRand - 200
+
         if yRand > 420 then yRand = yRand - 200 end
        end
 
         if yRand > 420 then
 
            yRand = yRand - 200
 
        end
 
 
     end
 
     end
 
     numberOfEnemies = numberOfEnemies + 1
 
     numberOfEnemies = numberOfEnemies + 1
Line 213: 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
        (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
    then
+
         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
            collided = true
+
         if x1 + w1 >= x2 and x2 + w2 >= x1 + w1 then collided = true end
        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
 
     end
 
     return collided
 
     return collided
Line 311: 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
        return
 
    end
 
 
     scoresUpdate()
 
     scoresUpdate()
 
     enemiesUpdate()
 
     enemiesUpdate()
Line 325: Line 294:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Wow! That’s a lot of code!<br>
+
Wow! That’s a lot of code! Here’s a screenshot of the game in action:
Here’s a screenshot of the game in action:
 
  
 
[[File:Jason-Oakley-Creating-Your-First-Game-Square-Dodge-screenshot.png|thumb|center]]
 
[[File:Jason-Oakley-Creating-Your-First-Game-Square-Dodge-screenshot.png|thumb|center]]
Line 333: Line 301:
  
 
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.
 
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 319:
  
 
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.
 
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
Line 361: Line 327:
  
 
For loading, we first try to open the file. If it fails because it is the first time, we set the hiScore to ‘0’.
 
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 376: Line 341:
  
 
Save the hiScore so we can later retrieve it.
 
Save the hiScore so we can later retrieve it.
 
 
<syntaxhighlight lang="lua">
 
<syntaxhighlight lang="lua">
 
-- Save high score
 
-- Save high score
Line 387: Line 351:
  
 
Initialise the score variable and set up the text at the top of the screen to display the score and hiScore.
 
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 402: Line 365:
  
 
Update the score. If ‘score’ is bigger than ‘hiScore’, make them the same.
 
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 378:
  
 
Load our tune and set it to repeat forever. Load the sound effect for Game Over and play that effect.
 
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 432: Line 393:
  
 
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.
 
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 444: 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
player.x = 286
+
if player.x < 0 then player.x = 0 end  
end
+
if player.y < 0 then player.y = 0 end
 
+
if player.y > 446 then player.y = 446 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)
 
player:setPosition(player.x, player.y)
 
end
 
end
Line 468: Line 415:
  
 
Load the player image and set its initial ‘x’ and ‘y’ co-ordinates. Configure the Event Listener for when the player touches the image.
 
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 433:
  
 
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.
 
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 526: 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
xdir = -1
+
if ydir == 0 then ydir = -1 end
end
 
 
 
if ydir == 0 then
 
ydir = -1
 
end
 
 
 
enemyShape[i].xdir, enemyShape[i].ydir = xdir, ydir
 
enemyShape[i].xdir, enemyShape[i].ydir = xdir, ydir
 
end
 
end
enemyCountdown=MAXCOUNTDOWN
+
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.
 
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 550: 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
xRand = xRand - 200  
+
if yRand > 420 then yRand = yRand - 200 end
end
+
end
if yRand > 420 then  
 
yRand = yRand - 200  
 
end
 
end  
 
 
numberOfEnemies = numberOfEnemies + 1
 
numberOfEnemies = numberOfEnemies + 1
 
enemyShape[numberOfEnemies].x, enemyShape[numberOfEnemies].y = xRand, yRand
 
enemyShape[numberOfEnemies].x, enemyShape[numberOfEnemies].y = xRand, yRand
Line 565: Line 499:
  
 
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.
 
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 597: Line 530:
  
 
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’.
 
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 604: 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
collided = true
+
if x2 + w2 >= x1 and x1 + w1 >= x2 + w2 then collided = true end
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
 
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
 
end
 
return collided
 
return collided
Line 625: Line 546:
  
 
Set up the enemies and player object, load the score and set up the text.
 
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 638: Line 558:
  
 
Set up the logo and game Start button to be touchable.
 
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 654: Line 573:
  
 
Create a start button object and add it to the screen.
 
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 669: Line 587:
  
 
Create a Game Over object and make it touchable. Remove the player and enemy objects and text at the top of the screen.
 
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 697: Line 614:
  
 
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.
 
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 722: Line 638:
  
 
Supply the player and enemy objects to test if any collided. If they did, go to the Game Over function.
 
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 738: Line 653:
  
 
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.
 
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
return
 
end
 
 
 
 
scoresUpdate()
 
scoresUpdate()
Line 754: Line 666:
  
 
Start the tune playing and start the game off.
 
Start the tune playing and start the game off.
 
 
<syntaxhighlight lang="lua">
 
<syntaxhighlight lang="lua">
 
-- Start it all up!
 
-- Start it all up!
Line 762: Line 673:
  
 
This tells the application to call the ‘updateAll’ function constantly to keep things running.
 
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)
Line 773: Line 683:
  
  
'''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/.
+
'''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!

Jason-Oakley-Creating-Your-First-Game-Square-Dodge-project.png

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:

Jason-Oakley-Creating-Your-First-Game-Square-Dodge-screenshot.png

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/


Written Tutorials