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

I really like coding this way though. It definitely takes some getting used to though. You keep thinking “but where’s my draw loop gone??”

@yojimbo2000 Nice game. I played it several times, but after several levels it crashes with “stack overflow” line 116. That’s line scene[2]:… below.

function Scene:draw() --this is only used by gameover and pause....
    scene[2]:dispatch("draw") --to draw the underlying scene
    Scene.wait(self)
end

@dave1707 thanks for the report! I’ve never seen that error. I wonder if it’s because the game loop carries on dispatching move events to scene[1], but now without anything to receive them?.. I’ll try unsubscribing Game from draw while its paused…

@dave1707 I updated the code above, and on CC, so that Gameover and Pause no longer trigger the Game draw routine, so they don’t broadcast the “move” events. (changes are to tabs Game, Hero, Pause, PauseButton).

It’d be great whether you could let me know whether this helps.

Is stack overflow a memory leak?

@yojimbo2000 I now ran into this error.

error: [string "-- 03..."]:388: attempt to call method 'over' (a nil value)

See line 388 below

function Hero:collision(body)
    if body.id=="goal" then
        scene[1]:over(true) --win flag set to true
    elseif body.id=="tree" then
        scene[1]:dispatch("catMoves", nil) --conceal cat position
        self.hiding=true
    elseif body.id=="evil bug" and not self.hiding then
        scene[1]:over()   --< line 388
    end
end

EDIT:I played the game again and received the same error. I was at level 24.

@dave1707 level 24, wow! Thanks again for the report. That means that the collision routine is somehow running even though Game is no longer at the first position in the stack. I’ll add a dummy Scene:over() function to the Scene class just to try to catch these kinds of errors. I wonder if this is happening because scene[1] is being switched on the fly… Maybe I should set a flag, and only switch scenes at the beginning of a cycle. I’ll look at it tomorrow.

May I ask what device you’re using?

@yojimbo2000 I’m using an iPadAir

@dave1707 I finally was able to reproduce the stack overflow bug, thanks for flagging that. version 1.04 pasted above, should hopefully be stable now. What was causing the problems was that game-ending events were not exiting the collision checking loop. So if a bug hit you at the same time as you reached the goal, two GameOver instances would be created, leading to the stack overflow. Now I use return to exit the collision loop when a game-ending event is triggered.

I also no longer use the “draw” event to trigger the draw() functions (which means I no longer need to subscribe and unsubscribe scenes from “draw”).

I tweaked the difficulty too: more trees, but faster bugs.

@dave1707 I’m going to put my neck on the block and say that v106, above, is bullet-proof (with apologies for mixed metaphor)

@yojimbo2000 I made it to level 35 without any errors. I guess you fixed the problems.

@jmv38 (and anyone else who’s keen to pitch in!) I’m very keen to understand your event manager because while I’ve had very good results with Codea (or at least, I’m happy with them!) the organisation of my code is still, I recognise, a complete mess. The scenemanager structure I mention up the thread is a big help but everything that happens within a scene tends to be coded in quite an ad hoc way.

With the best will in the world though, I’m struggling to work through your event manager example. Do you have other examples on Codea Community, or would it be possible to have a longer narrative going through what happens when your example program is run, stage by stage?

One question I have for example (one of many): I don’t currently get the use of “extend”. Your comment is that it transforms a table into an event manager. That’s not helping me because you use it on World and Noise which aren’t (as far as I can see) tables! Also I can see extend applied to World and Noise, but not Message or Ball. I can see how each individual bit works, but I’m struggling to bring them together and understand the whole.

At the moment I’m programming a game which involves a couple of (moving and rotating) objects which blow bubbles and release them in a particular direction. So the bubble needs to know, as it’s being inflated, where the tip of the object is and its rotation. I had huge issues shuttling info between the bubbles being inflated and the objects blowing them up (each of which are multiple instances of particular class objects), and while I’ve found a way to do it without an event manager I can’t help but feel that this event manager style of coding is a good fit for what I’m trying to do and I’d like to rewrite my game to accommodate it.

@yojimbo2000 - thanks for the link to the article, that was an excellent way to get the basic concepts sorted in my mind, now I just need to work on the implementation! Really glad you started this discussion.

@epicurus101 World and Noise are classes, and a class is a table. Classes aren’t native to Lua, they are implemented as tables by Codea. On the Codea wiki, in the “beyond the reference guide” section, you can see the Lua code that Codea uses to implement classes.

My version uses the class inheritance system built in to Codea’s class system. So with my version, events have a scope, and that scope has to be a class (a scene or screen in the game). I got this idea from Corona.

@Jmv38 's code is more flexible, because any table, regardless of whether it is a class or not, can be turned into an event manager (and the events don’t, as far as I can see have a scope as such. But I too still don’t fully understand the code, I still need to experiment with it)

So mine is more restricted (but restrictions aren’t always bad)

@epicurus101 This is how Codea implements classes inside Lua tables (the code at the end of the page:

https://bitbucket.org/TwoLivesLeft/core/wiki/CodeaClasses

Interesting stuff, will have a further look. Understanding classes as tables is a big help. Re scope, I was thinking that this was what Extend does? But then I got confused because everything in that example takes place in World, so why does the scope need to be any wider than that.

I’ll take a look at your example as well, because I already have a scene structure in my game so yours might work better. I would just really like to understand ONE example fully!

There are some aspects of @Jmv38 's code that I might borrow. @Jmv38 's version can control the order listeners respond in, because the listeners are held in an array (using table.insert), whereas I (and CodeaBot) hold them in a hash table indexed to the listener’s “self” . The advantage of this hash approach is that unsubscribe is one line, just a matter of setting that value to nil, rather than having to step through the table to find the entry for the listener. But being able to control the order that listeners respond in could be useful.

As with everything, it’s a matter of trade-offs, and what you’re using it for. At the moment for instance, in my code posted above, the event manager handles the draw and move loops. Draw order changes from session to session, as it is decided based on whatever the memory address of each “self”. In a 2D system, this is not ideal. The player appears on top of some trees, but behind others, whereas you might want the scenery to always be on top of the moving bodies, so that they appear to hide behind obstacles. So you might decide that draw order is important, and you want the listeners held in an array.

Then again, if you were going for a forced-perspective, not directly top-down but somewhat-angled look (like a 16-bit JRPG, as these sprite images suggest), you might decide that draw order should be based on Y, so it wouldn’t matter what order the listeners are held in.

And if you were doing 3D, then (I think…) OpenGL would automatically sort objects by Z distance, so the listener order again wouldn’t matter.

(in fact, if were going for the forced-perspective option, I would probably use a “fake 3D” system for this game, just to automate the draw order issue. ie use translate (x,y,y) so that objects are further back in the scene depending on their Y, but with a very narrow field-of-view and far-away camera to minimise how much the objects recede into the distance. But that’s completely off-topic)

@yojimbo2000 i think you already know everything you need! My event manager is just what i’ve been using up to now. I shared it and made a quick example because you requested some, but i am by no means a reference for coding style! Actually, i liked your event manager and thought about rewriting mine into a more simple one, without the extend function, so a true class, and removing some lines of code. As you’ve pointed out, the order of events is extremely important for making anything real.

@epicurus101 all you need to understand about event manager is:

  • make obj1 a event manager by EventMngr:extend(obj1).
  • call obj1:on(“any text”, func, obj2), so
  • when you write obj1:trigger(“any text”) then func(obj2) will be called.
  • when you dont want that any more to happen, do obj1:off(“any text”, func, obj2)).
    The manager provides other cool stuff but you dont need that right now.
    I understand you lack of understanding: i was pretty much like you when i first heard about events on this forum. I spend really much time to fully understand what is going on with this code. And you know what? After all that, i now understand that this code works really well and i dont need to understand why, i can just use it! But i dont blame you for following the same path i did. Lol.

@Jmv38 I’m trying to work through a small modification of your ball example to help me understand the workings of it, but it’s not behaving as I would predict.

What I’m trying to achieve is:-

  1. Each time the ball collides, it creates a new physics box (which DOESN’T collide with the balls, and just drops to the bottom of the screen), in a table, capped at 5 boxes to keep it manageable
  2. Also, each time the ball collides, each box should get a new, random colour.

Q1 - I’ve achieved part 1 fine, by altering the Ball collide function to create a new instance of a new Box class. This doesn’t use the event manager, and I don’t think there’s any way to achieve that more cleanly with your event manager, is that right? Perhaps I could create another “Box generator” class to intermediate between the ball collisions and the box generation but that seems like unnecessary complication for a relatively simple forward action. I think!

Q2 - However for the second bit I do want to use the event manager. I want each (free roaming) box instance to react to the event “ball collides”.

How I’ve tried to do this is by applying extend to “b” (the ball instance) in setup().
Correct so far? It’s the “sending” item which needs to be an event manager, yes?

Then I’ve created a b:on(“collide”,…) function which generates a random colour for self (each box instance). I wasn’t sure where to put this so I put it in the Box:draw() function. Is that a bad idea? I tried to put it outside any functions but it didn’t like calling b presumably because before setup b doesn’t exist.

Then I put a b:trigger, again in my Box’s draw function. So the Box:draw() looks like:-

function Box:draw()
    
    b:on("collide",function() self.colour = color(math.random(255), math.random(255), math.random(255)) end, self)
    b:trigger("collide")
    local pos = self.body.position
    self.x, self.y = pos.x, pos.y
    fill(self.colour)
    rect(self.x,self.y,self.r*2)
end

However this causes the boxes to flash wildly, and not just change colour when the collision occurs.

Can you help me out, because it seems like it’s really important WHERE you place all these event doohickeys and I’m just not getting it at the moment!

i’ll have a look.
Quick feedback meanwhile: the draw() is called 60 times per second. So what you’ve done is adding a new ‘collide’ event definition, and you trigger it too, 60 times per second… is this what you wanted to do? for sure not.
I guess the on(collide) should be defined once in the ball:init(), and the trigger(collide) should be in the ball:collide() function. Try to solve it yourself. I’ll post an example anyway.

this is not exactly what you want, but it is fun and you can study my changes. I had some hard time avoiding infinite ball creation, hence the tests in the ball:collide() function


--# 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 = #self.events[eventName] +1 else p = 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,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
        
-- 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,grav,noBaby)
    self.x = x
    self.y = y
    self.r = r
    self.noBaby = noBaby
    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 = grav or 0
    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)
    World:executeNextCallBeforeOthers()
    World:on("touched",self.touched,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
function Ball:babyBall()
    local baby = Ball(self.x,self.y,self.r/3,1,true)
end
function Ball: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) ) and not self.noBaby then self:babyBall() end
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

--# 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 = Ball(WIDTH/2, HEIGHT/2, 50)
end

-- This function gets called once every frame
function draw()
    background(40, 40, 50)
    strokeWidth(2)
    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