Is there a way to observe an value change on table?

Let’s say I have an object like:

local goblin = { health = 100 }

Is there an way to, when the goblin’s health get below an certain point, it fires a signal? Maybe something like this:

goblin.onLowHealth = Signal.new()
observer.observe(goblin.health, function(health) -- fired when
    local lowHealth = health < 30 -- value changes
    if not lowHealth then return end
    goblin.onLowHealth:Fire()
end)

You can either write your own connection module to mimic connections or connect to a bindable event then fire that bindable event everytime you write to the health value within the goblin table given in the example.

The simplest way would be to just use a method that updates the health, and does the check you want. Only interact with the health through that method.

If you really wanted you can do this with metatables, though:

local goblin
do
	local internal = {
		Health = 100
	}

	goblin = setmetatable({}, {
		-- getters
		__index = internal,

		-- setters
		__newindex = function(t, k, v)
			if t[k] then
				print(k, "changed to", v)
				rawset(t, k, v)
			else
				error("cannot add new keys!")
			end
		end,
	})
end

print(goblin.Health) --> 100

goblin.Health = 50 --> "Health changed to 50"

print(goblin.Health) --> 50

goblin.health = 75 --> error: "cannot add new keys!"
2 Likes

Thanks for the response, I found an OOP solution some minutes ago, but your solution is good too. Just for who will stumble upon this question in the future, here is the code:

function TableWrapper:wrap(tabl: table, callback)
    self.onTableChange = Signal.new()

    local proxy = newproxy(true)
    local metatable = getmetatable(proxy)

    metatable.__index = tabl
    metatable.__newindex = function(_, index, value)
        if tabl[index] == value then return end
        tabl[index] = value
        self.onTableChange:Fire(index, value)
        callback(index, value)
    end

    return proxy
end

It wraps the table, so you will set the values like this:

local goblin = {health = 100}
local wrapper = TableWrapper:wrap(goblin, function(i, v)
    print(("%s changed to %s"):format(i,v))
end)
wrapper.health = 40 --> "health changed to 40"
1 Like

Very nice, I was just going to post how you could imitate Instance:GetPropertyChangedSignal with OOP:

image

Script:

local Goblin = require(script.Goblin)

local goblin = Goblin.new(500)

goblin:GetPropertyChangedSignal("Health"):Connect(function()
	print("custom event handler: health changed to", goblin.Health)
end)

print(goblin.Health) --> 500
goblin.Health = 50 --> "custom event handler: health changed to 50"
print(goblin.Health) --> 50
goblin.health = 75 --> error: "cannot add new properties"

Goblin ModuleScript:

local Goblin = {}

function Goblin.new(health: number?)
	local properties = {
		Health = health or 100
	}
	
	local events = {}
	
	local meta = {
		__index = function(t, k)
			local curr = properties[k]
			if curr then return curr end
			return Goblin[k]
		end,
		__newindex = function(t, k, v)
			local curr = properties[k]
			if curr then
				local event = events[k]
				if event then event:Fire() end
				rawset(t, k, v)
			else
				error("cannot add new properties")
			end
		end
	}
	
	return setmetatable({_properties = properties, _events = events}, meta)
end

function Goblin:GetPropertyChangedSignal(property: string): RBXScriptSignal
	if not self._properties[property] then
		error("invalid property name")
	end
	
	local bindable = self._events[property]
	
	if bindable then return bindable.Event end
	
	bindable = Instance.new("BindableEvent")
	self._events[property] = bindable
	
	return bindable.Event
end

return Goblin