I think the easiest way to understand variables in Defold is to stop thinking about where they are written in the file, and instead think about who should own the value and how long it should live.
There are basically a few common cases:
local inside a function
Use this for temporary values that are only needed while that function is running.
function update(self, dt)
local distance = self.speed * dt
local position = go.get_position()
position.x = position.x + distance
go.set_position(position)
end
If distance and position don’t need to exist after update(), there is no reason to put them in self, so you can make them local to the scope. On the other hand speed might be adjusted in e.g. on_input, so it’s convenient to put it in self, and it’s available for all the Defold lifecycle functions in the script (and other functions that take self table as parameter - as it’s a simple Lua table like any other).
As a rule of thumb: prefer local unless the value needs to be available between function calls.
Top-level local
This one is important because it behaves differently from self (explained below). This is also sometimes a good optimization for variables used many times in huge loops in functions (rarely a really important for performance case, unless you’re making really huge loops) - in the above example we could declare local distance or position outside the update():
local distance = 0
local position
function update(self, dt)
distance = self.speed * dt
position = go.get_position()
position.x = position.x + distance
go.set_position(position)
end
so it doesn’t need to be created every time update() is called. But this also changes the scope of this variables. These are now available and could be modified in any other function in the script!
Moreover, it is shared by all instances using that script file!
This means, if you use the same script for two instances, e.g. two enemies - they will be overriding each other’s distance, which might cause unexpected behaviours. We say such a variable has a mutable state.
So this can be great for things that are effectively constants:
local MAX_ACCELERATION = 20
local MSG_DAMAGE = hash("damage")
local UP = vmath.vector3(0, 1, 0)
or for… helper functions (because functions can be assigned to variables in Lua in the same way as numbers or strings):
local function clamp_speed(speed, min_speed, max_speed)
return math.max(min_speed, math.min(speed, max_speed))
end
So if you want to have a variable that is only bound to a certain instance of the script (e.g. for per-enemy health, distance, etc) you want:
self.current_health = 100
self.something
Use self for state belonging to this particular script instance.
For example:
function init(self)
self.current_speed = 50
self.is_moving = false
end
function update(self, dt)
if self.is_moving then
self.current_speed = self.current_speed + dt
end
end
current_speed should be available and persistent from one frame to the next, so it should be in self.
Each instance using this script gets its own value.
So if you have 20 enemies using the same script:
self.health
means each enemy can have different health.
This is probably the closest equivalent to what you might think of as an instance/member variable in C#, GDScript, etc.
go.property()
I would think of these more as configuration for the script instance.
For example:
go.property("min_speed", 0)
go.property("max_speed", 100)
Defold then exposes these in the editor, lets you set different values on different instances, pass them when spawning objects from factories, access them externally, etc.
You noticed correctly all properties are then available in the self table, so they share the same characteristics as self variables described above - they are belonging to the instance of the script.
Inside the script they ca be accessed through self:
print(self.min_speed)
print(self.max_speed)
The fact that go.property() has to be used outside init() (or any other function) is only just because of how Defold declares these properties. I wouldn’t worry too much about the visual separation in the file, but it’s good to just put them always at the top of the script.
Globals
Something like:
current_speed = 50
without local or self creates a global variable. This means this variable is available not only in this script, but also in any other scripts. So if you declare a global in hero.script it will also be available in enemy.script.
I would generally avoid that. Globals are available much more widely than you probably intend and make it very easy for unrelated code to accidentally depend on or modify them. You might think it’s good for having them for constants that are really contants, immutable and it would be ok, but it’s always better to control the scope - why having a variable in memory that no script needs at the moment?
If you genuinely need shared game state, I would usually make that explicit with a Lua module rather than relying on globals. It’s better to put constants in separate Lua modules and require them where needed.
For your particular example, I would probably just write something like:
go.property("min_speed", 0)
go.property("max_speed", 100)
function init(self)
self.current_speed = 50
end
and then:
function whatever(self)
self.current_speed = math.max(
self.min_speed,
math.min(self.current_speed, self.max_speed)
)
end
That might look slightly less “grouped” than:
self.speed_stats = {
min = self.min_speed,
max = self.max_speed,
cur = 50
}
but there is an important difference.
With your version:
self.speed_stats.min = self.min_speed
you are copying the value.
After that these are two independent values:
self.min_speed
self.speed_stats.min
Changing one will not automatically change the other. That creates two potential “sources of truth”, which can become confusing later. If you really like grouping related runtime state, that’s completely reasonable:
function init(self)
self.speed = {
current = 50,
acceleration = 0
}
end
and keeping configuration as properties:
go.property("min_speed", 0)
go.property("max_speed", 100)
Then the distinction is actually quite useful:
self.min_speed -- configuration
self.max_speed -- configuration
self.speed.current -- runtime state
self.speed.acceleration
Personally, though, for only one or two values I wouldn’t create a table just for grouping. self.current_speed is perfectly normal, regular Defold code 
Beside go.properties specific to Defold, all this is really only Lua 