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)