After some weeks of working in the Defold engine I really appreciate the loosely coupled and the power of the messaging system. Now I think of another high-level system that would be nice to have in the engine. The Observer or Notification Center.
I want to observe property of a game object and then get notified when it has changed.
local function hero_hp_changed(self, url, id, new_value)
-- Do something when hero's hp has changed
end
function init(self)
go.observe("hero#script", "hp", hero_hp_changed)
end
I also want gui to be able to observe game object. And right now the .gui_script cannot access ‘go’ api. so, maybe another api instead of ‘go’ such as ‘obs.observe’ that can be called everywhere like ‘msg.post’
You could quite easily add a data binding/observer like pattern to values in a table through the use of meta tables, but when it comes to properties on a game object than we need new functionality similar to what you have suggested.
You could build something that is not fully automated, but at least fairly easy to use:
observable.lua:
local M = {}
local observables = {}
--- Create an observable value
-- @param name Unique name of the value to observer to create
-- @param message_id Message id to post to observers when the value changes
function M.create(name, message_id)
observables[name] = observables[name] or { message_id = message_id, observers = {} }
end
--- Observe changes to a previously created observable value. When the value
-- changes a message will be posted to the url of the script that called this
-- function.
-- @param name Name of the value to observe. Must have been created with @{create}
function M.observe(name)
assert(observables[name], "You must create an observable before you can observe it")
table.insert(observables[name], msg.url())
end
--- Notify observers of a change to a previously created observable value
-- @param name Unique name of the value that changed
-- @param data Data to pass to all observers
function M.notify(name, data)
assert(observables[name], "You must create an observable before you can use it")
data = date or {}
local message_id = observables[name].message_id
for _,observer_url in pairs(observables[name].observers) do
msg.post(observer_url, message_id, data)
end
end
return M
hero.script:
local observable = require("observable")
local function change_hp(self, new_hp)
self.hp = new_hp
observable.notify("hp", { hp = new_hp })
end
function init(self)
self.hp = 10
observable.create("hp", hash("hp_changes"))
end
hp_bar.gui_script:
local observable = require("observable")
function init(self)
observable.observe("hp")
end
function on_message(self, message_id, message, sender)
if message_id == hash("hp_changed") then
print("new hp", message.hp)
end
end
local Observer = {}
local observers_by_msg = {}
local function get_key(url, message)
url = type(url) == 'string' and msg.url(url) or url
local game_obj_id = hash_to_hex(url.path)
local msg_id = hash_to_hex(type(message) == 'string' and hash(message) or message)
return hash(game_obj_id .. msg_id)
end
local function find(t, e)
for i, v in ipairs(t) do
if v == e then return i end
end
return 0
end
function Observer.register(url, message)
local key = get_key(url, message)
observers_by_msg[key] = observers_by_msg[key] or {}
table.insert(observers_by_msg[key], msg.url())
end
function Observer.deregister(url, message)
local key = get_key(url, message)
local obs = observers_by_msg[key]
local ob = msg.url()
assert(obs, 'message not registered: ' .. message)
local id = find(obs, ob)
assert(id > 0, 'message not registered: ' .. message)
table.remove(obs, id)
if #obs <= 0 then
observers_by_msg[key] = nil
end
end
function Observer.notify(message, data)
local key = get_key(msg.url(), message)
local obs = observers_by_msg[key]
if obs then
for _, ob in pairs(obs) do
msg.post(ob, message, data)
end
end
end
return Observer
hero.script
local Observer = require('observer')
local function change_hp(self, new_hp)
self.hp = new_hp
Observer.notify('change_hp', {hp = new_hp})
end
game.gui_script:
local Observer = require('observer')
function init(self)
observable.register('/hero', 'change_hp')
end
function final(self)
observable.deregister('/hero', 'change_hp')
end
function on_message(self, message_id, message, sender)
if message_id == hash('hp_changed') then
print('new hp', message.hp)
end
end