Game Loop Architecture / Best Practice

The code below is a first prototype for a game that could involve any number of balls bouncing around on the screen. The prototype just implements two balls right now and also just one level.

The code places two balls on the screen, moves them around in a billard-ball fashion and listens for touch inputs to check if a touch has hit one of the balls during either a began or moved phase. It keeps count of a bunch of things, including the number of balls hit (numberOfBallsHit), and a table with references to the balls that have been hit. Again, in the future there could be many more balls than just two.

Once a ball has been hit, the code sets the velocity of that ball to zero, so it stops its movement on the screen.

Once both balls have been hit, the code makes all of the balls disappear. This is meant to be the end of that level, i.e. all balls have been cleared.

At this point, the intention is that the code initialises the next level (which could have different balls), but an error occurs. In this simple prototype there is only one level, so my intention is that once both balls have been hit and removed from the screen, the code should initialize this same level again, i.e. the balls should be newly created and placed in the upper left corner of the screen and start moving again. However, I can’t figure out how to fix the bug and suspect it has something to do with fundamental game architecture , i.e. how I’ve implemented the game loop and the event listeners and where I’ve placed the lines that trigger the ending of one level and the initialisation of the next level.

Can someone help to make this prototype work? After both balls have been hit and been removed from the screen, the initializeNextLevel function is meant to be executed and the gameLoop started again (which in the case of this simple prototype would just replay the only level that the prototype contains, i.e. starting the balls from the top left corner again). But the architecture is meant to allow for further levels to be added later.

[lua]local physics = require “physics”

physics.start()

physics.setGravity(0,0)

local numberOfBallsHit = 0

local ballsHit = {}

local ball1Hit = false

local ball2Hit = false

local levelHasEnded = false

local gameLoopSpeed = 200

local function emitAndInitializeBall1()

ball1 = display.newImage (“ball.png”)

ball1.name = “ball1”

local Ball1Scale = 2

ball1:scale(Ball1Scale, Ball1Scale)

ball1.x = 0; ball1.y = 0

ball1VelocityX = 2

ball1VelocityY = -0.5

physics.addBody(ball1, “dynamic”, {density=1.0, friction=0.9, bounce=0.8, radius=15*Ball1Scale})

ball1:addEventListener(“touch”, CheckIfBallsAreHit)

end

local function emitAndInitializeBall2()

ball2 = display.newImage (“ball.png”)

ball2.name = “ball2”

local Ball2Scale = 2

ball2:scale(Ball2Scale, Ball2Scale)

ball2.x = 0; ball2.y = 0

ball2VelocityX = 0.25

ball2VelocityY = -0.5

physics.addBody(ball2, “dynamic”, {density=1.0, friction=0.9, bounce=0.8, radius=15*Ball2Scale})

ball2:addEventListener(“touch”, CheckIfBallsAreHit)

end

local function moveBalls()

if (ball1) then 

ball1.x = ball1.x + ball1VelocityX

ball1.y = ball1.y + ball1VelocityY

if ball1.x < 0 or ball1.x + ball1.width > display.contentWidth then

ball1VelocityX = -ball1VelocityX

end

if ball1.y < 0 or ball1.y + ball1.height > display.contentHeight then

ball1VelocityY = -ball1VelocityY

end

end

if (ball2) then

ball2.x = ball2.x + ball2VelocityX

ball2.y = ball2.y + ball2VelocityY

if ball2.x < 0 or ball2.x + ball2.width > display.contentWidth then

ball2VelocityX = -ball2VelocityX

end

if ball2.y < 0 or ball2.y + ball2.height > display.contentHeight then

ball2VelocityY = -ball2VelocityY

end

end

return true

end

function CheckIfBallsAreHit(event)

if (event.phase == “began” or event.phase == “moved”) then

if event.target then – event.target = nil for as long as line hasn’t hit any balls 

if (event.target.name==“ball1” and ball1Hit==false) then

ball1VelocityX = 0

ball1VelocityY = 0

numberOfBallsHit = numberOfBallsHit +1

table.insert(ballsHit, 1)

ball1:removeEventListener(“touch”, CheckIfBallsAreHit)

ball1Hit = true

end

if (event.target.name==“ball2” and ball2Hit==false) then

ball2VelocityX = 0

ball2VelocityY = 0

numberOfBallsHit = numberOfBallsHit +1

table.insert(ballsHit, 2)

ball2:removeEventListener(“touch”, CheckIfBallsAreHit)

ball2Hit = true

end

end

end

return false

end

local function endLevel()

Runtime:removeEventListener(“enterFrame”, moveBalls)

Runtime:removeEventListener(“touch”, CheckIfBallsAreHit)

ball1:removeSelf()

ball1.myName=nil

ball2:removeSelf()

ball2.myName=nil

emitter1:removeSelf()

end

local function initializeNextLevel()

numberOfBallsHit = 0

ballsHit = {}

ball1Hit = false

ball2Hit = false

levelHasEnded = false

emitAndInitializeBall1()

timer.performWithDelay(1, emitAndInitializeBall2)

Runtime:addEventListener(“enterFrame”, moveBalls)

Runtime:addEventListener( “touch”, CheckIfBallsAreHit )

end

local function gameLoop()

if numberOfBallsHit==2 and levelHasEnded==false then

endLevel()

levelHasEnded = true

numberOfBallsHit = 0

end

if levelHasEnded==true then

timer.performWithDelay(5000, initializeNextLevel)

levelHasEnded = false

end

end

–Start game

emitAndInitializeBall1()

timer.performWithDelay(1000, emitAndInitializeBall2)

Runtime:addEventListener(“enterFrame”, moveBalls)

Runtime:addEventListener( “touch”, CheckIfBallsAreHit)

timer.performWithDelay(gameLoopSpeed, gameLoop,0)

[/lua]

Hmmm … my indentations are all gone with the wrapping-in-tags-method of pasting … I’ll try again with the code icon …

local physics = require "physics" physics.start() physics.setGravity(0,0) local numberOfBallsHit = 0 local ballsHit = {} local ball1Hit = false local ball2Hit = false local levelHasEnded = false local gameLoopSpeed = 200 local function emitAndInitializeBall1() ball1 = display.newImage ("ball.png") ball1.name = "ball1" local Ball1Scale = 2 ball1:scale(Ball1Scale, Ball1Scale) ball1.x = 0; ball1.y = 0 ball1VelocityX = 2 ball1VelocityY = -0.5 physics.addBody(ball1, "dynamic", {density=1.0, friction=0.9, bounce=0.8, radius=15\*Ball1Scale}) ball1:addEventListener("touch", CheckIfBallsAreHit) end local function emitAndInitializeBall2() ball2 = display.newImage ("ball.png") ball2.name = "ball2" local Ball2Scale = 2 ball2:scale(Ball2Scale, Ball2Scale) ball2.x = 0; ball2.y = 0 ball2VelocityX = 0.25 ball2VelocityY = -0.5 physics.addBody(ball2, "dynamic", {density=1.0, friction=0.9, bounce=0.8, radius=15\*Ball2Scale}) ball2:addEventListener("touch", CheckIfBallsAreHit) end local function moveBalls() if (ball1) then ball1.x = ball1.x + ball1VelocityX ball1.y = ball1.y + ball1VelocityY if ball1.x \< 0 or ball1.x + ball1.width \> display.contentWidth then ball1VelocityX = -ball1VelocityX end if ball1.y \< 0 or ball1.y + ball1.height \> display.contentHeight then ball1VelocityY = -ball1VelocityY end end if (ball2) then ball2.x = ball2.x + ball2VelocityX ball2.y = ball2.y + ball2VelocityY if ball2.x \< 0 or ball2.x + ball2.width \> display.contentWidth then ball2VelocityX = -ball2VelocityX end if ball2.y \< 0 or ball2.y + ball2.height \> display.contentHeight then ball2VelocityY = -ball2VelocityY end end return true end function CheckIfBallsAreHit(event) if (event.phase == "began" or event.phase == "moved") then if event.target then -- event.target = nil for as long as line hasn't hit any balls if (event.target.name=="ball1" and ball1Hit==false) then ball1VelocityX = 0 ball1VelocityY = 0 numberOfBallsHit = numberOfBallsHit +1 table.insert(ballsHit, 1) ball1:removeEventListener("touch", CheckIfBallsAreHit) ball1Hit = true end if (event.target.name=="ball2" and ball2Hit==false) then ball2VelocityX = 0 ball2VelocityY = 0 numberOfBallsHit = numberOfBallsHit +1 table.insert(ballsHit, 2) ball2:removeEventListener("touch", CheckIfBallsAreHit) ball2Hit = true end end end return false end local function endLevel() Runtime:removeEventListener("enterFrame", moveBalls) Runtime:removeEventListener("touch", CheckIfBallsAreHit) ball1:removeSelf() ball1.myName=nil ball2:removeSelf() ball2.myName=nil emitter1:removeSelf() end local function initializeNextLevel() numberOfBallsHit = 0 ballsHit = {} ball1Hit = false ball2Hit = false levelHasEnded = false emitAndInitializeBall1() timer.performWithDelay(1, emitAndInitializeBall2) Runtime:addEventListener("enterFrame", moveBalls) Runtime:addEventListener( "touch", CheckIfBallsAreHit ) end local function gameLoop() if numberOfBallsHit==2 and levelHasEnded==false then endLevel() levelHasEnded = true numberOfBallsHit = 0 end if levelHasEnded==true then timer.performWithDelay(5000, initializeNextLevel) levelHasEnded = false end end --Start game emitAndInitializeBall1() timer.performWithDelay(1000, emitAndInitializeBall2) Runtime:addEventListener("enterFrame", moveBalls) Runtime:addEventListener( "touch", CheckIfBallsAreHit) timer.performWithDelay(gameLoopSpeed, gameLoop,0)

Hmmm … now the colors didn’t come through … not sure how to paste my code in here so it has the indentations and color coding. I’m copying and pasting from Sublime Text Editor.

To fix your problem replace your endLevel() with mine

local function endLevel() &nbsp; Runtime:removeEventListener("enterFrame", moveBalls) &nbsp; Runtime:removeEventListener("touch", CheckIfBallsAreHit) &nbsp; ball1:removeSelf() &nbsp; ball1 = nil &nbsp; ball2:removeSelf() &nbsp; ball2 = nil end

Tiny Tip:  display.remove( obj ) is safer than obj:removeSelf()

Thanks SGS, that solved it. And thanks roaminggamer for your tip, too.

display.remove it’s safer, but it can be worst also to detect bad coding since it will ignore if the object doesn’t exist and will not provide any warning. there are situations that are needed because you know it could be empty, but while coding to detect errors, removeSelf is more friendly, later you can substitute all to display.remove.

you don’t declare ball1 or ball2, so they are global variables (a least I didn’t find it). that is a very bad practice. I never made an app that I needed a global variable. there is always a way to do things with local variables.

one thing you could do is:

local ball1 local ball2 local ball3 local function emitAndInitializeBall() local b= display.newImage ("ball.png") -- changed the variable name so its not confusing for the programmer, but you could use the same name b.name = "ball1" local Ball1Scale = 2 b:scale(Ball1Scale, Ball1Scale) b.x = 0; b.y = 0 ball1VelocityX = 2 ball1VelocityY = -0.5 physics.addBody(b, "dynamic", {density=1.0, friction=0.9, bounce=0.8, radius=15\*Ball1Scale}) b:addEventListener("touch", CheckIfBallsAreHit) return b end ball1=emitAnInitializeBall() -- if you will need the variable ball1 from here, you could declared it here, but if you need before this line you need to declared in the begining of your code ball2=emitAnInitializeBall() -- add arguments if you need different properties to each ball. you will need to change the function also to accept this arguments, ofc. ball3=emitAnInitializeBall() -- with this method you can create more balls without adding much more lines to your code (only 1)

 why did this changes? functions should be self-sustained so you can use it later on other projects or even on the same project. with this little change I could reduce your code even further, “emitAndInitializeBall2” function doesn’t need to exist anymore you can use the same function to create balls, just create arguments if you need changes on the ball properties.  hope I explained it enough since my English sucks.

Regards,

Carlos.

P.S. if you need more balls, the approach will be little different than this, and your all code needs to be different, to be more efficient and less prone to errors.

To fix your problem replace your endLevel() with mine

local function endLevel() &nbsp; Runtime:removeEventListener("enterFrame", moveBalls) &nbsp; Runtime:removeEventListener("touch", CheckIfBallsAreHit) &nbsp; ball1:removeSelf() &nbsp; ball1 = nil &nbsp; ball2:removeSelf() &nbsp; ball2 = nil end

Tiny Tip:  display.remove( obj ) is safer than obj:removeSelf()

Thanks SGS, that solved it. And thanks roaminggamer for your tip, too.

display.remove it’s safer, but it can be worst also to detect bad coding since it will ignore if the object doesn’t exist and will not provide any warning. there are situations that are needed because you know it could be empty, but while coding to detect errors, removeSelf is more friendly, later you can substitute all to display.remove.

you don’t declare ball1 or ball2, so they are global variables (a least I didn’t find it). that is a very bad practice. I never made an app that I needed a global variable. there is always a way to do things with local variables.

one thing you could do is:

local ball1 local ball2 local ball3 local function emitAndInitializeBall() local b= display.newImage ("ball.png") -- changed the variable name so its not confusing for the programmer, but you could use the same name b.name = "ball1" local Ball1Scale = 2 b:scale(Ball1Scale, Ball1Scale) b.x = 0; b.y = 0 ball1VelocityX = 2 ball1VelocityY = -0.5 physics.addBody(b, "dynamic", {density=1.0, friction=0.9, bounce=0.8, radius=15\*Ball1Scale}) b:addEventListener("touch", CheckIfBallsAreHit) return b end ball1=emitAnInitializeBall() -- if you will need the variable ball1 from here, you could declared it here, but if you need before this line you need to declared in the begining of your code ball2=emitAnInitializeBall() -- add arguments if you need different properties to each ball. you will need to change the function also to accept this arguments, ofc. ball3=emitAnInitializeBall() -- with this method you can create more balls without adding much more lines to your code (only 1)

 why did this changes? functions should be self-sustained so you can use it later on other projects or even on the same project. with this little change I could reduce your code even further, “emitAndInitializeBall2” function doesn’t need to exist anymore you can use the same function to create balls, just create arguments if you need changes on the ball properties.  hope I explained it enough since my English sucks.

Regards,

Carlos.

P.S. if you need more balls, the approach will be little different than this, and your all code needs to be different, to be more efficient and less prone to errors.