Script interaction

I see that messages are the primary way for scripts to communicate. But I foresee this becoming clumsy when you need a lot of interaction and response. But I can also see how this moves away from tight coupling.

I also noticed the module examples store the state of the module in the script. So this still limits interaction between scripts since unless the module had its own state (a singleton) you couldn’t have 2 component scripts on the same object using a module with a shared state between them.

The only shared properties between component scripts I can see is the built in properties of the game object, or any game object properties you manually add. But I don’t think you can add a complex data types as a game object property, they all have to be primitive types that can show up in the inspector, do I have this right?

1 Like

Yes. Modules have their own state that can be shared between scripts. If you set a change a variable in a model required by 2 different scripts, oth of them will receive this change. It’s very useful for creating global variables and functions.

You could also set go.property if you want to share numbers, vectors, and urls, but it’s not good for strings since the hash is encrypted I’d the size goes over 32 bits.

1 Like

Yep I’m aware of how to use a module as a singleton. My questions are an attempt to get a broader sense of how to think about using Defold and designing my games.

The implication SEEMS to be is that script components shouldn’t be referring to each other too directly other than using messages, otherwise I’d expect to see a similar feature as in Unity called “GetComponent”.

Looking at the modules example in the manual Lua modules in Defold, in the first example, the script stores the state of the module, but I have not seen a way to share state with another script component on the same object.

Could you also store a module state this way? In the modules page (linked above) could you store the local my_state as a go.property, eg. self.my_state?

I will keep moving forwards and using messaging for these momentary interactions, perhaps the idea is if I need more complex interaction between scripts and a shared state, they should be a single script?

1 Like

Could you also store a module state this way? In the modules page (linked above) could you store the local my_state as a go.property, eg. self.my_state ?

No. Values set with go.property are limited to a small amount of variable types you can actually set with them. Doing this would be useless and counterintuitive anyways because module states are global and are not local to a single script. They are shared throughout the entire project.

I will explain how to use properties and how to use modules.

I don’t know much about Unity’s “GetComponent”, but I would assume it works similar to go.get, which allows you to get properties from different scripts.

Example:

Script named “main.script” attached to gameobject “/go” and “/go1”:

go.property("speed", 200) --this is outside of any functions, in the script itself because
--its supposed to be set for all instances of this script

Setting this property would allow it to be visible in the collection. The script can refer to its own property through self.speed.
Now, remember, I put “main.script” in 2 different objects. These are 2 different script instances with 2 different instances of the same property. I can now go into, let’s say, “/go#main” (script URL) and change “Speed” to 300. Now in runtime, “go#main”'s “Speed” property is 300 and “go1#main”'s Speed property is still 200. If I go into the script and write this code:

local go_speed = go.get("go#main", "speed")

The number returned will be 300, because we set this specific script’s property to 300 from the default 200.

This is a way to share values between scripts, but as I said, it’s only good for numbers, vectors, and URLs to instances and textures because strings are serialized in hashes and are practically useless.

Now for modules:

The way modules work is that you store everything you want to include inside a table and at the very end of the table, you return the table. This is an example of an empty module:

local globals = {}

return globals

Now if I want to store variables and functions multiple scripts can use, I have to store it inside the globals table. Luckily, you don’t have to put everything inside the brackets.

local globals = {}
globals.food = {}

function globals.addFood(food)
    table.insert(globals.food, food)
end

function globals.eatEverything()
    for i, v in ipairs(globals.food) do
        print("i ate the "..v)
    end
    globals.food = {}
end

return globals

Lets say we have 2 scripts, “/cook#cook” and “/custom#er”. The cook adds the food.

--in script cook.script in object /cook
local globals = require "main.globals" --require the model in every script its used in
--this path will be its path from the project root with each folder separated with periods instead of slashes

function init(self)
    globals.addFood("pizza")
    globals.addFood("tacos")
    globals.addFood("pasta")
end

function update(self, dt)
   if #globals.food == 0 then
        print("hey! you ate all the food! now i must add another so i dont print this line for all eternity.")
        globals.addFood("rotten meat")
    end
end

By calling this function, the cook.script adds to the globals.food table.
Without affecting anything, the “er.script” (inside /custom#er) requires the same script.

--er.script inside of object /custom
local globals = require "main.globals" --dont forget to require!

function update(self, dt)
    if #globals.food == 3 then
        globals.eatEverything()
    end
end

Upon running this code, you will find the output to be “I ate the pizza” “I ate the tacos” “I ate the pasta”. The cook will respond with its little message and add the one food item.

(btw # is the length operator for integer-indexed tables if you didn’t know)
If you go into the update function of any of these scripts and type print(#globals.food), you will see the exact same pattern:

3 --cook adds the food
0 --customer eats it*
1 --cook adds rotten meat
*EDIT: I actually do not know if the pattern will go to 0 then 1 or straight to 1, it depends on whether both things happen in the same frame, which because of the quickness of code, it likely would register the cook adding the food before the next frame. Again, not too sure, but this is irrelevent to my point.

This would not be possible if each script had their own instance of a module, because how would the 2nd script know that the first script added to the food list? How would the 1st script know the 2nd script ate all the food?
This method is very useful and simpler than, for example, sending the same message to multiple scripts. You also won’t have to worry about messaging limitations and you can think of it as a shared variable rather than having to keep track of multiple, locally stored variables and functions.

2 Likes

Again - while I appreciate your time, what you’re describing is like a “singleton” design pattern where modules store their state globally, and I already understand this.

Modules can in fact have their state stored elsewhere, the manual link I provided previously shows you 3 different ways of doing this. It first shows you the way you know, where the state is internal to the module and is “global”. Then it shows 3 ways of storing the module state in individual script components. In those cases, the Module then becomes a way of re-using code throughout the project, where each script component can indeed store their own state to use on the module.

My observation was that while scripts can store their own states for any module, it seems you can’t share this state between scripts components.

I don’t get what you’re saying, “their own module state”? Can you elaborate what you mean by this? I don’t even think scripts have their own module states.

What exactly are you looking for? What is your purpose of doing this? Are you trying to create individual states from modules?

I have described how I deal with this here: What to do instead of passing callback in message - #4 by GooseSwanson

I have implemented something like a generic version of your callback_manager. I use it to send references to tables and functions via message (msg.post will always clone the table argument you give it, and it can’t contain functions).

What most Defold users do is store state (tables, functions, values) inside modules in order to share it between scripts. I try to avoid that as much as possible, since module state is global, and I prefer to tie state lifetime to the lifetime of scripts to ease clean-up and resetting the game.

I also use callbacks instead of the message system for most things so that I can run callback code without delay.

This is what I do in code:

-- registry.lua --
-- This is my generic callback_manager.
M.counter = 0
M.entries = {}

function M.add(entry)
    local id = M.counter + 1
    M.counter = id
    M.entries[id] = entry
    return id
end

function M.remove(id)
    local entry = M.entries[id]
    M.entries[id] = nil
    return entry
end

function M.get(id)
    return M.entries[id]
end

-- main.script --
-- Here I create state that I want to share with via the registry.
local appstate = {
    event_system = event_system.create(),
    game_world = game_world.create(),
}

-- Add appstate to the registry and send its id to another script.
local appstate_id = registry.add(appstate)
msg.post('/game_presenter', 'start', {appstate_id = appstate_id})

-- Later on when I'm done with appstate, maybe in the final function, I remove it from registry.
registry.remove(appstate_id)

-- game_presenter.script --
-- The other script receives the id and retrieves the appstate.
function on_message(self, message_id, message)
    if message_id == hash('start') then
        self.appstate = registry.get(message.appstate_id)
        -- Through appstate, I can now share state and invoke callbacks added to appstate.
        self.appstate.event_system:add_callback('my_event_type', function(event) print(event) end)
        self.appstate.event_system:send_event('my_event_type', {'my data table'})

        -- There's nothing preventing me from adding functions to the registry instead of tables.
        -- I do this in one place to get a callback from another script when its final function runs.
        local func_id = registry.get(function(...) print(...) end)
        registry.remove(func_id)("remove and call the callback")
    end
end

This allows me to have no stateful/singleton modules. The only except is the registry module mentioned in the post above.

Essentially, I have one main script that sets up core functionality (my own systems for events, callbacks, input, and a scheduler), and then creates the presentation/view via a collection factory.

I use a model-view-presenter architecture. In the presentation collection, each script during init sets up a view and a presenter table and posts a message to the main script letting it know that a new presenter was created. The main script then responds to the sender with a reference to the app state (that holds the event system table etc.), which the script uses to subscribe to events and post input commands.

The reason for sending two messages like this is that there’s no way for the main script, when creating the presentation collection, to know the URL of any scripts within it. Instead, those scripts need to let the main script know they exist.

--- main script ---
local registry = require 'registry'

function init(self)
	local as = {...} -- 'as' is short for 'application state'
	self.as_id = registry.add(as)
	-- ...populate as with references to core systems
end

function on_message(self, id, t, sender)
	if id == hash('init_presenter') then
		msg.post(sender, 'init_args', {
			as_id = self.as_id,
		})
	end
end

--- presentation script: monster_horde.script ---
local pres = require 'pres' -- module for managing presenters
local presenter = require 'monster_presenter'

function init(self)
	msg.post(msg.url('/main#script'), 'init_presenter')
	self.view = {
		-- set up references to go stuff for use in the presenter
		url = msg.url('#'),
		monster_parent_url = msg.url('.'),
		monster_factory_url = msg.url('#monster_factory'),
	}
end

function on_message(self, id, t)
	if id == hash('init_args') then
		-- retrieve the application state table
		self.as = registry.get(t.as_id)

		-- create the presenter, passing it 1) the as table so that
		-- it can send input commands to the game logic, and 2) the
		-- view so that it can create monsters and update their go
		-- position, animation etc.
		self.presenter = presenter.create(self.as, self.view)

		-- subscribe the presenter (module + instance/context) to
		-- events and callbacks
		pres.add_presenter(self.as.pres, presenter, self.presenter)
	end
end
3 Likes

Thank you for sharing this, I’m with you on not tying up too much in global state, I’ve saved your post to experiment with when I feel like messages aren’t the right choice anymore. The more I learn about LUA the more confident I am that it’s so flexible that I don’t have to worry too much about having to work in a specific way if it comes to it.

Though I was kind of hoping one of the Defold creators would have the time to weigh in - I’m trying to do things in the way the engine and manual seem to suggest - and from what I can tell, it seems to suggest you shouldn’t be having too much direct communication between components. I hope I’m getting the right message.

I believe that’s the idea — hard to say what the original vision was back in 2009 that set this architecture, and how best practice for using Defold has evolved since then.

Off the top of my head there are two good reasons to not have direct communication (or read/write dependencies):

  • You can’t control in which order different script’s init and update are called, making it troublesome to have one component depend on output of another (during the same frame).
  • Code is executed in the context of the script or gui_script that got the original call, determining what e.g. go.get_position() produces. This can get confusing If you have a lot of callbacks that are conceptually associated with one script/GO but called in another GO’s context.

Still, if you have a complex simulation with many update steps and data dependencies, it might get complicated to design that primarily with messages (I haven’t attempted it). Personally, I prefer having as much logic as I can in my own game loop and modules that I fully manage, and have scripts’ responsibility be to manage Defold engine objects.

4 Likes

Thanks again. I have previously learned my lesson from trying to do too much myself and skipping over existing functionality that would have made my life easier, if I only gave it a look in and tried to understand it… so it’s reassuring to know other Defold users are already fully managing their own logic in regards to scripts.

1 Like