Event bus


#1

Hi there!

I’m really new to Defold engine so forgive me if my question is too basic (I just didn’t find anything about it).

I’m trying to create a very decoupled code for a series of games and what I’m looking for is a something like the message bus pattern where I can register for a message and other objects (more than one) can listen to it without any one being aware of others (except, of course, of the message bus itself).

Is this possible with Defold? Is that a plugin or any resource that I can use?


#2

No, there is nothing like that in Defold. Messages are sent to specific recipients. You can implement what you want in Defold though. It will be Lua code running so take care not to break performance. Iterating over large tables with messages, listeners and whatnot each frame will eat some performance.


#3

I created a nifty one that we are using heavily in my company and in our products.
Not super documented but I think you will understand it.
Here you go:

local M = {}
local register = hash("register")
local unregister = hash("unregister")
local unregister_all = hash("unregister_all")
local debug_time = require "utils.debug_time"
local context = "msg_proxy"

local msg_id = {}
local msg_group = {}
local msg_both = {}

local long_storage_buffer = 50
local long_storage = {}
for i = 1,long_storage_buffer do
	table.insert(long_storage, { fetched = true, msg = nil, msg_id = nil })
end

local key_ix = 0

--- Post message to all registered listeners
-- @param tbl Table to store for receivers
-- @return table with message_key
function M.create_long_message(tbl, message_id)
	key_ix = key_ix%long_storage_buffer + 1
	local buffer_tbl = long_storage[key_ix]
	if buffer_tbl.fetched == false then
		-- note: this  must NOT use log instead of print because log uses long messages -> stack overflow
		print("No script retrieved long message: [" .. key_ix .."] [" .. tostring(buffer_tbl.msg_id) .. "], will be overwritten by circular buffer")
	end
	buffer_tbl.msg = tbl
	buffer_tbl.msg_id = message_id
	buffer_tbl.fetched = false
	return { message_key = key_ix }
end

function M.get_message(key, message_id)
	if type(key) == "table" then key = message.message_key end
	if type(message_id) == "string" then message_id = hash(message_id) end
	local tbl = long_storage[key]
	if message_id and tbl.msg_id ~= message_id then
		print("found wrong message type in long message storage (requested:" .. tostring(message_id) ..", found:" .. tostring(tbl.msg_id) .. ")")
	end
	tbl.fetched = true
	return tbl.msg
end

--- Post message to all registered listeners
-- @param message_id
-- @param message
-- @param group Optional
-- @param long Optional If message is too long to be sent by msg.post a table_storage key is sent instead.
function M.post(message_id, message, group, long)
	if type(message_id) == "string" then message_id = hash(message_id) end
	message = message or {}
	if long then 
		message = M.create_long_message(message, message_id)
	end	
	local lists
	-- post to group
	if group then
		lists = msg_group[group]
		if lists ~= nil then
			for k,v in pairs(lists) do
				msg.post(v,message_id,message)
			end
		end
		-- post to both
		local tbl = msg_both[message_id]
		if tbl ~= nil then
			local lists = tbl[group]
			if lists ~= nil then
				for k,v in pairs(lists) do
					msg.post(v,message_id,message)
				end
			end
		end
	end

	-- post to id_msg_id
	lists = msg_id[message_id]
	if lists then
		for k,v in pairs(lists) do
			msg.post(v,message_id,message)
		end
	end
end

function M.register(mi, group)
	local sender = msg.url()
	-- hash_to_hex(sender.socket) ..
	local readable =  hash_to_hex(sender.path) .. hash_to_hex(sender.fragment or hash(""))
	if mi and group then
		if type(mi) == "string" then mi = hash(mi) end
		if msg_both[mi] == nil then msg_both[mi] = {} end
		local tbl = msg_both[mi]
		if tbl[group] == nil then tbl[group] = {} end
		tbl[group][readable] = sender
	elseif mi then
		local str
		if type(mi) == "string" then 
			str = mi
			mi = hash(str) 
		end
		if msg_id[mi] == nil then msg_id[mi] = {} end
		if msg_id[mi][readable] then 
			log.w(string.format("Already registered message %s to %s",tostring(mi),tostring(sender)),context) 
			return
		end
		msg_id[mi][readable] = sender
	elseif group then
		if msg_group[group] == nil then msg_group[group] = {} end
		msg_group[group][readable] = sender
	end
end

function M.unregister(mi, group)
	local sender = msg.url()
	--hash_to_hex(sender.socket) ..
	local readable =  hash_to_hex(sender.path) .. hash_to_hex(sender.fragment or hash(""))
	if mi and group then
		if type(mi) == "string" then mi = hash(mi) end
		local tbl = msg_both[mi]
		if tbl == nil then
			log.i("Cant unregister a message_id that doesn't exist: " .. mi, context)
		elseif tbl[group] == nil then
			log.i("Cant unregister a group that doesn't exist: " .. group, context)
		else
			tbl[group][readable] = nil
		end
	elseif mi then
		if type(mi) == "string" then mi = hash(mi) end

		if msg_id[mi] == nil then
			log.i("Cant unregister a message_id that doesn't exist: " .. mi, context)
		else 
			msg_id[mi][readable]=nil
		end
	elseif group then
		if msg_group[group] == nil then
			log.i("Cant unregister a group that doesn't exist: " .. group, context)
		else 
			msg_group[group][readable]=nil
		end
	else
		error("msg_proxy.script needs message_id or msg_group in message when registering")
	end
end

function M.has_listener(message_id, group)
	if type(message_id) == "string" then message_id = hash(message_id) end
	local lists
	if group then
		lists = msg_group[group]
	else
		lists = msg_id[message_id]
	end
	if lists == nil or next(lists) == nil then return false end
	return true
end

-- Lazy version of destroying all listeners for a script. Will parse through all tables.
-- Use M.unregister if performance is important
function M.destroy()
	local sender = msg.url()
	--hash_to_hex(sender.socket) ..
	local readable =  hash_to_hex(sender.path) .. hash_to_hex(sender.fragment or hash(""))

	-- id
	for k,v in pairs(msg_id) do
		for kk,vv in pairs(v) do
			if kk == readable then
				msg_id[k][kk] = nil
			end
		end
	end

	-- group
	for k,v in pairs(msg_group) do
		for kk,vv in pairs(v) do
			if kk == readable then
				msg_id[k][kk] = nil
			end
		end
	end

	-- both
	for k,v in pairs(msg_both) do
		for kk,vv in pairs(v) do
			for kkk,vvv in pairs(vv) do
				if kkk == readable then
					msg_both[k][kk][kkk] = nil
				end
			end
		end
	end
end

function M.final()
	local sender = msg.url()
	for k,tbl in pairs(msg_id) do
		for kk,vv in pairs(tbl) do
			if kk == sender then
				tbl[kk] = nil
			end
		end
	end
	for k,tbl in pairs(msg_group) do
		for kk,vv in pairs(tbl) do
			if kk == sender then
				tbl[kk] = nil
			end
		end
	end
end


return M


#4

Oops, it contains calls to log (one of the very few functions we actual think is worth putting in global scope). Maybe create your own or replace those lines with prints?


#5
-- notification_center.lua

-- localization
local defold = _G
local next = next

-- functions
local add_observer
local remove_observer
local post_notification
local execute_in_context

local dispatch_table = {}

function add_observer (observer, notification, callback)
	local observers = dispatch_table[notification]
	if not observers then
		observers = {}
		dispatch_table[notification] = observers
	end
	observers[observer] = callback
end

function remove_observer (observer, notification)
	if notification then
		local observers = dispatch_table[notification]
		if observers then
			observers[observer] = nil
		end
	else
		for _, observers in next, dispatch_table do
			observers[observer] = nil
		end
	end
end

function post_notification (notification, sender, payload)
	local observers = dispatch_table[notification]
	if not observers then return end

	for observer, callback in next, observers do
		execute_in_context(observer, callback, sender, payload)
	end
end

function execute_in_context(context, fn, ...)
	local current = defold.__dm_script_instance__
	defold.__dm_script_instance__ = context
	fn(context, ...)
	defold.__dm_script_instance__ = current
end

-- export
return {
	add_observer = add_observer,
	remove_observer = remove_observer,
	post_notification = post_notification,
}

Usage:

local nc = require("notification_center")

-- init & final in some scripts
nc.add_observer (self, "say_hi", function ()
	print("hi!")
end)

nc.remove_observer (self)

-- in other scripts
nc.post_notification ("say_hi")

#6

What’s the difference between log.w and log.i ? warning and information?

I can write and upload an alternative, I have something close to this already with err. Or maybe set a day in the future to release some of these on your company github as importable libs?


#7

I have a log module started now.

I wasn’t sure what you were using the context string for so I guessed it might be for a tag whitelist.

Example of output

[CRIT  2018-06-11 23:40:23] /example/example.script:14: Does this work?
[CRIT  2018-06-11 23:40:23] /example/example.script:17: Does this still work?
[INFO  2018-06-11 23:40:32] /example/example.script:10: hello2

It supports filtering log levels, and by context tags so you can select what kinds of information you get saved to log files. Log lines automatically include the log level, timestamp, and the script and line number where the logging occurred along with whatever custom message you want.

How much different is this to your version of log?


#8

Yes it’s quite similar.
I log INFO, DEBUG, WARNING and ERROR as you guessed.
Context is quite a big thing for us in the projects as they are just that, a string telling the context of the script/module. eg. main_menu, top_bar, user_data, loader …etc…
Then I can use that in logs so we can see what script is logging and also use that to enable/disable them so it doesn’t get so noisy (eg. I want to only see logs from gamesparks communication and user module that takes care of responses).
Context is also used in localisation helping out to understand the context of this particular OK-button. Also in my screen manager and gui handlers… we use them a lot :slight_smile:


#9

Also here https://github.com/britzl/ludobits is a broadcast example from @britzl