Scope Question / local function vs. global function

The code below:

  • places two white circles on the screen

  • upon touch input draws a white line (free-hand style)

  • when the line hits one of the balls, then that ball changes its color from white to red

  • as soon as both balls are hit by the line, then both balls and the line disappear and the game loop begins again

This code, as it currently stands, works, however, when I turn the drawLineAndCheckIfBallsAreHit function by replacing the line

function drawLineAndCheckIfBallsAreHit(event)

with

local function drawLineAndCheckIfBallsAreHit(event)

then the code no longer recognizes when the line hits one of the balls. Instead, you can draw lines across either ball without that ball ever turning red.

Would someone be able to explain why the code no longer works when the drawLineAndCheckIfBallsAreHit function is turned into a local function? I understand it has to do with scope but I can’t figure out what exactly is going on. It seems to me that the code should still work, even with that function being a local function.

display.setStatusBar(display.HiddenStatusBar) local numberOfBallsHit = 0 local ballsHit = {} local ball1Hit = false local ball2Hit = false local levelHasEnded = false local gameLoopSpeed = 200 local drawLineAndCheckIfBallsAreHit, linePoints, line = display.newGroup(), {}, nil local function InitializeBalls() ball1 = display.newCircle(200,100, 30) ball1.name = "ball1" ball1:setFillColor(1,1,1) ball1:addEventListener("touch", drawLineAndCheckIfBallsAreHit) ball2 = display.newCircle(400,300, 30) ball2.name = "ball2" ball2:setFillColor(1,1,1) ball2:addEventListener("touch", drawLineAndCheckIfBallsAreHit) end local function distanceBetweenTwoPoints( ax, ay, bx, by ) local width, height = bx-ax, by-ay return (width\*width + height\*height)^0.5 end function drawLineAndCheckIfBallsAreHit(event) if (event.phase == "began") then linePoints = {x=event.x, y=event.y} phaseHasBegun = true return true elseif phaseHasBegun then if (event.phase == "moved") then if (distanceBetweenTwoPoints( linePoints.x, linePoints.y, event.x, event.y ) \> 1) then linePoints = {x=event.x, y=event.y} if (line==nil) then line = display.newLine(event.xStart, event.yStart, linePoints.x, linePoints.y) line:setStrokeColor (1,1,1) line.strokeWidth = 20 else line:append(linePoints.x, linePoints.y) 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 ball1:setFillColor(1,0,0) timer.performWithDelay(10, ball1:removeEventListener("touch", drawLineAndCheckIfBallsAreHit)) ball1Hit = true numberOfBallsHit = numberOfBallsHit +1 table.insert(ballsHit, 1) end if (event.target.name=="ball2" and ball2Hit==false) then ball2:setFillColor(1,0,0) timer.performWithDelay(10, ball2:removeEventListener("touch", drawLineAndCheckIfBallsAreHit)) ball2Hit = true numberOfBallsHit = numberOfBallsHit +1 table.insert(ballsHit, 2) end end end end else -- touch has ended; now remove line so a new line can be drawn phaseHasBegun = false display.remove(line) linePoints, line = {}, nil end return true end return false end local function endLevel() timer.performWithDelay(10, Runtime:removeEventListener("touch", drawLineAndCheckIfBallsAreHit)) display.remove(ball1) ball1=nil display.remove(ball2) ball2=nil display.remove(line) end local function initializeNextLevel() numberOfBallsHit = 0 ballsHit = {} ball1Hit = false ball2Hit = false levelHasEnded = false linePoints, line = {}, nil InitializeBalls() Runtime:addEventListener( "touch", drawLineAndCheckIfBallsAreHit ) end local function gameLoop() if numberOfBallsHit==2 and levelHasEnded==false then endLevel() levelHasEnded = true numberOfBallsHit = 0 end if levelHasEnded==true then timer.performWithDelay(2000, initializeNextLevel) levelHasEnded = false end end --Start game InitializeBalls() Runtime:addEventListener( "touch", drawLineAndCheckIfBallsAreHit) timer.performWithDelay(gameLoopSpeed, gameLoop,0)

Same code again, just pasted into this forum using a different method, hoping for better formatting. Apologies but I seems to get weird formatting when I paste code into this forum.

[lua]display.setStatusBar(display.HiddenStatusBar)

local numberOfBallsHit = 0

local ballsHit = {}

local ball1Hit = false

local ball2Hit = false

local levelHasEnded = false

local gameLoopSpeed = 200

local drawLineAndCheckIfBallsAreHit, linePoints, line = display.newGroup(), {}, nil

local function InitializeBalls()

ball1 = display.newCircle(200,100, 30)

ball1.name = “ball1”

ball1:setFillColor(1,1,1)

ball1:addEventListener(“touch”, drawLineAndCheckIfBallsAreHit)

ball2 = display.newCircle(400,300, 30)

ball2.name = “ball2”

ball2:setFillColor(1,1,1)

ball2:addEventListener(“touch”, drawLineAndCheckIfBallsAreHit)

end

local function distanceBetweenTwoPoints( ax, ay, bx, by )

local width, height = bx-ax, by-ay

return (width*width + height*height)^0.5

end

function drawLineAndCheckIfBallsAreHit(event)

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

linePoints = {x=event.x, y=event.y}

phaseHasBegun = true

return true

elseif phaseHasBegun then

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

if (distanceBetweenTwoPoints( linePoints.x, linePoints.y, event.x, event.y ) > 1) then

linePoints = {x=event.x, y=event.y}

if (line==nil) then

line = display.newLine(event.xStart, event.yStart, linePoints.x, linePoints.y)

line:setStrokeColor (1,1,1)

line.strokeWidth = 20

else

line:append(linePoints.x, linePoints.y)

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

ball1:setFillColor(1,0,0)

timer.performWithDelay(10, ball1:removeEventListener(“touch”, drawLineAndCheckIfBallsAreHit))

ball1Hit = true

numberOfBallsHit = numberOfBallsHit +1

table.insert(ballsHit, 1)

end

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

ball2:setFillColor(1,0,0)

timer.performWithDelay(10, ball2:removeEventListener(“touch”, drawLineAndCheckIfBallsAreHit))

ball2Hit = true

numberOfBallsHit = numberOfBallsHit +1

table.insert(ballsHit, 2)

end

end

end

end

else – touch has ended; now remove line so a new line can be drawn

phaseHasBegun = false

display.remove(line)

linePoints, line = {}, nil

end

return true

end

return false

end

local function endLevel()

timer.performWithDelay(10, Runtime:removeEventListener(“touch”, drawLineAndCheckIfBallsAreHit))

display.remove(ball1)

ball1=nil

display.remove(ball2)

ball2=nil

display.remove(line)

end

local function initializeNextLevel()

numberOfBallsHit = 0

ballsHit = {}

ball1Hit = false

ball2Hit = false

levelHasEnded = false

linePoints, line = {}, nil

InitializeBalls()

Runtime:addEventListener( “touch”, drawLineAndCheckIfBallsAreHit )

end

local function gameLoop()

if numberOfBallsHit==2 and levelHasEnded==false then

endLevel()

levelHasEnded = true

numberOfBallsHit = 0

end

if levelHasEnded==true then

timer.performWithDelay(2000, initializeNextLevel)

levelHasEnded = false

end

end

–Start game

InitializeBalls()

Runtime:addEventListener( “touch”, drawLineAndCheckIfBallsAreHit)

timer.performWithDelay(gameLoopSpeed, gameLoop,0)

[/lua]

drawLineAndCheckIfBallsAreHit needs to be declared before you use it. on your function InitializeBalls you are calling it so you can’t declare it after that. you need to declare it before.

so to resolve your problem you just need to add this line on your line 10 (before the function initializeBalls):

local drawLineAndCheckIfBallsAreHit

In your first example, the following happens:

  1. You define local drawLineAndCheckIfBallsAreHit near the top, (as a display group, oddly…) putting this reference in scope for the whole .lua file.

  2. You then define a global drawLineAndCheckIfBallsAreHit function. This is not the same as the local instance already set up, because you did not assign to the existing local. The code works because global functions are accessible from everywhere, but it’s not good practise.

In your second example,

  1. You define a new local drawLineAndCheckIfBallsAreHit. This has the effect of replacing your previously defined reference, now putting it out of scope to those functions defined above it in the listing.

I think what you mean to do is:

[lua]

drawLineAndCheckIfBallsAreHit = function (event)

[/lua]

Here you take the existing local reference, and assign the function to it. Generally I try to order my code so that it only calls functions above it in the listing, but sometimes it’s unavoidable that functions above and below need access to it. In that case this approach solves that problem.

Lua is what’s known as a one-pass compiler. In languages like C, the compiler will read the code multiple times. Typically the first pass scans for variables and function definitions and assigns them memory addresses. The compiler then knows about everything and in the second pass, it compiles the code substituting all the known memory addresses in. This gives the programmer flexibility to define something later in the code towards the bottom and use that definition near the top of the code. This is cool and all because users of multi-pass compilers don’t have to worry about “Scope” as much. The trade off is multiple passes are slow and in scripting languages like Lua you can’t really afford the extra time to make multiple passes.

Because of this any variable or function has to be defined before you use it. We have a tutorial that covers scope:

https://docs.coronalabs.com/tutorial/basics/scope/index.html

Hopefully that will help with the understanding.

Rob

Thanks very much carloscosta, nick_sherman and Rob! I managed to solve my problem and it looks like there were two things going on:

(1) I had overlooked that I had declared drawLineAndCheckIfBallsAreHit as a display.newGroup(). This declaration was a remnant from an earlier approach I had taken and forgotten to delete.

(2) I had overlooked that there is a reference to drawLineAndCheckIfBallsAreHit in the InitializeBalls() function which appears earlier in the code than the drawLineAndCheckIfBallsAreHit.

In order to get the code to work whilst declaring drawLineAndCheckIfBallsAreHit as a local (rather than global) function I’ve deleted the drawLineAndCheckIfBallsAreHit = display.newGroup() declaration in line 9 and moved the InitializeBalls() function down to appear after, rather than before, the drawLineAndCheckIfBallsAreHit function:

display.setStatusBar(display.HiddenStatusBar) local numberOfBallsHit = 0 local ballsHit = {} local ball1Hit = false local ball2Hit = false local levelHasEnded = false local gameLoopSpeed = 200 local linePoints, line = {}, nil local function distanceBetweenTwoPoints( ax, ay, bx, by ) local width, height = bx-ax, by-ay return (width\*width + height\*height)^0.5 end local function drawLineAndCheckIfBallsAreHit(event) if (event.phase == "began") then linePoints = {x=event.x, y=event.y} phaseHasBegun = true return true elseif phaseHasBegun then if (event.phase == "moved") then if (distanceBetweenTwoPoints( linePoints.x, linePoints.y, event.x, event.y ) \> 1) then linePoints = {x=event.x, y=event.y} if (line==nil) then line = display.newLine(event.xStart, event.yStart, linePoints.x, linePoints.y) line:setStrokeColor (1,1,1) line.strokeWidth = 20 else line:append(linePoints.x, linePoints.y) 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 ball1:setFillColor(1,0,0) timer.performWithDelay(10, ball1:removeEventListener("touch", drawLineAndCheckIfBallsAreHit)) ball1Hit = true numberOfBallsHit = numberOfBallsHit +1 table.insert(ballsHit, 1) end if (event.target.name=="ball2" and ball2Hit==false) then ball2:setFillColor(1,0,0) timer.performWithDelay(10, ball2:removeEventListener("touch", drawLineAndCheckIfBallsAreHit)) ball2Hit = true numberOfBallsHit = numberOfBallsHit +1 table.insert(ballsHit, 2) end end end end else -- touch has ended; now remove line so a new line can be drawn phaseHasBegun = false display.remove(line) linePoints, line = {}, nil end return true end return false end local function InitializeBalls() ball1 = display.newCircle(200,100, 30) ball1.name = "ball1" ball1:setFillColor(1,1,1) ball1:addEventListener("touch", drawLineAndCheckIfBallsAreHit) ball2 = display.newCircle(400,300, 30) ball2.name = "ball2" ball2:setFillColor(1,1,1) ball2:addEventListener("touch", drawLineAndCheckIfBallsAreHit) end local function endLevel() timer.performWithDelay(10, Runtime:removeEventListener("touch", drawLineAndCheckIfBallsAreHit)) display.remove(ball1) ball1=nil display.remove(ball2) ball2=nil display.remove(line) end local function initializeNextLevel() numberOfBallsHit = 0 ballsHit = {} ball1Hit = false ball2Hit = false levelHasEnded = false linePoints, line = {}, nil InitializeBalls() Runtime:addEventListener( "touch", drawLineAndCheckIfBallsAreHit ) end local function gameLoop() if numberOfBallsHit==2 and levelHasEnded==false then endLevel() levelHasEnded = true numberOfBallsHit = 0 end if levelHasEnded==true then timer.performWithDelay(2000, initializeNextLevel) levelHasEnded = false end end --Start game InitializeBalls() Runtime:addEventListener( "touch", drawLineAndCheckIfBallsAreHit) timer.performWithDelay(gameLoopSpeed, gameLoop,0)

@nick_sherman

I think what you mean to do is:

 

 

  1.  
  2. drawLineAndCheckIfBallsAreHit = function (event)
  3.  

 

 

Here you take the existing local reference, and assign the function to it. Generally I try to order my code so that it only calls functions above it in the listing, but sometimes it’s unavoidable that functions above and below need access to it. In that case this approach solves that problem.

I, too, was thinking that there might be situations sometimes where functions above and below need access to another function and would love to understand how to solve that problem but I couldn’t get it to work in the way you describe. 

Would you be able to explain this a little more please? Is this perhaps related to doing it the following way, which is an approach I had tried a while ago:

1. local drawLineAndCheckIfBallsAreHit = display.newGroup()

2. function drawLineAndCheckIfBallsAreHit:touch (event)

Would the function in this case still be local and could be accessed by functions above and below it? And would adding “local” to line 2 make any sense?

1. local drawLineAndCheckIfBallsAreHit = display.newGroup()

  1. local function drawLineAndCheckIfBallsAreHit:touch (event)

drawLineAndCheckIfBallsAreHit needs to be declared before you use it. on your function InitializeBalls you are calling it so you can’t declare it after that. you need to declare it before.

so to resolve your problem you just need to add this line on your line 10 (before the function initializeBalls):

local drawLineAndCheckIfBallsAreHit

In your first example, the following happens:

  1. You define local drawLineAndCheckIfBallsAreHit near the top, (as a display group, oddly…) putting this reference in scope for the whole .lua file.

  2. You then define a global drawLineAndCheckIfBallsAreHit function. This is not the same as the local instance already set up, because you did not assign to the existing local. The code works because global functions are accessible from everywhere, but it’s not good practise.

In your second example,

  1. You define a new local drawLineAndCheckIfBallsAreHit. This has the effect of replacing your previously defined reference, now putting it out of scope to those functions defined above it in the listing.

I think what you mean to do is:

[lua]

drawLineAndCheckIfBallsAreHit = function (event)

[/lua]

Here you take the existing local reference, and assign the function to it. Generally I try to order my code so that it only calls functions above it in the listing, but sometimes it’s unavoidable that functions above and below need access to it. In that case this approach solves that problem.

Lua is what’s known as a one-pass compiler. In languages like C, the compiler will read the code multiple times. Typically the first pass scans for variables and function definitions and assigns them memory addresses. The compiler then knows about everything and in the second pass, it compiles the code substituting all the known memory addresses in. This gives the programmer flexibility to define something later in the code towards the bottom and use that definition near the top of the code. This is cool and all because users of multi-pass compilers don’t have to worry about “Scope” as much. The trade off is multiple passes are slow and in scripting languages like Lua you can’t really afford the extra time to make multiple passes.

Because of this any variable or function has to be defined before you use it. We have a tutorial that covers scope:

https://docs.coronalabs.com/tutorial/basics/scope/index.html

Hopefully that will help with the understanding.

Rob

Thanks very much carloscosta, nick_sherman and Rob! I managed to solve my problem and it looks like there were two things going on:

(1) I had overlooked that I had declared drawLineAndCheckIfBallsAreHit as a display.newGroup(). This declaration was a remnant from an earlier approach I had taken and forgotten to delete.

(2) I had overlooked that there is a reference to drawLineAndCheckIfBallsAreHit in the InitializeBalls() function which appears earlier in the code than the drawLineAndCheckIfBallsAreHit.

In order to get the code to work whilst declaring drawLineAndCheckIfBallsAreHit as a local (rather than global) function I’ve deleted the drawLineAndCheckIfBallsAreHit = display.newGroup() declaration in line 9 and moved the InitializeBalls() function down to appear after, rather than before, the drawLineAndCheckIfBallsAreHit function:

display.setStatusBar(display.HiddenStatusBar) local numberOfBallsHit = 0 local ballsHit = {} local ball1Hit = false local ball2Hit = false local levelHasEnded = false local gameLoopSpeed = 200 local linePoints, line = {}, nil local function distanceBetweenTwoPoints( ax, ay, bx, by ) local width, height = bx-ax, by-ay return (width\*width + height\*height)^0.5 end local function drawLineAndCheckIfBallsAreHit(event) if (event.phase == "began") then linePoints = {x=event.x, y=event.y} phaseHasBegun = true return true elseif phaseHasBegun then if (event.phase == "moved") then if (distanceBetweenTwoPoints( linePoints.x, linePoints.y, event.x, event.y ) \> 1) then linePoints = {x=event.x, y=event.y} if (line==nil) then line = display.newLine(event.xStart, event.yStart, linePoints.x, linePoints.y) line:setStrokeColor (1,1,1) line.strokeWidth = 20 else line:append(linePoints.x, linePoints.y) 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 ball1:setFillColor(1,0,0) timer.performWithDelay(10, ball1:removeEventListener("touch", drawLineAndCheckIfBallsAreHit)) ball1Hit = true numberOfBallsHit = numberOfBallsHit +1 table.insert(ballsHit, 1) end if (event.target.name=="ball2" and ball2Hit==false) then ball2:setFillColor(1,0,0) timer.performWithDelay(10, ball2:removeEventListener("touch", drawLineAndCheckIfBallsAreHit)) ball2Hit = true numberOfBallsHit = numberOfBallsHit +1 table.insert(ballsHit, 2) end end end end else -- touch has ended; now remove line so a new line can be drawn phaseHasBegun = false display.remove(line) linePoints, line = {}, nil end return true end return false end local function InitializeBalls() ball1 = display.newCircle(200,100, 30) ball1.name = "ball1" ball1:setFillColor(1,1,1) ball1:addEventListener("touch", drawLineAndCheckIfBallsAreHit) ball2 = display.newCircle(400,300, 30) ball2.name = "ball2" ball2:setFillColor(1,1,1) ball2:addEventListener("touch", drawLineAndCheckIfBallsAreHit) end local function endLevel() timer.performWithDelay(10, Runtime:removeEventListener("touch", drawLineAndCheckIfBallsAreHit)) display.remove(ball1) ball1=nil display.remove(ball2) ball2=nil display.remove(line) end local function initializeNextLevel() numberOfBallsHit = 0 ballsHit = {} ball1Hit = false ball2Hit = false levelHasEnded = false linePoints, line = {}, nil InitializeBalls() Runtime:addEventListener( "touch", drawLineAndCheckIfBallsAreHit ) end local function gameLoop() if numberOfBallsHit==2 and levelHasEnded==false then endLevel() levelHasEnded = true numberOfBallsHit = 0 end if levelHasEnded==true then timer.performWithDelay(2000, initializeNextLevel) levelHasEnded = false end end --Start game InitializeBalls() Runtime:addEventListener( "touch", drawLineAndCheckIfBallsAreHit) timer.performWithDelay(gameLoopSpeed, gameLoop,0)

@nick_sherman

I think what you mean to do is:

 

 

  1.  
  2. drawLineAndCheckIfBallsAreHit = function (event)
  3.  

 

 

Here you take the existing local reference, and assign the function to it. Generally I try to order my code so that it only calls functions above it in the listing, but sometimes it’s unavoidable that functions above and below need access to it. In that case this approach solves that problem.

I, too, was thinking that there might be situations sometimes where functions above and below need access to another function and would love to understand how to solve that problem but I couldn’t get it to work in the way you describe. 

Would you be able to explain this a little more please? Is this perhaps related to doing it the following way, which is an approach I had tried a while ago:

1. local drawLineAndCheckIfBallsAreHit = display.newGroup()

2. function drawLineAndCheckIfBallsAreHit:touch (event)

Would the function in this case still be local and could be accessed by functions above and below it? And would adding “local” to line 2 make any sense?

1. local drawLineAndCheckIfBallsAreHit = display.newGroup()

  1. local function drawLineAndCheckIfBallsAreHit:touch (event)