Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Using Events to communicate between instances of a class.

BriarfoxBriarfox Mod
edited January 2014 in Questions Posts: 1,542

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

Comments

  • Posts: 536

    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)

  • Jmv38Jmv38 Mod
    Posts: 3,297

    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?

  • Posts: 536
    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..
  • Jmv38Jmv38 Mod
    Posts: 3,297

    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)

  • edited January 2014 Posts: 580

    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).

  • Jmv38Jmv38 Mod
    Posts: 3,297

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

  • edited January 2014 Posts: 580

    @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 ;) 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.

  • Posts: 536

    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 :)

  • Posts: 536

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

  • Posts: 580

    @tnlogy: Ah yes! I forgot to mention that the Events code I posted should handle those cases as well :) 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!

  • Posts: 536

    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.

  • Jmv38Jmv38 Mod
    Posts: 3,297

    @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.

  • BriarfoxBriarfox Mod
    edited January 2014 Posts: 1,542

    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?

  • BriarfoxBriarfox Mod
    edited January 2014 Posts: 1,542

    @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.

  • toffertoffer Mod
    edited January 2014 Posts: 151

    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
    
  • Jmv38Jmv38 Mod
    edited January 2014 Posts: 3,297

    @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.

  • edited January 2014 Posts: 580

    @Briarfox: sorry I'm not sure I understand what you are trying to do, sorry :( 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.

  • edited January 2014 Posts: 580

    @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! :)

  • Posts: 140

    @MrScience101 It's really a matter of complexity. For simple examples, using a whole Event class is like killing a fly with a sledgehammer. A quick local implementation may be all that you need. For complex examples, a separate class is probably necessary. Events are also helpful to communicate between different classes, as well.

    In my current project, I finally had to implement a Touch class to capture touches as an accidental stray touch would crash my code. However, I found that only certain ones needed to be recorded, so I just capture those and pass the rest through. And rather than use an Event system to handle callbacks, they are handled by the data structure itself. This lets me serialize my data to my heart's content.

    And that's probably all even more confusing. Sorry. :(

  • Posts: 536

    I guess @Briarfox want to set a global event, which the @toadkick code handles with the "all" context argument, if i understand it correctly?

    I guess it depends on the problem you want to solve, but events can decouple the code creating the event and the listener nicely and that you can add several callbacks.

    Nice that you are using a weak hash, haven't used that myself.

  • BriarfoxBriarfox Mod
    Posts: 1,542

    @toadkick sorry that was sudo code that I used to try to explain my problem. I believe @tnlogy explained it well. I wanted to set Global events that each instance of a class could listen for.

  • This is how i created an event system. It was more for a timed event thing, than a "call when you need me" event system. I guess it was more for queueing up functions within the draw() class that you didn't want to call every frame.

    --Event.lua
    Event = class()
    
    events = nil
    eventcount=0
    
    
    function Event:init( func, freq, params  )
        -- you can accept and set parameters here
        if( events == nil ) then -- no events have ever been created.  create the global structure we store events in
            events = {}
            eventcount = 1
        end
        if( func == nil ) then 
            print( "Event:init(): NIL function" )
            return 
        end
        print( "Event:init()", func, freq, params )
        freq = freq or 1
        params = params or {}
        local ev = event_t(func, freq, params)
        print( ev, ev.f, ev.hz )
        events[eventcount] = ev
        eventcount = eventcount + 1
        print( "Event:init()", ev.f, ev.hz, ev.param )
    end
    
    function Event:ServiceEvents(frameCounter)
    
        frameCounter = frameCounter or 0
        local ev
        for i=1, eventcount-1 do
            --print( "Event:ServiceEvents", #events, events[i], i )
            ev = events[i]
            if( (frameCounter % ev.hz) == 0 ) then
                events[i].f( events[i].param )
            end
        end
    end
    
    function Event:removeEvent( event, frequency )
        --search thru events[] and find the matching object
    
    end
    
    event_t = class()
    
    function event_t:init( func, freq, params )
        self.f = func
        self.hz = freq
        self.param = {}
        for i=1, #params do
            self.param[i] = params[i]
        end
        print( "created event", self.f, self.hz, self.param )
        return self
    end
    

    and here's the main.lua usage:

    ---- Event
    --[[
    an event queue system
        codea tries to run at 60FPS
        you can schedule events to be run at x hz
    
        syntax:
    
        addEvent( event, frequency )
            event = function to call.  
            frequency = how often
        removeEvent( event, frequency )
            removes event from the 
    --]]
    -- Use this function to perform your initial setup
    
    function setup()
        ev1 = Event( testPrint, 10, { 1, 2, 3 } ) 
        ev2 = Event( testPrint, 30, { 4, 5, 6 } )
        counter = 0
        --print( list[2] )
    end
    
    -- This function gets called once every frame
    function draw()
        -- This sets a dark background color 
        background(40, 40, 50)
        if( counter == 60 ) then 
            counter = 0
        end
        counter = counter + 1
        Event:ServiceEvents( counter )
        -- This sets the line thickness
        strokeWidth(5)
    
        -- Do your drawing here
    
    end
    
    function testPrint( a )
        print( "testing string", a[1], a[2], a[3] )
    end
    
    
  • edited January 2014 Posts: 580

    @tnlogy Not quite: "all" will cause all events triggered by a specific sender to call the callback. In this case, the handler's function signature is slightly different, in that the event name will be sent as the first argument (unless the callback has context, in which case it will be the second argument), followed by any arguments passed to the callback when the event was triggered. This can be useful for perhaps proxying all events from one object to another, debugging your events, etc.

    @Briarfox: Ah okay! One way is to make a global table a broadcaster. You can create a table specifically for that, or piggyback on another existing global object. Here's an example of creating a dedicated global event broadcaster.

    -- create the global event broadcaster
    gEvents = extend({}, Events)
    
    -- note that MyClass doesn't need to extend Events
    -- if it just wants to handle events: only to *trigger()* events,
    -- or to *subscribe()* to events, 
    -- neither of which we are doing in this example.
    MyClass = class()
    
    function MyClass:init()
        -- listen for gEvents' "global:test" event.
        -- pass "self" as context, meaning it will be
        -- passed as the first argument to the event handler;
        -- this allows us to implement our handler as
        -- a member function.
        gEvents:on("global:test", "handleEvent", self)
    
        -- equivalent to:
        --gEvents:on("global:test", self.handleEvent, self)
    end
    
    function MyClass:handleEvent()
        print("Event handled by: " .. self)
    end
    
    -- elsewhere:
    local obj = MyClass()
    gEvents:trigger("global:test")
    
    

    Alternatively, you could have subscribed, which may be preferable, because a) the listener will not keep the event broadcaster alive (it is weakly linked, as @tnlogy noted), and b) you can subscribe an object from all events that it is subscribed to by calling object:unsubscribe(). Here's the above example using subscribe() instead:

    -- create the global event broadcaster
    gEvents = extend({}, Events)
    
    -- note that MyClass doesn't need to extend Events
    -- if it just wants to handle events: only to *trigger()* events,
    -- or to *subscribe()* to events, 
    -- neither of which we are doing in this example.
    MyClass = extend(class(), Events)
    
    function MyClass:init()
        -- listen for gEvents' "global:test" event.
        -- pass "self" as context, meaning it will be
        -- passed as the first argument to the event handler;
        -- this allows us to implement our handler as
        -- a member function.
        self:subscribe(gEvents, "global:test", "handleEvent")
    
    
        -- equivalent to:
        --self:subscribe(gEvents, "global:test", self.handleEvent)
    end
    
    function MyClass:handleEvent()
        print("Event handled by: " .. self)
    end
    
    -- elsewhere:
    local obj = MyClass()
    gEvents:trigger("global:test")
    

    Note that MyClass now must extend Events to use subscribe().

  • Jmv38Jmv38 Mod
    Posts: 3,297

    @toffer i ran your code but it doesnt quite work: here is what i get for he anonymous function:

    touch:trigger(10,20) -- outputs "touch at    false    10"
    touch:trigger(50,20) -- outputs "touch at    false    50"
    

    any idea on how to get it work (i have not a clue, i dont understand your code, sorry)

  • Jmv38Jmv38 Mod
    Posts: 3,297

    @tnlogy i have tried your code. Works as expected, see my test below.
    However, you have to trigger the event with the object himself. You must know who is listening the events to trigger them. With this you cant just throw an event in the air, and the listneeres get contacted by magic. Just to be sure i understand correctly.

    Item = {}
    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: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
    
    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
    local function func(x,y)
        print("touch at",x,y)
    end
    function setup()
        Item:on("touch",func)
        Item:trigger("touch",10,50)
        Item:off("touch")
        Item:trigger("touch",10,50)
    end
    function draw()
        background(20)
    end
    
  • toffertoffer Mod
    Posts: 151

    @Jmv38 - sorry for that, haven't tested, that what I say by 'naive implementation'. I've edited to fix the mistake.

  • Jmv38Jmv38 Mod
    Posts: 3,297

    @toadkick i've tried your code (partially). It work well when an object triggers its own events. But not when another object triggers the event. My assumption is pbly very naive, but i would have expcted it as a basic requirement? Here is my test code:

    local function func(x,y)
        print("touch at",x,y)
    end
    local function funcAll(name,x,y)
        if name == "touch" then
        print("touch at",x,y)
        end
    end
    
    function setup()
        
    myObject1 = extend({}, Events)
    myObject2 = extend({}, Events)
    
    myObject1:on("touch", func)
    myObject1:trigger("touch",10,50) -- this works
    myObject2:trigger("touch",10,50) -- this does not
    
    myObject1:on("all", funcAll)
    myObject2:trigger("touch",10,50) -- this does not
    end
    
  • Jmv38Jmv38 Mod
    Posts: 3,297

    @toadkick ok i tried your last post code abou global event: now i get what i want. Works nicely.

  • edited January 2014 Posts: 580

    @Jmv38: The thing to realize here is that any object that extends Events is an event broadcaster. So, that actually does work as expected. 'myObject2:trigger(etc)' isn't doing anything because nothing is listening to myObject2. Does that make sense? Also, you are having 'myObject1' listen to its own events, I'm not sure that's what you intended.

  • BriarfoxBriarfox Mod
    Posts: 1,542

    Thanks @toadkick I understand the on vs subscribe much better now.

  • Jmv38Jmv38 Mod
    Posts: 3,297

    @Toffer i have tried your modified code: now it works ok! Thanks.
    The advantage of your version is that it is so compact. I like that. I have some more questions:
    -1/ could you explain a bit what 'once' is doing? It is not immediately clear to me from reading the code...
    -2/ the remark about weak table is important. It makes management easier. Would it be difficult to rework your code to impleant this weak link?
    Thanks!

  • Jmv38Jmv38 Mod
    Posts: 3,297

    For -1/ nevermind, i've understood! I thought 'once' meant 'after it' but here it means 'one time only' , so it is clear now.

  • edited January 2014 Posts: 536

    Yes, my code is quite simple, just events within the item itself. Should have mentioned that Item was a class. :)

    You can make the event system a bit more interesting, by propagating the event to the parent and allow broadcasting of events.

  • toffertoffer Mod
    Posts: 151

    @Jmv38 - Compact code does not mean better :) I think you can write json decoder in less than 10 lines but that don't mean it will handle all the cases and be efficient. The example I gave was just to illustrate that there is many approach to handle communication within an 'application'. And as pointed by syntonica, the better is to write or use a lib that fit the needs for the project, not use this or that lib because it's beutifully formated or it can create a button in one line off code, IMHO. That is say, you can have weak listeners by adding the key __mode = "kv" to the proto, the side effect is that anonymous listeners will be destroyed when the garbage is collected. Hope that make sens.

  • Jmv38Jmv38 Mod
    Posts: 3,297

    @toffer still trying to wrap my head around this. Sorry for my naive comments.
    I looked into you code and find there is a flaw (i dont say you didnt know): it does not work with instances of a class, because the functions of instances have all the same adress. Only 1 objec is triggered there, the last one registered

    -- method class listener
    local Obj = class()
    function Obj:init(name) self.name = name end
    function Obj:ontouch(x,y)
        print(self.name .. " touch at",x,y)
    end
    
    function setup()
    obj1 = Obj("obj1")
    obj2 = Obj("obj2")
    touch:on(obj1.ontouch,obj1)
    touch:on(obj2.ontouch,obj2)
    
    -- trigger event 
    touch:trigger(10,20) -- 
    
    end
    
  • toffertoffer Mod
    Posts: 151

    @Jmv38 - Don't be sorry, there is nothing naive in your comments. The problem was that listeners are registered by their function (and instances of a same class share the same functions pointers). I've fixed the issue. Also I feel that I've little spammed the thread, I apologize going out of the original subject which targeted Events.

  • Jmv38Jmv38 Mod
    Posts: 3,297

    Hello friends-in-Codea !
    I have progressed in my understanding.
    @toffer example is missing the ability to have several register to the same event, so it is not really usable. I tried to extend it, but the code is so compact i am not able to modify it without errors.
    So i looked back to tnlogy code and found it really corresponds to my needs. And i understand it! Sorry for puting you down in my last comment, @tnlogy, my understanding was just wrong. So i have modified you code to extend it a little bit.
    I am not using @Toadkick code for the momnt, because it is so long i havent even tried to understand it...
    Can you guys have a look at my code and tell me if it looks ok, or i have completely misunderstood how to use events?

    -- @tnlogy >> @JMV38 modifications
    
    -- ############## START of EVENT MANAGER ##################
    -- example of usage:
    --    evMngr = EventMngr()               -- create the event manager
    --    evMngr:on("touch",func)            -- register func() to fire on "touch" event
    --    evMngr:on("touch", obj.func, obj)  -- register obj:func() to fire on "touch" event
    --    evMngr:trigger("touch",10,50)      -- fires func(10,50) and obj:func(10,50)
    --    evMngr:off("touch", func)          -- unregister func()
    --    evMngr:off("touch", obj.func, obj) -- unregister obj:func()
    --    evMngr:off("touch")                -- unregister all "touch" listeners
    --    evMngr:off(obj.func)               -- unregister all listeners with obj.func
    --    evMngr:off(obj)                    -- unregister events with obj listening
    --    evMngr:reset()                     -- unregister everything
    
    EventMngr = class()
    function EventMngr:init() end
    function EventMngr:on(names, fn, obj)
        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
            local new = true -- confirm it is a new request
            for i,fa in ipairs(self.events[name]) do
                if fa.func == fn and fa.obj == obj then new = false end
            end
            if new then table.insert(self.events[name], {func = fn, obj = obj }) end
        end
        return self
    end
    
    function EventMngr:reset()
        self.events = {}
    end
    
    function EventMngr:off(nameOrFnOrObj, fn, obj)
        local name
        local fn,obj = fn,obj -- manage the case when they are nil
        local firstType = type(nameOrFnOrObj)
        local request
        if firstType == "string" then 
            name = nameOrFnOrObj
            if fn == nil then request = "remove all instances of this event"
            else request = "remove this event" end
        elseif firstType == "function" then 
            fn = nameOrFnOrObj
            request = "remove all events with this function"
        else 
            obj = nameOrFnOrObj
            request = "remove all events with this object" 
        end
    
        if request == "remove all suscriptions to this event" then
            self.events[name] = nil
        else
            local evs = self.events            -- go through all events ...
            if name then evs = {evs[name]} end -- ... or through 1 event only
            for k,fns in pairs(evs) do
                local n = #fns
                for i=0,n-1 do
                    local j = n-i  -- go backward because of remove, ipairs not suitable
                    local f = fns[j] 
                    local match
                    if request == "remove this event" 
                    then match=(f.func==fn and f.obj==obj)
                    elseif request == "remove all events with this function" 
                    then match=(f.func==fn)
                    elseif request == "remove all events with this object" 
                    then match=(f.obj==obj)
                    end
                    if match then table.remove(fns,j) end
                end
            end
        end
        return self
    end
    
    function EventMngr:trigger(name, ...)
        self.lastTrigger = name
        local evs = (self.events and self.events[name]) or {}
        for i,fa in ipairs(evs) do 
            local func,obj = fa.func, fa.obj
            if obj then func(obj,...) 
            else func(...) end
        end
    end
    
    -- ############## END of EVENT MANAGER ##################
    
    local function func(x,y)
        print("touch at",x,y)
    end
    -- fake class for the tests
    local Obj = class()
    function Obj:init(name) 
        self.name = name
    end
    function Obj:echo(x,y) 
        print(self.name .. " touch at ".. tostring(x) .. " , " .. tostring(y) )
    end
    
    function setup()
        evMngr = EventMngr()
        -- test on a standalone function
        print("-------------------")
        print("one touch should be triggered:")
        evMngr:on("touch",func)
        evMngr:trigger("touch",10,50)
        print("-------------------")
        print("here nothing should happen:") print("")
        evMngr:off("touch")
        evMngr:trigger("touch",10,50)
        -- test on a class member
        obj1 = Obj("object 1")
        obj2 = Obj("object 2")
        print("-------------------")
        print("2 objects should echo a touch event:")
        evMngr:on("touch", obj1.echo, obj1)
        evMngr:on("touch", obj2.echo, obj2)
        evMngr:trigger("touch",10,50)
        print("touch object 2 is off:")
        evMngr:off("touch", obj2.echo, obj2)
        evMngr:trigger("touch",10,50)
        print("-------------------")
        print("re-arm the 2 events:")
        evMngr:on("touch", obj1.echo, obj1)
        evMngr:on("touch", obj2.echo, obj2)
        evMngr:trigger("touch",10,50)
        print("echo removed :nothing should happen:") print("")
        evMngr:off(obj1.echo)
        evMngr:trigger("touch",10,50)
        print("-------------------")
        print("re-arm the 2 events:")
        evMngr:on("touch", obj1.echo, obj1)
        evMngr:on("touch", obj2.echo, obj2)
        evMngr:trigger("touch",10,50)
        print("object 1 removed :")
        evMngr:off(obj1)
        evMngr:trigger("touch",10,50)
        print("-------------------")
        print("re-arm the 2 events:")
        evMngr:on("touch", obj1.echo, obj1)
        evMngr:on("touch", obj2.echo, obj2)
        evMngr:trigger("touch",10,50)
        print("trigger removed :nothing should happen:") print("")
        evMngr:off("touch")
        evMngr:trigger("touch",10,50)
        print("-------------------")
        print("re-arm the 2 events:")
        evMngr:on("touch", obj1.echo, obj1)
        evMngr:on("touch", obj2.echo, obj2)
        evMngr:trigger("touch",10,50)
        evMngr:reset()
        print("reset :nothing should happen:") print("")
        evMngr:trigger("touch",10,50)
    end
    function draw()
        background(20)
    end
    function touched(t)
        evMngr:trigger("touch",t.x,t.y)
    end
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
  • BriarfoxBriarfox Mod
    edited January 2014 Posts: 1,542

    @Jmv38 that looks really good. It's very straight forward. I'm not sure which I want to use now. I like how you can unregister all events that use a specific callback function.

    I have been using @Toadkick 's Event manager. I like how each class can act as its own event manager but can subscribe to a global event manager if needed. Another feature that toadkicks event manager has is the ability to register an event to fire once then remove itself.

  • BriarfoxBriarfox Mod
    edited January 2014 Posts: 1,542

    @Jmv38 the second test does not appear to be successful. evMngr:off("touch") still triggers a touch event.

    Fixed it. One request string uses instances. Then the check looks for subscriptions.

  • BriarfoxBriarfox Mod
    edited January 2014 Posts: 1,542

    @Jmv38 Another feature that I find very useful in @Toadkick 's version is the use of "all" receiving all event triggers with the first param being the event that was triggered.

    edit I added in "all" thats called with each trigger.

    function EventMngr:trigger(name, ...)
        self.lastTrigger = name
        local evs = (self.events and self.events[name]) or {}
        for i,fa in ipairs(evs) do 
            local func,obj = fa.func, fa.obj
            if obj then func(obj,...) 
            else func(...) end
        end
        --trigger all
        local evs = (self.events and self.events["all"]) or {}
        for i,fa in ipairs(evs) do 
            local func,obj = fa.func, fa.obj
            if obj then func(obj,name,...) 
            else func(name,...) end
        end
    end
    
  • Jmv38Jmv38 Mod
    Posts: 3,297

    About

    Fixed it. One request string uses instances. Then the check looks for subscriptions.

    I dont get it. I thought it worked in my test. But it doesn't? what did you change to make it work? In my code?

  • BriarfoxBriarfox Mod
    Posts: 1,542
    if fn == nil then request = "remove all instances of this event" --This is correct
            else request = "remove this event" end
        elseif firstType == "function" then 
            fn = nameOrFnOrObj
            request = "remove all events with this function"
        else 
            obj = nameOrFnOrObj
            request = "remove all events with this object" 
        end
    
        if request == "remove all instances of this event" then --This had subscription instead of instances.
    
  • Jmv38Jmv38 Mod
    Posts: 3,297

    Adding an 'all' keyword is quite easy. But using @Toadkick class might be the best way for you

  • BriarfoxBriarfox Mod
    Posts: 1,542

    @jmv38 I added in "all" to the above post.

  • Jmv38Jmv38 Mod
    Posts: 3,297

    Ha i see! I changed the code be be clear and made this bug!

  • BriarfoxBriarfox Mod
    edited January 2014 Posts: 1,542

    @Jmv38 I really have not found a need to have a class specific event manager and realized that I've mainly been using a global event manager. I'm liking your event manager very much.

  • Jmv38Jmv38 Mod
    Posts: 3,297

    But my example is a class!
    I made a class because i though it would be better to have specialized evnt managers: 1 for draw, 1 for touch, 1 for object internals, because of speed reasons (going through a whole long table might be long?)

  • BriarfoxBriarfox Mod
    Posts: 1,542

    @Jmv38 you are right. I had been using @Toadkick 's Event manager incorrectly. I was extending each class I had which was un nessesary.

  • BriarfoxBriarfox Mod
    Posts: 1,542

    Added in a little function to extend a class with EventMngr using @Toadkick 's extend example.

    function EventMngr.class()
            local target = class()    
            for k, v in pairs(EventMngr) do
                target[k] = v
            end
        return target
    end
    
    usage:
    Obj = EventMngr.class()
    function Obj:init()
    ...
    end
    
    
Sign In or Register to comment.