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