Making sense of variables

Having just started with Lua and Defold, coming from other engines like Unity and Godot, one of the first things that really confused me was how Defold and Lua handle variables.

Mainly, I’m referring to the fact that variables often need to be defined in init() rather than being organized at the top of a script, as I might be used to from other languages/engines. Then there are concepts like local, self, and so on.

One thing that really confused me at the beginning was how I seemed to be forced to “separate” variables that are part of the same piece of logic depending on whether I wanted them to be exposed in the editor or kept internal to the script.

For example, let’s say I have:

  • min_speed

  • max_speed

  • current_speed

In another engine, I’d probably want min_speed and max_speed to be exposed in the editor so I can easily tweak them, while current_speed would remain internal to the script since there’s no real benefit in exposing it.

If I understand correctly, in Defold I could achieve this by using go.property() for the first two, while defining the third as a self variable in init().

However, the idea of having to “split” these three variables between the top of the script and init(), even though they are all part of the same logic, feels a little strange to me.

So I kept researching and experimenting, and I ended up wondering whether a structure like this might make sense.

Using the same example, something along these lines:

go.property("min_speed", 0)
go.property("max_speed", 100)

function init(self)
    self.speed_stats = {
        min = self.min_speed,
        max = self.max_speed,
        cur = 50
    }
end

function whatever(self)
    local speed = self.speed_stats

    speed.min = whatever
    speed.max = whatever
    speed.cur = whatever
end

This way, all the speed-related variables are grouped together, while max_speed can still be exposed in the editor.

I also like that, inside other functions, I can do:

local speed = self.speed_stats

and then work with speed.min, speed.max, etc., rather than having to repeatedly write self.variable.

So I’m wondering: is this a reasonable/idiomatic way to structure variables in Defold? Is this something commonly done, or would there be a better approach that I’m missing?

I’m still getting used to the way Defold/Lua handles state and variables, so I’d really appreciate some insight into how more experienced Defold developers usually approach this.

Thanks for helping out! :blush:

1 Like

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 :smiley:

Beside go.properties specific to Defold, all this is really only Lua :wink:

4 Likes

Yes, I think I have a basic understanding of how local and self work.

That said, I still find the way variables can end up being spread throughout a Defold script, with different variables being declared in different functions, quite messy and… unpleasant, at least for my brain. :sweat_smile:

For example, I imagine having a very long script with a lot of functions, and each function has its own local variables. If I ever need to find a specific variable, I might have to go through the entire script, function by function, looking at all the variables declared in each one, unless I remember exactly where it is.

That’s why it is quite important for me to have some kind of clearly identifiable section where I can see the variables/state of a particular system in one place. It makes the code much easier for me to scan and understand later.

That’s actually why I spent way too much time on this yesterday… I was up until 4 AM reading about Lua/Defold variables and experimenting with different ways of structuring them. :sweat_smile:

And I think this is also where I may not have explained my original question very well.

What I was specifically trying to achieve was to keep all the variables belonging to the same piece of logic together, even when some of them are exposed as go.property() and others are internal runtime state.

So, using the same example, I was thinking about something like:

go.property("min_speed", 0)
go.property("max_speed", 100)

function init(self)
    self.speed = {
        min = self.min_speed,
        max = self.max_speed,
        current = 50
    }
end

The idea is that min, max, and current are conceptually all part of the same speed state, so I would then use:

self.speed.min
self.speed.max
self.speed.current

throughout the rest of the script.

And then, inside a function where I’m working extensively with that state, I would simply do:

function whatever(self)
    local speed = self.speed

    speed.min = ...
    speed.max = ...
    speed.current = ...
end

So I’m actually asking about both of these things:

  1. Is it reasonable to group related state into a table like self.speed, even if min and max initially come from go.property()?

  2. Is it also reasonable/common practice to create a local reference such as local speed = self.speed inside a function, so I don’t have to repeatedly write self.speed.current, self.speed.min, etc.?

  3. , as a “bonus” question: are there any issues that would be created, in the short/long run by this approach?

I understand your point that:

self.speed.max = self.max_speed

creates a copy, meaning self.speed.max and self.max_speed become two separate values. My intention would be that the go.property() values are only used to initialize the runtime table, and then self.speed becomes the actual runtime state that I work with.

So the main thing I’m trying to figure out is whether this overall approach is considered reasonable/idiomatic in Defold, or whether I’m introducing some kind of problem or bad practice by doing it this way.

I’m not really trying to avoid typing self just for the sake of avoiding a few characters. It’s more about organization: I want related state to live together, and then I want to be able to locally reference that state when working with it inside a particular function.

Thanks for explaining the fundamentals of variables in Defold, that definitely helped. I’m just still trying to get an answer to the specific organizational approach I’m proposing. :grinning_face_with_smiling_eyes:

1 Like

Doing what you propose is totally fine.
Instancing a local variable to shorten variable lookup path is handy and usefull, it even speed up the lookup of values since there is less tables to lookup.

For go.property() you should just be aware that they can be animated via go.animate(), this can be handy for example for a color property or a things like that. So if you copy its value, you wont have this live-link, so you’ll have somewhere (in the update for ex.) reset self.speed.current_speed = self.animated_speed to synchronize things.

Personnally I dont use these much, for me go.property() is just an interface for the editor and for factories. This point is crucial tho:
If you want to be able to configure your instance at object creation with factories, you have to set them as go.property().
Another way is to not use that, but use a message to configure the object, like:


function on_message(self, message_id, message)
 If message_id == hash('configure') then 
    Self.speed = message.min_speed
End
End 

Then when spawning the object, you

Local id = factory.create(my_object_factory)
msg.post(id, hash('configure'), {min_speed=100})

I tend to write only lua modules, like player.lua, then the player.script is just the glue to wire script init/update/message etc. functions and go.properties() to my module.
So I consider go.properties() as arguments to my module instance construtor.

Like this everything is lua first code, and you can grow your code base without linking it to .script.
For example you can have only one enemies_manager.script that just require a bunch of lua modules for all enemies prefabs, a generic enemy.go and enemy.script, and just spawn and configure.

2 Likes

Hey, thank you for jumping in!

That’s actually very helpful, especially knowing that the approach I was thinking about is totally fine. The part about creating a local reference to shorten the lookup path is exactly what I was getting at with local speed = self.speed. :blush:

I still haven’t gone as far as understanding what you mean when you talk about messages and modules, though. I’m really a total beginner with Lua and Defold right now. :sweat_smile:

I do know that Defold uses messages to let different elements communicate with each other, but I haven’t reached the point where I can really make sense of how and when I should use them. Same thing with Lua modules: that’s probably a bit further down the rabbit hole for me. :grinning_face_with_smiling_eyes:

The go.animate() part is really interesting, though! I hadn’t considered that a go.property() could be animated and that copying its value into another table would break that live connection. That’s definitely something I’ll keep in mind.

For now, I think I’ll stick to the simpler approach and experiment with the concepts one at a time. There’s already quite a lot to digest coming from Unity/Godot. :joy:

Thanks again for the explanation!

2 Likes

You’re welcome!
As for the module part, you can absolutely start with creating a game object and an associated small .script for everyone of your objects/prefabs, and just use them as is in your collections. This is actually even a better idea to start this way as everything is simple and statically defined in your project, it will be easyer to think about your project this ways.
I recommand you to create a folder for each type of object you create, with everything inside (gameobject, script) so things are organised functionaly and you find things back. A good way to find stuff is also just ctrl+O (ctrl+p in vscode) and type name of file to open it.
Or ctl+shift+f to just search into files.

One last thing:
I strongly recommand to get used to Lua ANNOTATIONS quickly, and defines them early for your objects, as it really helps to avoid dumb errors or typos. It provides also autocomplete for fields.

See Lua Language Server | Wiki

What I do even in small scripts is to annotate a class at start of script with name MyscriptData and add all fields that should be in the self object:

-- player.script

---@class PlayerData
---@field health number 
---@field current_speed number current speed 
---@field velocity vector3 velocity vector
---@field sprite url url to the sprite component

---@param PlayerData
local function my_helper(self)
  --self will auto complete
end

---@param self PlayerData
function init(self)
  self.health = 100
 Self.current_speed = 10
  self.velocity = vmath.vector3(1,0,0) * self.current_speed
  self.sprite = msg.url('#sprite')
end

---@param self PlayerData
---@param dt number
function update(self, dt)
 -- here self will autocomplete
my_helper(self)
end

You’ll get linter and lua check information in the “Problem” console when building, or live in VSCode for example

1 Like