Organising larger projects, a simple game state engine [ed: with event handler]

ps: i didnt use the event mechanism to create baby balls, because it is within ball itself, and fairly systematic, and i dont care about the balls afterwards. So events seemed more complex than simple in this case, so i wrote a direct call to color change and ball creation. You may want to change this, but only if it makes things simpler.

I’ve made a few changes:

  • put the mother ball in a special class for clarity. You can study inheritance from another class.
  • added a ‘delete’ function to ball because it is often a problem with physics objects. Note that all references must be removed, so worl:off is called too. And collectgarbage is important too.
  • due to this delete, i had to modify the order the objects are triggered in the event function. That was an interesting exercise.

--# EventMngr
-- ############## START of EVENT MANAGER ##################
-- @tnlogy & @JMV38 & @Briarfox
-- example of usage:
--    EventMngr:extend(evMngr)           -- extend an existing table with event manager funcs
--    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
--    "all" captures all events and passes the event name as the first param:
--    evMngr:on("all", func) 

EventMngr = {}

local fifo = true -- first in (to register) first out (to be triggered)
function EventMngr:on(eventName, fn, obj)

    if not self.events then self.events = {} end -- init event table if does not exist
--    if not self.events[eventName] then self.events[eventName] = {} end -- init this event name
    if not self.events[eventName] then self.events[eventName] = {} end -- init this event name
    
    local new = true -- confirm it is a new request
    for i,fa in ipairs(self.events[eventName]) do
        if fa.func == fn and fa.obj == obj then new = false end
    end
    
    local p -- insertion point in the table
    if new then 
        if fifo then p = 1 else p = #self.events[eventName] +1 ; fifo=true end
        local listener = {func = fn, obj = obj }
        table.insert(self.events[eventName], p, listener) 
    end

    return self
end

function EventMngr:executeNextCallBeforeOthers()
    fifo = false
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" or firstType == "number" then 
        name = nameOrFnOrObj
        if name == "all" then request = "remove all events"
        elseif 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 instances of this event" then
        self.events[name] = nil
    elseif request == "remove all events" then
        self.events = {}
    else
        local evs = self.events            -- go through all events ...
        if name then evs = {evs[name]} end -- ... or through 1 event only
        for eventName,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 = #evs,1,-1 do
        local fa = evs[i]
        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
        
-- to transform a table into an event manager
function EventMngr:extend(target)
    for k, v in pairs(self) do
        if type(v) == "function" and v ~= EventMngr.extend
        then target[k] = v 
        end
    end
    return target
end


-- ############## END of EVENT MANAGER ##################




--# World
World = class()
EventMngr:extend(World)

function World:init()
end

function World:draw()
    self:trigger("draw")
end

function World:touched(touch)
    self:trigger("touched", touch)
end

--# Edge
Edge = class()
EventMngr:extend(Edge)

function Edge:init(data)
    if data.x then
        local x = data.x
        self.pos0 = vec2(x,0)
        self.pos1 = vec2(x,HEIGHT)
    elseif data.y then
        local y = data.y
        self.pos0 = vec2(0, y)
        self.pos1 = vec2(WIDTH, y)
    else
        error("please define x or y")
    end
    self.body = physics.body(EDGE, self.pos0, self.pos1)
    self.body.info = self
    -- events
    World:on("draw",self.draw,self)
    World:executeNextCallBeforeOthers()
    World:on("touched",self.touched,self)
end

function Edge:draw()
    fill(223, 223, 223, 255)
    line(self.pos0.x, self.pos0.y, self.pos1.x, self.pos1.y)
end

function Edge:touched(touch)

end

--# Ball
Ball = class()

function Ball:init(x,y,r)
    self.x = x
    self.y = y
    self.r = r
    self:updateColor()
    self.body = physics.body(CIRCLE, self.r)
    self.body.x = self.x
    self.body.y = self.y
    self.body.sleepingAllowed = false
    self.body.gravityScale = 1
    self.body.linearVelocity = vec2(400,200)
    self.body.linearDamping = 0
    self.body.angularDamping = 0
    self.body.friction = 0
    self.body.restitution = 1
    self.body.info = self
    -- events
    World:on("draw",self.draw,self)
end

function Ball:delete()
    self.body:destroy()
    World:off(self)
end

function Ball:draw()
    local pos = self.body.position
    self.x, self.y = pos.x, pos.y
    fill(self.color)
    ellipse(self.x,self.y,self.r*2)
end

local rand = math.random
function Ball:updateColor()
    -- lets change color
    local r,g = rand(255),rand(255)
    local b = 255 - g
    self.color = color(r,g,b)
end
local abs = math.abs
function Ball:touched(t)
    if abs(self.x-t.x)<self.r and abs(self.x-t.x)<self.r then
        if t.state == BEGAN then 
        end
    end
end

--# MotherBall
MotherBall = class(Ball)

function MotherBall:init(x,y,r)
    Ball.init(self, x,y,r)
    self.body.gravityScale = 0
end
function MotherBall:babyBall()
    local baby = Ball(self.x,self.y,self.r/3)
    World:on("delete",baby.delete,baby)
end
function MotherBall:collide(c)
    -- lets change color
    self:updateColor()
    -- if i touch a wall, make a baby ball
    if c.bodyA.info:is_a(Edge) or c.bodyB.info:is_a(Edge) then self:babyBall() end
end


--# Main
-- eventsExample

-- Use this function to perform your initial setup
function setup()
    e1 = Edge({x=0})
    e2 = Edge({x=WIDTH})
    e3 = Edge({y=0})
    e4 = Edge({y=HEIGHT})
    b = MotherBall(WIDTH/2, HEIGHT/2, 50)
    parameter.action("delete",function() World:trigger("delete") collectgarbage() end )
    
end

-- This function gets called once every frame
function draw()
    background(40, 40, 50)
    strokeWidth(1.5)
    noSmooth() 
    World:draw()
end

function touched(t)
    World:touched(t)
end

function collide(c)
    if c.state == BEGAN then 
        fA = c.bodyA.info.collide
        if fA then fA(c.bodyA.info, c) end
        fB = c.bodyB.info.collide
        if fB then fB(c.bodyB.info, c) end
    end
end

Thanks @Jmv38, I really appreciate the effort, but the main thing this tells me is I chose a lousy project to experiment with the use of Event managers! Both the things I wanted to achieve are easier using conventional coding (or whatever non-event coding is called!). I think I’m still struggling with the event-driven mindset. I will give it more of a ponder. But thank you again, I do appreciate it, and there’s always something to learn from seeing how someone else does something

You are welcome. As i explained above, the choice of what to use is not systematic, it depends on what you do and want to do. As you practice, it gets more and more obvious. Creating 1 child each time you bounce a wall dont need events. Deleting all the balls with one tap is easier with events.

@epicurus101 This is kind of echoing what @Jmv38 just said, Ithink of it as a kind of broadcast system. If there’s one specific instance of a class that you need to communicate with, there’s no point IMO communicating with that instance via the event manager, as you might as well just use a direct link.

But if you need to let a bunch of objects know, it’s useful. One good example is anything where the user can touch an object on screen. As soon as a touch registers, you need to test all of the touchable bodies to see if the touch position lies within them. The touched routine, rather than looping through all touchable objects, broadcasts a “touched” event. If you’re using branching class inheritance, it’s even more powerful, as you can add the “subscribe to touched” command to the super-class for all of the touchable objects (in my example, Button). This means that touchables can be stored anywhere, they don’t need to be in a single array (like they should probably be with a conventional looping approach).

If events are confined to a scope, this becomes really powerful (ie flicking between an inventory screen and the main game screen in an RPG or whatever), as you can easily switch which scope the touch event broadcasts to.

I’m reviving this thread because my experiments with @Jmv38 's event manager have revealed a worrying problem in my code that I’m extremely grateful to know about, and since my discovering it relates to the event driven method I thought I would see how people deal with this.

I have a class called “Cannon” in my game, which has instructions for a cannon to be drawn / fire projectiles. Every time someone starts a level, I create a new table called Cannons and put in it the requisite number of instances for that level.

I used to just iterate through the Cannons table in my draw function to draw all the cannons. Fairly straightforward, but not very neat. I now have a trigger in the draw cycle of each game level to trigger drawing of all the objects, just as in the example @Jmv38 posted. So my cannons have the following in their init function:-

Play:on(“draw”, self.draw, self)
(Play is the class which draws during game time)

I’ve got the event-driven method working for this, and my Cannons draw beautifully. The worrying thing though (which I hadn’t realised) is that every time I finish a level, the cannon instances I’ve created aren’t killed. The second time I run through a level, ghost cannons are drawn on the screen from the last level! And the third time, I have two sets of ghost cannons etc…

Previously I had assumed that because at the start of every level I redefined the Cannons table that the old cannons were deleted. But clearly they aren’t and I’ve realised it must be something to do with the table only POINTING to the instance, not in any real sense holding it. This must have been happening before I implemented events, I just didn’t realise it because the cannon instances didn’t have their own self-contained draw trigger.

If I’ve created the cannons by doing this, for example:-

Cannons = {Cannon(1,1), Cannon(2,-1)}

and this has generated two instances, what’s the best way of properly deleting those instances when I want them done? Cannons[1] = nil doesn’t do it. collectgarbage() doesn’t seem to help.

I’m worried that I’ve been creating lots of zombie instances without realising it and this might have performance implications. I haven’t even tried the same experiment with other game items, but the result’s likely to be the same.

OK, a bit more experimentation reveals that it’s only because I didn’t force the instances to unsubscribe from the draw function that they weren’t picked up in garbage collection. Not nearly as fundamental an issue with my coding as I thought!

looks like you’ve nailed it! For each cannon you want to get rid off, make it delete himself with play:off(self)