I understand the difference between the scope of local and global variables, but I am confused about the difference and effects of declaring a variable OUTSIDE of Lua functions (or LOVE2D core callback functions). Consider these three code snippets:
declare function as global, outside of all functions
var = 30
function f1()
--
end
function f2()
print(var)
end
f1()
f2()
declare function as local, outside of all functions
local var = 30 function f1() -- end function f2() print(var) end f1() f2()
function f1() var = 30 end function f2() print(var) end f1() f2()
All three of these have the same result. I know that all global variables are stored in the global environment table (accessible via _G), and that official Lua documentation discourages the use of global variables:
It is good programming style to use local variables whenever possible. Local variables help you avoid cluttering the global environment with unnecessary names. Moreover, the access to local variables is faster than to global ones. (from https://www.lua.org/pil/4.2.html)
So, what is the difference between these three and which is the best one to use?
I am specifically looking at LOVE2D, although their core callback functions act like normal functions. I guess my question is, if I want to declare a Player (table), where should I put Player = {} (either at the top of main.lua or inside love.load) and if I should declare it 'local' (if at the top of main.lua)?
One good reason to declare a variable local outside of function scope is that your file is used as a module, and you don't want to pollute the global variables table with unnecessary entries. for example:
-- file: foo.lua
local function hello()
return 'Hello World'
end
local self = {
['hello'] = hello
}
return self
Note that both self and hello are local symbols, so they don't overwrite any existing global variables.
-- file: main.lua
self = 'Lorem Ipsum'
local hello = require('foo')
print(hello.hello())
print(self) -- this prints the global self, 'Lorem Ipsum'
Now, if self in foo.lua was local, the last line wouldn't print a string, but the table defined in foo.lua. But instead, we get the output we expect:
$ lua main.lua
Hello World
Lorem Ipsum
As a more general programming practice, you should always be aiming to limit variables to the least visible scope possible. Not just in a global vs. non-global sense, but in all scopes (e.g., if a variable only needs to be visible to a certain block, make it so). This helps manage memory (by avoiding leaving references hanging around longer than needed) and eliminates a whole host of problems associated with naming conflicts. This practice makes debugging much easier.
Locals are (see: should be) faster in Lua, but you would have to profile that to see if it is meaningful to you.
Aside from providing access to program arguments, love.load is a context in which you can be absolutely certain other love.* functions and properties will work (i.e., the module will be fully loaded).
Note: You can see the default calling order of the callback functions in love.run (the main callback function).
If the data you are initializing does not rely on the love module context, there is no need for it to be done so in the love.load callback.
Best practices would be to use local as often as possible, and initialize data where it is most appropriate to do so.
--[[
`player` is local to our file, it does not need to be visible outside this scope
it contains a couple of integers right now, these don't rely on `love` in any way
so can be safely initialized right here
]]
local player = {
health = 100,
gold = 0,
}
function love.load(args)
--[[ here we can be certain the `window` and `graphics` modules are up and running ]]
local w, h = love.window.getMode()
player.sprite = love.graphics.newImage('sprite.jpg')
player.position = { x = w / 2, y = h / 2 }
end
function love.draw()
love.graphics.draw(player.sprite, player.position.x, player.position.y)
love.graphics.print(('health: %d, gold: %d'):format(player.health, player.gold),
player.position.x, player.position.y + 65)
end
This last point is difficult, as the most appropriate place to initialize data will change with the structure of your program. You will find that the cursory example above only remains relevant for so long before this structure of program becomes unwieldy. You'll almost certainly gravitate towards a more object-oriented style, where the entities in your game become instances of classes, with their own initializers, the definitions of which live in their own files, and are acquired with require.
Once you have multiple files, it becomes even more important to not pollute the global namespace.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With