Using Events to communicate between instances of a class.

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

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.

@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

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

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

@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

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

@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

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

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

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

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

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

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

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

@Jmv38 - Compact code does not mean better :slight_smile: 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.

@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

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

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















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