Property Observer

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’

2 Likes

Maybe property that can be observed have to be declared with ‘go.property()’. But that is your decision :slight_smile:

1 Like

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
1 Like

The observable module is awesome! Now my game utilizes several modules from you and other Defold team members that are posted in this forum :stuck_out_tongue_winking_eye:

Still looking for Defold’s api to observe properties on a game object that will work nicely with go.property() and go.animate() :smile:

Thank you

May be this can help :slight_smile:

observer.lua

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
1 Like