I see a lot of posts asking how to create a system similar to Roblox’s “Change” event mechanism.
I’ve decided to make this post to answer the question once and for all.
The Basic Idea
To Luau, when you use an Instance
, you’re referring to a userdata
proxy that points to the actual Instance. These userdatas
have no data written to them directly, but instead use the __index
metamethod to retrieve data from the Instance
, and __newindex
to set it.
A
userdata
is a Lua construct for exposing a C object to Lua in a more safe manner. Almost all Roblox datatypes areuserdatas
, the notable exception isVector3
, which uses the nativevector
type instead.
Most properties set through __newindex
will invoke the Change handler system, which is eventually sent downstream to your change handlers.
You can emulate this behaviour simply with your own metatable and some BindableEvents
Code outline
The basic example uses two tables and a BindableEvent
, first of all, lets create the two tables, the one holding the raw data, and the one with a metatable to the raw data:
local changeSignal = Instance.new("BindableEvent") -- change event (you dont have to use a bindable, but it makes this easier)
local raw = {
apples = 10
} -- the raw data table, we dont access this directly unless to bypass the change signal handler
local proxy = setmetatable({}, {
__index = nil,
__newindex = nil
} -- the change handler proxy, each method will be outlined shortly
So, from here, we should explain what to put in each of the metamethod keys.
__index
For index, it should be safe to just point it directly to the raw
table, we dont actually need any special behaviour here, unless you want to add access modifiers.
__newindex
This is where the magic happens, in __newindex
, we need to define a function to send a change downstream to the event, unless the value didn’t change. Its worth noting that the existence of __newindex
does not automatically write the key-value pair to the table, unless you tell it to (through rawset
).
My approach to this is as follows, this sends the property name, new and old value to the event signal.
__newindex = function(self, key, value)
local oldValue = raw[key]
if oldValue == value then return end -- we dont want changes that didn't change anything firing the event
raw[key] = value -- write the new key to the raw data table
changeSignal:Fire(key, value, oldValue) -- send the change through the event
end
As you can see, this is an extremely simple system once you get to grips with how it works. We’re simply changing the key value then sending that to an event, because we’re not writing to the proxy table itself, this should work.
Writing a key to the proxy table directly, either within
setmetatable
orrawset
will disable__newindex
for that key, as this metamethod only invokes for undefined/nil keys.
Showcase
If we now use this proxy and bindable to listen for changes, we’ll notice that any changes to apples
is sent to the event handler:
changeSignal.Event:Connect(print)
proxy.apples = 15 --> apples 15 10
proxy.apples = 10 --> apples 10 15
proxy.apples = 10 --> nothing (expected)
Full Code
For ease of access, here’s the full code outline:
local changeSignal = Instance.new("BindableEvent")
local raw = {
apples = 10
}
local proxy = setmetatable({}, {
__index = raw,
__newindex = function(self, key, value)
local oldValue = raw[key]
if oldValue == value then return end
raw[key] = value
changeSignal:Fire(key, value, oldValue)
end,
__metatable = "The metatable is locked" -- you dont have to lock the metatable if you dont want to, but more just preference for me
})
The ChangeEvent invokes if you write keys to the proxy, not raw.
Summary
As you can see, this is a really simple mechanism to set up on any table, I believe the existence of metatables makes this appear scary, but its really not once you figure out how it works.