Using Events to communicate between instances of a class.

I was digging around in Cargo Bots and noticed the Events class. I did a little reading up on events and this looks like a great way to allow class instances to communicate with each other. As some of my projects get bigger I am constantly trying to tweak classes so they can communicate with newer classes and this has made for some messy code. So I wanted to see if I have the concept correctly understood and was looking for any other ideas on how an event dispatcher and manager could be implemented.

My understanding of the concept is that a class can register events with the event manager. If the event is triggered then any instance of a class or classes receive the event and run the registered function. Is this the basic concept? Have any of you guys used anything similar or have any advice for using event driven classes?

Cargo Bot Code:

-- Events.lua

-- Events facilitates message passing between objects. 
-- Mostly for user generated events
-- but some internal events too like "won" or "died" or "moveDone"
-- Classes that respond to events should define a bindEvents method where all the events 
-- are bound so that they can be easily rebinded if needed
Events = class()

Events.__callbacks = {}

function Events.bind(event,obj,func)
    if not Events.__callbacks[event] then
        Events.__callbacks[event] = {}
    end
    
    if not Events.__callbacks[event][obj] then
        Events.__callbacks[event][obj] = {}
    end
    
    Events.__callbacks[event][obj][func] = 1
end

-- event is optional
function Events.unbind(obj,event)
    for evt,cbs in pairs(Events.__callbacks) do
        if event == nil or event == evt then
            cbs[obj]=nil
        end
    end
end

function Events.unbindEvent(event)
    Events.__callbacks[event] = nil
end

function Events.trigger(event,...)
    if Events.__callbacks[event] then
        -- make a clone of the callbacks. This is because callbacks 
        -- can bind or unbind events. for example Stage.play can
        -- recreate its state and needs to rebind
        local clone = {}
        for obj,funcs in pairs(Events.__callbacks[event]) do
            clone[obj] = {}
            for func,dummy in pairs(funcs) do
                clone[obj][func] = 1
            end
        end

        for obj,funcs in pairs(clone) do
            for func,dummy in pairs(funcs) do
                
                local argCopy = Table.clone(arg)
                table.insert(argCopy,1,obj)
                func(unpack(argCopy))
            end
        end
    end
end

Events can clean up the code a bit, maybe especially when you have a graph of object and can propagate events up in the graph.

I just wrote this to handle events, not doing much yet in a class.

function Item:on(names, f)
    for name in string.gmatch(names, "%S+") do
        if not self.events then self.events = {} end
        if not self.events[name] then self.events[name] = {} end
        table.insert(self.events[name], f)
    end
    return self
end

function Item:trigger(name, ...)
    self.lastTrigger = name
    local evs = (self.events and self.events[name]) or {}
    for i,f in ipairs(evs) do f(...) end
end

then you can write self:on(“click move”, funcA):on(“drop”, funcB) and call it with self:trigger(“move”, x, y)

Very interesting discussion.
@tnlogy:
1- What is the use for self:on("click move", funcA):on("drop", funcB)?
Just to avoid self:on("click move", funcA) ; self:on("drop", funcB)?
2- how would you write an eventRemove function with your scheme?

  1. Yes, to be able to chain the calls, like in Smalltalk or jquery. Thats why I return self in Item:on.
  2. I would iterate the list, a bit depending on if you call it with self:off(funcA) or name the event or both…

Thanks @tnlogy
For 2/, what is the recommended way to do it? (the kind of code you produce let me think you have the adequate experience to give the correct answer)

I’ve got some events code I wrote awhile back that’s based on the excellent Backbone.Events object in javascript (http://backbonejs.org/#Events). I’ve been meaning to share it but I’ve never got around to separating it from a bunch of other personal library code that I use. Since it’s come up, I went ahead and did that today so I could share it. Here it is:

-- Events

local tinsert = table.insert
local tremove = table.remove

local eventsKey = setmetatable({}, {__tostring=function() return "[events]" end})
local sendersKey = setmetatable({}, {__tostring=function() return "[senders]" end})
    
-- subscriber will not keep sender alive
local sendersMT = {__mode="k"}
local function _senders(self)
    local senders = self[sendersKey]
    if not senders then
        senders = setmetatable({}, sendersMT)
        self[sendersKey] = senders
    end
    return senders
end

local function _event(self, name)
    local events = self[eventsKey]
    if not events then
        events = {}
        self[eventsKey] = events
    end
    
    local callbacks = events[name]
    if not callbacks then
        callbacks = {}
        events[name] = callbacks
    end
    return callbacks    
end
    
local function _callback(context, callback)
    if type(callback) == "string" then
        if type(context) == "table" then
            callback = context[callback] or
                error("table does not contain method '"..callback.."'")
        else
            error("context must be a table")
        end
    end
        
    return callback
end
    
local function _remove(callbacks, callback, context, predicate)
    for i = #callbacks, 1, -1 do
        if predicate(callbacks[i], callback, context) then
            tremove(callbacks, i)
        end
    end
end

local function _on(self, once, name, callback, context)
    assert(name ~= nil, "you must specify an event name.")
    
    callback = _callback(context, callback)
    
    assert(callback ~= nil, "you must specify a valid callback.")

    tinsert(_event(self, name), {
        context = context,
        callback = callback,
        once = (once == true) and once or nil
    })
    
    return self
end

local function on(self, ...)
    return _on(self, false, ...)
end

local function once(self, ...)
    return _on(self, true, ...)
end

local _offPred = {
    eq = function(v, cb, ctx) return ctx == v.context and cb == v.callback end,    
    cbEq = function(v, cb, ctx) return cb == v.callback end,
    ctxEq = function(v, cb, ctx) return ctx == v.context end,        
    eqNoCtx = function(v, cb, ctx) return (not v.context) and cb == v.callback end
}

local function off(self, name, callback, context)
    local events = self[eventsKey]
    if not events then return self end
    callback = _callback(context, callback)
        
    if not (callback or context) then
        if name then 
            events[name] = nil
        else 
            self[eventsKey] = nil
        end
        
        return self
    end
        
    if name then events = {name = events[name]} end
        
    local pred
    if callback and context then
        pred = (context == "all") and _offPred.cbEq or _offPred.eq
    else
        pred = callback and _offPred.eqNoCtx or _offPred.ctxEq
    end                
        
    for _, callbacks in pairs(events) do
        _remove(callbacks, callback, context, pred)
    end
    
    return self
end

local function _trigger(_handlers, name, all, ...)
    if not _handlers then return end
    
    local nhandlers = #_handlers
    if nhandlers == 0 then return end
    
    local handlers = {unpack(_handlers)}
    
    for i = 1, nhandlers do
        local handler = handlers[i]
        if handler.once then
            for ci = 1, #_handlers do
                if handler == _handlers[ci] then
                    tremove(_handlers, ci)
                    break
                end
            end
        end
        
        local context = handler.context        
        
        if context then
            if all then
                handler.callback(context, name, ...) 
            else
                handler.callback(context, ...) 
            end
        else
            if all then
                handler.callback(name, ...)
            else
                handler.callback(...)
            end
        end
    end    
end

local function trigger(self, name, ...)
    assert(name ~= nil, "you must specify an event name")

    local events = self[eventsKey]
    if not events then return self end

    _trigger(events[name], name, false, ...)
    _trigger(events["all"], name, true, ...)
    
    return self
end

local function _subscribe(self, once, sender, name, method)
    assert(type(sender) == "table", "sender table expected")
    assert(name ~= nil, "event name expected")
    assert(method ~= nil, "callback method expected");
        
    _senders(self)[sender] = true
    _on(sender, once, name, method, self)
    
    return self
end

local function subscribe(self, ...) return _subscribe(self, false, ...) end
local function subscribeOnce(self, ...) return _subscribe(self, true, ...) end

local function unsubscribe(self, sender, name, method)
    if sender ~= nil and type(sender) ~= "table" then
        error("sender table expected")        
    end

    local senders = self[sendersKey]
    if not senders then return self end
    
    if sender then
        senders = {[sender] = senders[sender] and true or nil}
    end

    for sender in pairs(senders) do
        sender:off(name, method, self)
        if not (name or method) then
            self[sendersKey][sender] = nil
        end
    end
    
    return self
end

-- Events mixin
Events = {
    on = on,
    once = once,    
    off = off,
    subscribe = subscribe,
    subscribeOnce = subscribeOnce,
    unsubscribe = unsubscribe,
    trigger = trigger    
} 

To use it, you need a function like extend:

-- extend target table with properties of other table(s).
-- left-most table in param lest takes precedence
function extend(target, ...)
    for i = 1, select("#", ...) do
        for k, v in pairs(select(i, ...)) do
            target[k] = v
        end
    end

    return target
end

There’s quite a few features here, so it’s a bit much to explain all at once, but the API is almost identical to that of Backbone.Events, except that I’ve renamed listenTo() and listenToOnce() to subscribe() and subscribeOnce().

You can extend any object to send events (or be an event subscriber) pretty easily using extend(). One way is to extend an object directly:

myObject = extend({}, Events)

function eventHandler()
print("someEvent fired!")
end

myObject:on("someEvent", eventHandler)
myObject:trigger("someEvent")

Alternative, you can extend a class, and then all of it’s instantiated objects will be able to send events:

MyClass = extend(class(), Events)

function MyClass:doSomething()
    self:trigger("someEvent")
end

function setup()
    local obj = MyClass()

    function eventHandler()
        print("someEvent fired!")
    end

    obj:on("someEvent", eventHandler)
    obj:doSomething()
end

A couple of other notes:

  1. “all” is a special event; any handler listening for it will receive all broadcasted events, with the event name as the first argument, followed by any additional arguments passed to the event.
  2. subscribe() can be used instead of on() so that objects can easily unsubscribe from many/all events/senders at once. Additionally, when using subscribe, since the handler must be a member function, you can specify the member function by name, like:
self:subscribe(someObject, "someEvent", "myEventHandler")

is equivalent to:

self:subscribe(someObject, "someEvent", self.myEventHandler)

so long as self actually has a method named “myEventHandler”.

Anyway, sorry for the lack of explanation/documentation. The Backbone.Events reference might be helpful, since this code is pretty much based on it. Also, I’ll be happy to answer any questions about it. For what it’s worth, I pretty much use this code in all of my projects now. It’s especially helpful when implementing UI code (like Buttons, etc).

@Toadkick wow! The code is long, but the usage seems peatty straightforward. Thanks for sharing. Is there any bug? Or is it really settled?

@Jmv38: There aren’t any bugs known to me, but as I’m the only one that’s used this code so far, it’s entirely plausible there may be at least one lurking in there somewhere :wink: I do use this pretty extensively though (particularly subscribe()/unsubscribe()) so I’d like to say that it’s pretty solid, but hey, software is complex!

If you believe you have come across a bug, lemme know and I’ll hunt it down and fix it.

EDIT: I’ve also shared this on CC, though there are no examples currently. tomorrow I’ll try to carve out an hour or two to write up some simple examples and update the CC project.

I’ve just thrown away my Item, so haven’t needed to, but to remove it I wrote this now:

function Item:off(nameOrFn, fn)
    local name = nameOrFn
    if type(nameOrFn) == "function" then
        name,fn = nil,nameOrFn
    end
    if not fn then
        self.events[name] = nil
    else
        local evs = self.events
        if name then evs = {evs[name]} end
        for k,fns in pairs(evs) do
            for i,f in ipairs(fns) do
                if f == fn then table.remove(fns,i) end
            end
        end
    end
    return self
end

quite long, but handles the cases self:off(fn) self:off(eventname) and self:off(eventname,fn). not so tested :slight_smile:

Backbone is nice, even though I’ve lately more used Meteor and knockout.

@tnlogy: Ah yes! I forgot to mention that the Events code I posted should handle those cases as well :slight_smile: I will look at Meteor and knockout, I’ve not been javascripting much over the last year or so so I’m a bit out of the loop there, but they look very useful!

Meteor is nice since you can write mongodb queries on the client and they will automatically be pushed to the server, don’t need to do any REST. but it is quite early in development, so I think it gets quite messy when building larger sites. Then I think angularjs might be a bit more mature, but a bit heavy to dig into the docs.

@tnlogy thanks for the item:off.
@tnlogy and @Toadkick i assume you both had a look at the code of the other. They are very different, one being pretty short, and the other longer. In term of functionnalities, how do compare them? Are there additionnal functionnalities present in your version, @Toadkick? Just to understand.
Thank you both.

Very cool examples. I was just finishing up my version when I saw the Backbone.Events version. I’ll need to look at It a little closer.

So how often do you guys finding yourself using an Event type system?

@toadkick I’m having trouble figuring out how to have instanced objects all respond to the same trigger. For instance, classA and classB set self:on(“begin”) from within the class. How do I trigger “begin” and have both classes recieve the event?

I’m also curious about @Jmv38 's question. Also how do these compare with the cargoBot version? I’m still having trouble wrapping my mind around this concept.

Very interesting thread. I think all the implementation presented are really good. It may depends on what feature you need. For example, I didn’t see one that open preventing event propagation (@toadkick one ?) or listener priority. At the end, that’s just callbacks :). To add my little contrib, signals are also a nice way to handle events, with the benefits of composition. Here is a naive implementation :

local proto = {} proto.__index = proto

function proto:on(fn,obj)
    self[obj or fn] = obj and fn or false
end

function proto:off(target)
    self[target] = nil
end

function proto:trigger(...)
    for k,v in pairs(self) do
        if v then v(k,...) else k(...) end
    end
end

signal = function()
    return setmetatable({},proto)
end

usage ex:

-- instanciate signal
local touch = signal()

-- anonymous listener
touch:on(function(x,y)
    print("touch at",x,y)
end)

-- method instance listener
local obj = {}
function obj:ontouch(x,y)
    print(self,"touch at",x,y)
end
touch:on(obj.ontouch,obj)

-- trigger event 
touch:trigger(10,20)
touch:trigger(50,20)

also you can extends it easily:

function proto:once(fn,obj)
    local function wrap(...)
        fn(...)
        self:off(wrap)
    end
    self:on(wrap,obj)
end

@Toffer all that code look so interesting i wish i would understand it just by reading it…
But i dont. :-(( That is all so abstract and nevertheless extremely concise.
Anyway now i have to choose between 3 solutions…

I am curious. What is the difference between having event managers like those listed above and just calling an ‘event’ function in the class itself in the draw function? Please pardon my ignorance, as I am only trying to grasp this concept.

@Briarfox: sorry I’m not sure I understand what you are trying to do, sorry :frowning: When you say:

classA and classB set self:on("begin") from within the class

do you mean you have self:on(“begin”) in the init function? I’m not sure that’s doing what you think; self:on(“begin”) is not correct usage, and should be throwing an error. it’s a bug if it’s not, because you have not specified a function to handle the “begin” event that the object represented by “self” will presumably trigger.

Perhaps if you could explain what you want to accomplish I could point you in the right direction.

I haven’t had a chance to look at cargo bot’s event code yet, but I’ll try to later to try to identify the main differences.

@MrScience101: There are lots of reasons to use events in your game. For one, you can (easily) broadcast an event to multiple listeners. For another, events decouple your objects in such a way that they don’t need to have intimate knowledge of each other’s internal workings.

A practical example might be this: suppose in your game you have scripted a scenario where you spawn a wave of bad guys at the player, and want to know when they are all dead. Your script could listen for the actors to broadcast when they are destroyed, and then do something when that happens.

Also as I said before, I find them quite useful for UI event handling code.

I’m sure other people can provide their own examples too! :slight_smile: