What to do instead of passing callback in message

Hi there.
I’m using behaviour tree for my objects. One of my task is about moving object from one point to another.
I can do it by sending msg to move_system script.
But in order to complete task in behaviour tree, I need to run task:success() function.
I can’t pass any function in message to move_system, due to engine restrictions.

I have some strange idea, but I don’t like it and I would like to hear your opinion.

-- I can create module callback_manager which will store references to functions
local callback_id = callback_manager:create(function task:success() end)

-- So I can send a message in my behaviour tree like this
msg.post(MOVE_SYSTEM, "move", { callback_id })

-- In move system, when object arrives at destination, it will check if callback_id exists, and then call
callback_manager:call(callback_id)

Another idea is to create state in behaviour tree script instance, to which I will assign callback for task.
So no callback_manager, but move_system will just send message “move_done” to behaviour tree script, and here I will use something like self.task:success()

Generally I don’t like both solutions, I think I am missing something.
Right now I’ve created module with class for my object, where I’m using go.animate in move function.
So in behaviour tree task I just use object:move(position, callback) which works perfectly, however I also don’t like idea that go.animate functions are used in modules, instead of dedicated script like move_system in my opinion. Too much OOP instead of using msg.

I have created three solutions, and I am not really sure which approach is the correct one, so as not to create spaghetti code.

Any advice?

If I can choose between 1 and 2, I would choose 2 although option 1 is also a nice one.
For your option 3, I’m confused. It seems you move your object in the same script without passing the task to your move_system script? If you don’t like using go.animate in modules then just use it in the place you call object:move(…)

This is what I have in third option, there is function, which I put in module (instance of Ship class):

function Ship:sailTo(target_position, success_callback)
	local current_position = go.get_position(self:getDefoldId())
	local distance = vmath.length(target_position - current_position)
	local duration = distance / self:getSpeed()

	msg.post(msg.url(nil, self:getDefoldId(), "sprite"), MSG.PLAY_ANIMATION, { id = hash(textura) })
	go.animate(self:getDefoldId(), "position", go.PLAYBACK_ONCE_FORWARD, target_position, go.EASING_LINEAR, duration, 0, function()
		if success_callback ~= nil then success_callback() end
	end)
end

My behavior tree is also in module, and I create one instance per ship.
RIght now I see that putting functions like above in module is not good option.
Why? Because msg.url() for exampe, will give me different results depening from which script ship:sailTo() is fired.

Maybe I’m doing stupid thing, thinking too much about architecture, but what I created doesn’t seem right, but other options also xD

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
2 Likes

For me, I prefer your third option, you know, I love OOP :smile:
If you got different results, I think it’s not related to having msg.url or go.animate in your object script