Howdy, Stranger!

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

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

edited February 2015 in Suggestions Posts: 2,020

I wanted to ask what kind of game state engines people have come up with for larger projects. Initially I had a set of variables defined:

game={splash=1, playing=2, paused=3, over=4, level=5, edit=10}
game.state=game.splash

And had branching if structures in Draw() , Touched() etc.
But then I had the idea of storing separate routines in a table indexed by the game state value, like this:

game={splash=1, playing=2, paused=3, over=4, level=5, edit=10} --must be declared outside of a function.
touchBegan={} 
touchBegan[game.playing]=function(args)
...

dr={} --Codea won't allow a table called draw
dr[game.over]=function(args)
...

Then to call the routines, you don't need an if structure, just touchBegan[game.state] or dr[game.state]

Interestingly, you have to declare these with variable=function(arguments). If you try this, Codea won't compile it:

function touchBegan[game.playing](arguments) --won't work

How do you setup your game state engines? Another potential structure I haven't explored in my own code yet is the way the different tests are called in the physics debug example, with each function name held in a table.

Comments

  • I found this scenemanager class, by @Frosty, to be extremely intuitive and it helped me massively.

    http://codea.io/talk/discussion/677/scenemanager/p1

    Each screen you encounter in the game is then defined by a class, and you can perform specified functions just before a new screen is called, or on exiting that screen, and the draw / touched functions are automatically segregated.

    Bear in mind I'm a real beginner and I expect others here will have more sophisticated ways of doing things. This is a good thread to start!

  • Posts: 418

    @yojimbo2000 I have a fairly large game thats uses if statements in the my draw, predraw and touched functions. It works fine, my touched function is a complete mess tho.

  • Posts: 300

    That Scene Manager is similar to how Corona does it, so is probably a good way to go.

  • Posts: 2,020

    @Goatboy76 yeah, I was also using large branching if structures. But having blocks of code spanning hundreds of lines, indented several layers deep, is so messy. When you're editing an if block, adding some elseifs or whatever, I was always losing the end.

    This is Cargobot's touched loop. I'd love to get as minimal as this.


    function touched(t) if currentScreen and currentScreen.touched then currentScreen:touched(t) end end

    Thanks for the scene manager link. I don't really understand it though. In the example, he defines an example menuscene(), but it doesn't refer to the scenemanager() class?

  • Jmv38Jmv38 Mod
    Posts: 3,295

    in CodeaShowcase i have in th Main:

    function touched(t)
        if Screen then Screen:touched(t) end
    end
    

    in th screen class i have

    function Screen:touched(t)
        if not currentScreen or not currentScreen.touchable then return end
        currentScreen:trigger("touched",t)
    end
    

    the trigger mechanism provided by the Event class makes extremely simple to build complex project by decoupling things. If you wànt to be that simple, look for 'event' discussions on the forum. At first it sounded difficult to me but after understanding it is really simple. And mandatory.

  • Posts: 418

    @yojimbo2000 If its spanning hundreds of lines then you should break it down into different functions for related tasks.

  • Posts: 688

    I've created my own heavyweight screen manager library which is inspired by the Corona storyboard lib, plus some of my own additions. It's fully object oriented and supports transitions and event callbacks and forms the base for a complete ui / widget library. I've already used it in one released app and it's running at least two other prototypes as well.

    The screen manager part is done but really needs properly documenting. I was going to post it when the widget library is a bit more complete but if you want a copy then drop me a PM (I'll see if I can get it sorted on my GitHub account)

  • edited January 2015 Posts: 2,020

    @Jmv38 thanks for the tip about searching for event managers, there are some really interesting discussions there. I still find them hard to follow, I think because the event managers are all presented as templates (understandably), but it would be nice to see how they would work in a simple use case.

    For instance, at the moment my code is clogged with the tutorial system I have. The idea is that as the player plays the first level, alerts are triggered at various points telling them how to play. This means that the code is punctuated with tests like this (this one is in the collision routine):

     
    if lev==1 and tut.extra==true and tut.now==false and tut.goal==false then
                    tut.now=true
                    tut.goal=true
                    tween.delay(0.8,function() tutorial("Get the gem meter\nover the target line\nto clear the level")end)
                end

    the if statement is saying, if this is level 1 (lev=1), and the previous tutorial has already printed (ie they have to occur in a given order tut.extra), and there isn't currently an alert onscreen ( ie to prevent them piling up on top of oneanother tut.now), and this particular event has not triggered before (ie this is once only tut.goal), then activate the tutorial function (after a brief delay, so that the player can register the triggering event), which displays the text in a fancy way, and waits for a user tap to clear it.

    Aside from looking ugly, and lengthening the code block, and scattering the tutorial texts throught the code, and being difficult (even for me) to read, and being easy to break, it means that an if statement is called every time this particular type of collision occurs, for what is a once-only event, which doesn't seem like a good use of resources.

    I understand the principle that with an event manager system I could uncouple the tutorial code from the collision code, so that one triggers the other without the kind of ugly direct link that I have here. But I'm struggling to see how I would implement this using the various event manager templates I've seen, or even work out how "heavy weight" a manager I would need to implement this kind of system.

  • Jmv38Jmv38 Mod
    Posts: 3,295

    i cant answer that in a couple of lines. I think in codeaShowcase i ended up with very complex behavior, a bit like what you need. But it is not coded as you did, it would be impossible this way. The key is maybe:

    1- think 'objects': an object is defined by a class, has some methods and properties. Not too many. Less than 100 lines is a good limit. If you need more, then can the object have some other objects as properties? So each object remains understandable. Use 'getters' and 'setters' to change their properties, this will pay when you start modifying your code from its first version (happens quite quickly). When the *has a" relationship does not make sense, you can use a "is a" relationship, by building a class from another one. But this is more complex. If you look at cargobot, you will see many classes piled up, each adding a few things. It is much simpler to understand, in the long run, than putting everything in the same long class definition tab.

    2- make objectA be an event broadcaster of "jump" event and objectB listen to these events to run objectB:jump(), rather than directly calling the objectB:jump() from objectA. Dont do it when the relation is systematic with all objects known in advance. Do this when it makes sense: when there may be several impacts of "jump", but you are not sure how many, etc. This is a huge relief for the brain...

    Defining the good breakdown into objects, or when to use events, is not obvious, it requires some first attemps of coding, then meditation, then rewriting the code completely (many times). For instance, in my buttons library first various style properties were directly in the object. But there were so many i put them into a table button.stlye. Then i had various objects with a style property, and it became obvious that is was simpler to define a Style() class and object, and then i changec for button.style = Style(). On the other hand i've always kept x,y,w,h instinsic properties of a button, because that seems natural, and the access is faster with button.x than button.position.x, and i thought it is important for speed. I still hesitate and may change my mind on that.

    Maybe i'll try to make an example from your description, but this is going to require some thinking, because i have not implemented collisions yet.

  • Jmv38Jmv38 Mod
    edited January 2015 Posts: 3,295

    PS: when i say 'think object', i really mean 'think'. it is difficult. It takes time. It took me about 2 years to switch from 'procedural' thinking to 'oop' thinking. You can find several discussions on the forum i started under the subject 'how to make big projects?'. I was always hitting a 'complexity wall', and now i've passed through it, via the above concepts.

  • IgnatzIgnatz Mod
    Posts: 5,396

    @yojimbo2000 - I agree with Jmv38, evolve your code. I don't think you need sophisticated event management at this stage, though.

    I would start by getting the tutorial code out of the setup, draw and touched functions, and putting it into its own set of functions. What setup, draw and touched should do is to pass these functions the information they need to decide what to do - and that is all.

    For example, the touched function might run Tutorial("touch",level).

    The tutorial code could include a table of help messages in sequence, have a counter that keeps track of where you're up to, and a function (called by the main draw function) that puts the current message on the screen. You can queue messages by putting them in a table, so that as one finishes, it gets deleted from the table and the next one is shown.

    When you've got it working, I'd think about how to make it efficient, so you don't waste a lot of time processing tutorial code once they've been shown.

    This needn't be very complex code, IMHO.

  • Jmv38Jmv38 Mod
    edited February 2015 Posts: 3,295

    Here is an example with some of the ingredients you want.
    Events are used for drawing various objects, with collisions, delays.
    [EDIT] i've added a couple buttons and a quiet state to show how to trigger a global status (like your tuto / not tuto). You can see that all the state impacts are resolved locall by each object.
    [EDIT] i've changed the shout mechanism so teach shout generates next (you wanted an sequence of events in a given order).
    [EDIT] modified again: you can tap the ball, so you see the touched events too.



    --# 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 ################## --# Noise Noise = class() EventMngr:extend(Noise) function Noise:init(b) self:setSilence(b) end function Noise:setSilence(b) self.silence = b self:trigger("silence",self.silence) end Noise:init(false) --# 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 --# Message Message = class() local enabled = true Noise:on("silence",function(b) enabled = not b end) function Message:init(txt,x,y,t,size) if not enabled then return end self.x, self.y, self.t, self.txt, self.size = x,y,t,txt, (size or 50) World:on("draw",self.draw,self) tween.delay(self.t,function() World:off(self) end) end function Message:draw() fill(255, 0, 0, 255) fontSize(self.size ) text(self.txt, self.x, self.y) 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.body = physics.body(CIRCLE, self.r) self.body.x = self.x self.body.y = self.y self.body.sleepingAllowed = false self.body.gravityScale = 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) print ("try to tap the ball") end function Ball:draw() local pos = self.body.position self.x, self.y = pos.x, pos.y fill(223, 223, 223, 255) ellipse(self.x,self.y,self.r*2) end function Ball:shout(size) self.soundSize = size or self.soundSize or 50 -- this one Message("A",self.x,self.y,1,size) -- next one local newSize = self.soundSize * 0.9 if newSize > 20 then tween.delay(0.05,function()self:shout(newSize)end) else collectgarbage() end end function Ball:collide(c) self:shout(50) 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 Message("Outch!",self.x,self.y,1,50) 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) parameter.action("silence",function() Noise:setSilence(true) end) parameter.action("loud",function() Noise:setSilence(false) end) end -- This function gets called once every frame function draw() background(40, 40, 50) strokeWidth(5) 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
  • Jmv38Jmv38 Mod
    edited February 2015 Posts: 3,295

    What is interesting in the code above, is that it was extremely easy to program: it worked right away, thanks to objects / events. Try to achieve the same thing with 'if xxx and zz and yyy then ... ', it would be a real headache!
    [EDIT] and i've modified it several times since first post: it was really easy to add, remove of change structure for various actions. No headache, no bug... that's almost incredible. That was not possible with my former procedural way to code.

  • Posts: 2,020

    @Jmv38 Thank you very much for that, I'm working through your code, I don't understand it all yet, but it's got some fascinating parts.

    I've also been working on an adaptation of the CargoBot events handler. I basically took out everything I don't (yet) understand, so that it's very simple, but also changed a few things based on how Corona handles events (or at least my understanding of Corona).

    What really helped me get my head round the concepts is this tutorial for Corona here:

    http://www.omidahourai.com/from-zero-to-oo-ardentkids-guide-to-object-oriented-lua-with-corona-sdk#customevents

    As events are built-in to Corona this tutorial doesn't say anything about what Corona does under the hood, but you can see the syntax and usage.

    So what I decided to experiment with was to make my Events class a super-class of all the scenes in the game. It mimics the Corona usage of:

    scene:subscribe("event name", listener, callback)

    The advantage of this method is it becomes very easy to manage a large set of events, without explicitly having to handle them in the event manager eg, a "pause" state which temporarily suspends all of the events can be done with one line.

    @Jmv38 the method you use instead, of extending a table, is really interesting though, and not something I'd encountered before. I need to study your approach more, as I suspect it's more powerful and flexible. The advantage of the one I'm working on is that it's very light (the event manager is 30 lines). I'll post mine in a few moments, so people can compare

  • edited February 2015 Posts: 2,020

    I'll have to split it over 2 comments. I'll put it on Codea Community too. Feedback welcome! [EDIT 1.06 replaced scenes table with a set of variables for more readable, unambiguous code, and to prevent redundant copies of scenes being added to a scene table. Added separate objectInit routine to avoid having to pass the event scope to every object class.]

     
     
    --# Main
    --[[
    RETURN TO THE GEOSPHERE 
    By Utsira
    a simple stealth game serving as a proof of concept for a light modular game design, using class inheritance and an event manager to decouple the code, allowing extra objects and states to be added with very little extra code. There are two strands of class inheritance (matching left-to-right organisation of tabs): 
    1) SCENES or states, which also houses the event manager as a superclass, so that events are restricted to the scope of the scene:
    Event > (Scene > (Splash, Game, GameOver, Pause))
    2) OBJECTS:
    Object > (Button > (PauseButton), WorldObject > (Tree, Goal, Agent > (Hero, AI > (Bug))))
      ]]
     
    displayMode(OVERLAY)
     
    function setup()
        centre=vec2(WIDTH/2,HEIGHT/2)
        difficulty=3
        font("DINAlternate-Bold")
        fill(31, 31, 95, 255)
        fontSize(30)
        textMode(CENTER)
        textAlign(CENTER)   
        splash=Splash()   --variable "scene" automatically set whenever a scene is defined
     
       -- fps.init()    
    end
     
    function draw()
        sprite("SpaceCute:Background", centre.x, centre.y, WIDTH, HEIGHT)
        scene:draw()
    end
     
    function touched(touch)
      if not scene.delay then scene:dispatch("touched", touch) end
    end
     
    --# Event
    Event = class()
    --[[
    very simple event handler, adapted from Cargobot, but also drawing on Corona.
    Event is made a superclass of Scene, and therefore of every scene in the game.
    This means that events have a scope, the scene they belong to,
    a concept borrowed from Corona.
    This greatly helps modularity. For an example look at how PauseButton & Pause are able to suspend, remember, and recall a set of events with one-line calls to table.insert and remove
     
    syntax:
    -------
    scene:subscribe(event, listener, callback)
    scene:unsubscribe(event, listener) --listener optional
    scene:dispatch(event, arguments)
     
    examples:
    ---------
    scene:subscribe("catMoves", self, self.chase) 
        --a bug subscribes to event "catMoves"
    scene:dispatch("catMoves", self.pos) 
        --broadcast cat position from hero class to current scene
    self:subscribe("touched", self, self.touched) 
        --a scene subscribes itself to a system event, note repetition of self
     
    notes:
    ------
    1. the callback is defined in the class in the usual way, ie with a colon
    "Bug:chase(arg)", but is given as a callback argument with a period, 
    and without brackets or arguments "self.chase"
    2. if the scene init() defines objects that subscribe to events, those objects won't be able to access the scene variable in their own init() routines, because the scene is still in the process of being defined. So you need to pass "self" as an argument from the scene init() to the object init() if you want those objects to be able to subscribe to events
    3. Roadmap: Currently dispatch needs to be called every frame for anything to happen, not the best use of resources. A better system would be an on off flag for dispatch, a callbackOn and callbackOff in subscribe, and a state saver at either end.
    4. Issues: order of listeners currently random, see note below
    ]]
    function Event:init()
        self.events={}
        print("Scene "..self.id.." created\n"..string.rep("=",34))
    end
     
    function Event:subscribe(event, listener, callback)
        if not self.events[event] then
            self.events[event]={} --create event
            print ("Event "..event.." created in "..self.id.."\n"..string.rep("-",34))
        end
        self.events[event][listener]=callback --add listener. Problem: no way of controlling order, depends on random memory address. So cant set order of listeners, eg if using event as draw loop
        print (listener.id.." is subscribed to "..event) 
    end
     
    function Event:unsubscribe(event, listener) --listener optional
        if listener then
            self.events[event][listener]=nil --unsubscribe one listener
            print (listener.id.." unsubscribed from "..event) 
        else
            self.events[event]=nil --unsubscribe all listeners
        end
    end
     
    function Event:dispatch(event,...)
        if not self.events or not self.events[event] then
          --  print ("no subscribers to "..event) --error message for debug purposes
            return
        end
        for listener,callback in pairs(self.events[event]) do
            callback(listener, unpack(arg))
        end
    end
     
    --# Scene
    Scene = class(Event)
     
    function Scene:init()
        Event.init(self)
        self.time=ElapsedTime
        self.delay=0
        self.objects={}
        self.note="Tap anywhere to continue"
        scene=self --automatically switch scope when defining a scene
    end
     
    function Scene:draw() --this is used by gameover and pause....
        game:dispatch("draw") --to draw the underlying game scene
        self:wait()
    end
     
    function Scene:touched(touch) --used by gameover and splash
        game=Game() --(re)boot game
        game:initObjects()
    end
     
    function Scene:wait() --handles timeout at start of scenes
        fontSize(30)
        if self.delay then
            text(string.format(self.note.."\nin %.1f", self.delay-(ElapsedTime-self.time)), centre.x, centre.y-200)
            if self.time<ElapsedTime-self.delay then
                self.delay=nil
            end
        else 
            text(self.note.."\n", centre.x, centre.y-200 ) --"Tap anywhere to continue\n"
        end
    end
     
    --# Splash
    Splash=class(Scene)
     
    function Splash:init()
        self.id="splash"      
        Scene.init(self)    
        self.note="A super simple stealth game.\nHide behind trees to fool bugs.\n\n"..self.note
        self:subscribe("touched", self, self.touched)
    end
     
    function Splash:draw()
        fontSize(50)
        text("Return to the\nGeosphere!", centre.x,centre.y+100)
        self:wait()
    end
     
    --# Game
    Game=class(Scene)
     
    function Game:init()
        self.id="game"
        Scene.init(self)
        self.delay=2
       -- parameter.action("test if objects are listening", function() self:dispatch("testButton") end)
        self.note="Level "..(difficulty-2).."\nReady to go"
    end
     
    function Game:initObjects() --objects need their own setup so that they can access the scene/game var and define events                  
        self.button=PauseButton(1)            
        self.hero=Hero(vec2(WIDTH-50,50))  
        self.worldObjects={ Goal(vec2(200,HEIGHT-50))} --self.worldObjects used for collisions ...
        for a=1, math.max(1, 20-(difficulty*0.4)) do --trees need to be before bugs...
            table.insert(self.worldObjects, Tree())   -- for hiding to work properly....     
        end --no draw loop
        for a=1, difficulty do 
            table.insert(self.worldObjects, Bug()) 
        end  
    end
     
    function Game:draw()
        if self.delay then 
            self:wait()
        else
            self:dispatch("move")
        end
        self:dispatch("draw")
      --  fps.draw()
    end
     
    --# GameOver
    GameOver = class(Scene)
     
    function GameOver:init(won)
        self.id="game over"
        Scene.init(self)    
        if won then
            difficulty = difficulty + 1
            self.note="YOU REACHED THE GOAL!\nTap anywhere\nto go to next level"
        else
            self.note="GAME OVER!\nTap anywhere\nto retry level"
        end
        self.delay=2  
        self:subscribe("touched", self, self.touched)
    end
     
    --# Pause
    Pause = class(Scene)
     
    function Pause:init()
        self.id="pause"
        Scene.init(self)
        self.delay=2
        self.note="PAUSED\n"..self.note
        self:subscribe("touched", self, self.touched)
    end
     
    function Pause:touched(touch)
        game.delay=2 --delay game when it restarts
        game.time=ElapsedTime
        game.note="Resume"
        scene=game --switch scope to game
    end
  • edited February 2015 Posts: 2,020

    Part 2 [EDIT 1.06 replaced scenes table with a set of variables for more readable, unambiguous code, and to prevent redundant copies of scenes being added to a scene table. Added separate objectInit routine to avoid having to pass the event scope to every object class.]:

     
    --# Object
    Object = class() --helper class for everything drawn in game...
     
    function Object:init(pos)
        self.pos=pos or vec2(math.random(0, WIDTH-100),math.random(100, HEIGHT))
        self.width,self.height=spriteSize(self.img)
        scene:subscribe("draw", self, self.draw) --...subscribes to draw
    end
     
    function Object:draw()
        sprite(self.img, self.pos.x, self.pos.y, self.width)
    end
     
    function Object:touched(touch)
        if touch.x<self.pos.x+self.width*0.5 and touch.x>self.pos.x-self.width*0.5 and touch.y<self.pos.y+self.height*0.5 and touch.y>self.pos.y-self.height*0.5 then
            self:action(touch)
        end
    end
     
    function Object:testButton()
        print(self.id.." hears button being pressed")
    end
     
    --# Button
    Button = class(Object)
     
    function Button:init(number)   
        self.id="button"
        Object.init(self)
        self.pos.x=WIDTH-self.width
        self.pos.y=HEIGHT-(self.height*number)
        scene:subscribe("touched", self, self.touched)
    end
     
    --# PauseButton
    PauseButton = class(Button)
     
    function PauseButton:init(number)
        self.img=readImage("Cargo Bot:Step Button")
        Button.init(self, number)
    end
     
    function PauseButton:action(touch)
        if touch.state==BEGAN then pause=Pause() end 
    end
     
    --# WorldObject
    WorldObject = class(Object) --everything in the game world that can be collided with
     
    function WorldObject:init(pos)
        Object.init(self, pos) 
        self.width=self.width*0.5 --scale down objects in world
        self.height = self.height * 0.5
        self.radius=math.min(self.width, self.height)*0.4 --...used for (generous) collision detection
    end
     
    --# Tree
    Tree = class(WorldObject)
     
    function Tree:init(pos)
        self.id="tree"   
        self.img=readImage("Planet Cute:Tree Tall")    
        WorldObject.init(self, pos)
    end
     
    --# Goal
    Goal = class(WorldObject)
     
    function Goal:init(pos)
        self.id="goal"
        self.img=readImage("SpaceCute:Planet")
        WorldObject.init(self, pos)
    end
    --# Agent
    Agent = class(WorldObject) --anything that moves...
     
    function Agent:init(pos)
        WorldObject.init(self, pos)
        self.angle=0
        self.delta=vec2(0,0)
         --   self.bound={a=vec2(-w,-h), b=vec2(WIDTH+w,HEIGHT+h)}
        self.bound={a=vec2(0,0), b=vec2(WIDTH, HEIGHT)}
        scene:subscribe("move", self, self.move) --...subscribes to move
    end
     
    function Agent:move()
        --bounds
        if self.pos.x>self.bound.b.x then
            self:outBounds("x", self.bound.b.x)
        elseif self.pos.x<self.bound.a.x then 
            self:outBounds("x", self.bound.a.x)
        end    
        if self.pos.y>self.bound.b.y then
            self:outBounds("y", self.bound.b.y)
        elseif self.pos.y<self.bound.a.y then 
            self:outBounds("y", self.bound.a.y)
        end
        --move, decelerate
        self.pos = self.pos + self.delta
        self.delta = self.delta * 0.75
     
    end
     
    function Agent:draw()
        pushMatrix()
        translate(self.pos.x, self.pos.y)
        rotate(math.deg(self.angle))
        sprite(self.img,0,0,self.width)
        popMatrix()    
    end
     
    function Agent:outBounds(axis,limit)
        self.pos[axis]=limit
        self.delta[axis]=-self.delta[axis]
    end
     
    --# Hero
    Hero = class(Agent) --this is you
     
    function Hero:init(pos)
        self.id="cat girl"    
        self.img=readImage("Planet Cute:Character Cat Girl")    
        self.speed=15
        Agent.init(self, pos)
        scene:subscribe("touched", self, self.touched)      
    end
     
    function Hero:move()
        Agent.move(self)    
        --detect collisions
        self.hiding=false
        for a,b in ipairs(scene.worldObjects) do
            if self.pos:dist(b.pos)<(self.radius+b.radius) then
                if b.id=="goal" then
                    gameOver=GameOver(true) --win flag set to true
                    return --exit collision loop
                elseif b.id=="tree" then
                    scene:dispatch("catMoves", nil) --conceal cat position
                    self.hiding=true
                elseif b.id=="evil bug" and not self.hiding then
                    gameOver=GameOver()
                    return --exit collision loop
                end
            end
        end
    end
     
    function Hero:touched(touch) --try changing touched to action to turn off offset handling
        self.delta=vec2(touch.deltaX,touch.deltaY) 
      --  self.pos=vec2(touch.x,touch.y)
        if not self.hiding then scene:dispatch("catMoves", self.pos) end --broadcast cat pos 
    end
     
    --# AI
    AI = class(Agent)
     
    function AI:init(pos)
        self.seed=math.random(1000)
        self.mult=1.5 + math.random() * 1.5
        self.turn=0
        Agent.init(self, pos)
    end
     
    function AI:calculate() --produce organic seeming quasi-random movement
        local pixel = noise((self.seed+self.pos.x)*.005, (self.seed+self.pos.y)*.005)
        local brightness = (pixel+1) / 2 * self.mult
        self.speed = brightness * self.mult * (1+(difficulty*0.05)) 
        self.angle = brightness * 360 * math.pi / 180 + self.turn
    end
     
    function AI:outBounds(axis, limit) --turn bug round when it gets to bound
        self.pos[axis]=limit
        self.seed = self.seed - 0.01
        self.turn = self.turn - math.pi/100
    end
     
    --# Bug
    Bug = class(AI)
     
    function Bug:init(pos)
        self.id="evil bug"        
        self.img=readImage("Planet Cute:Enemy Bug")
        self.speed=difficulty*2 
        scene:subscribe("catMoves", self, self.chase)
      --  current:subscribe("catMoves", self, function(target) self.target=target end) --nb anonymous functions work, tho not sure how you pass args to them
         --   scene:subscribe("testButton", self, self.testButton)
        AI.init(self, pos)
    end
     
    function Bug:move()
        if self.target then
            local diff=self.target-self.pos
            self.angle=math.atan2(diff.y,diff.x)
        else
            AI.calculate(self)
        end
        self.delta.x = math.cos(self.angle) * self.speed
        self.delta.y = math.sin(self.angle) * self.speed
        Agent.move(self)
    end
     
    function Bug:chase(target)
        self.target=target   
    end
  • Jmv38Jmv38 Mod
    edited February 2015 Posts: 3,295

    your event class is similar to the one i use. mng:on() and mng:trigger() are lke suscribe and dispatch. Mine is the result of various improvements by several people on the forum, and i've used it a lot with 100% satisfaction, so i'll stick to it.
    Concerning extending a class (as i do) or subclassing (as you do) the result is exactly the same. My method (which is really to be credited to @toadkick) is maybe a bit more versatile, because my scene class is derived from a panel class, and i then add the event methods to it with extend(). Codea class() does not accept several superclasses in the definition.

  • Posts: 2,020

    It's now on Codea Community too.

  • Posts: 2,020

    @Jmv38 yeah, I'll have to experiment with extending the table. I can see that it allows more flexibility. Although I do like the elegance of events being restricted to a given scope. Sometimes restrictions can be helpful!

  • Posts: 2,020

    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??"

  • dave1707dave1707 Mod
    Posts: 7,553

    @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
    
  • edited February 2015 Posts: 2,020

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

  • Posts: 2,020

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

  • dave1707dave1707 Mod
    edited February 2015 Posts: 7,553

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

  • Posts: 2,020

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

  • dave1707dave1707 Mod
    Posts: 7,553

    @yojimbo2000 I'm using an iPadAir

  • edited February 2015 Posts: 2,020

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

  • Posts: 2,020

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

  • dave1707dave1707 Mod
    Posts: 7,553

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

  • edited February 2015 Posts: 2,020

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

  • Posts: 2,020

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

  • Posts: 2,020

    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)

  • Jmv38Jmv38 Mod
    Posts: 3,295

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

  • Jmv38Jmv38 Mod
    Posts: 3,295

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

  • Jmv38Jmv38 Mod
    Posts: 3,295

    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.

  • Jmv38Jmv38 Mod
    edited February 2015 Posts: 3,295

    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
  • Jmv38Jmv38 Mod
    Posts: 3,295

    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.

  • Jmv38Jmv38 Mod
    Posts: 3,295

    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

  • Jmv38Jmv38 Mod
    Posts: 3,295

    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.

  • edited February 2015 Posts: 2,020

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

  • Jmv38Jmv38 Mod
    Posts: 3,295

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

Sign In or Register to comment.