Closure scheduler/ sequencer: for coding easy behaviours (ie AI behaviours)

If you use motion graphics software, you know there’re two powerful options for animating things. One is tweens. Codea has a great tween library, and I use it a lot. Tweens are great. But sometimes, you don’t want a tween, you want a behaviour: “Continue going left until you hit a wall/ the side of the screen” etc. What if you could sequence together a string of behaviour modules as easily as you create a tween sequence?

Here’s v1.1 of my coroutine sequencer. Now coding different behaviours is as easy as:

 self.behaviour = coroutine.sequence(
    walkLeft(startPos, endPos, speed), 
    wait(1, throwBomb, self, "BANG!"),
    walkRight(endPos, startPos, speed),
    wait(1)
    )

A couple of nice features to note. I use a recycling function, so that the sequencer just creates and reuses one thread per entity, to which various functions are passed. Secondly, and TBH i didn’t anticipate this, the scheduler can save and remember states (walkleft remembers the size of the object from the previous run through, toggleGrey remembers the initial colour and toggleState without needing any new input). This is incredibly useful for keeping behaviours modular. ie the AI instance itself never has to remember its own colour. That animation is all handled by the coroutine. Sweet. :)>-

--# Main
-- Coroutine scheduler, for programming AI and other behaviours. by Yojimbo 2000
-- Coroutine recycling function from http://www.lua.org/gems/sample.pdf

function setup()
    ai = {}
    local cent = vec2(WIDTH, HEIGHT) * 0.5
    for i=1,200 do
        local startP = vec2(cent.x + math.random()*cent.x, math.random()*HEIGHT)
        local endP = vec2(math.random()*cent.x, startP.y)
        local col = color(math.random(255),math.random(255),math.random(255))
        ai[i] = PatrollingAI(startP, endP, col)
    end
    profiler.init()
    strokeWidth(5)
end

function draw()
    background(40, 40, 50)
     
    for i=1, #ai do
        ai[i]:draw()
    end
    profiler.draw()
    PatrollingAI.mesh:draw()
end

profiler={}

function profiler.init(quiet)    
    profiler.del=0
    profiler.c=0
    profiler.fps=0
    profiler.mem=0
    if not quiet then
        parameter.watch("profiler.fps")
        parameter.watch("profiler.mem")
    end
end

function profiler.draw()
    profiler.del = profiler.del +  DeltaTime
    profiler.c = profiler.c + 1
    if profiler.c==10 then
        profiler.fps=profiler.c/profiler.del
        profiler.del=0
        profiler.c=0
        profiler.mem=collectgarbage("count", 2)
    end
end

--# CoroutineSequenceOld
coroutine.sequence = class() --v1.0 by Yojimbo2000. Creates a new thread for each behaviour, creates them over on each reset. Uses about 2.5 x more memory than v 1.1. Fps seems to be similar though.

function coroutine.sequence:init(...)
    self.args={...}
    self:reset()
end

function coroutine.sequence:reset() --reset function is probably a little heavy. Think I know a way lighter way to do this
    self.r = {}
    for i,v in ipairs(self.args) do
        self.r[i]=coroutine.create(v)
    end
    self.c = 1 --current
    self.status = "running" 
end

function coroutine.sequence:resume() 
    local ok,out1,out2,out3 = coroutine.resume( self.r[self.c]) --is there a better way to receive varargs?
    if coroutine.status(self.r[self.c]) == "dead" then
        if self.c < #self.r then 
            self.c = self.c + 1 
        else
          --  self.status = "dead" --run once
            self:reset() --loop endlessly
        end
    end
    return out1, out2, out3 --ok is discarded, these will be nil if routine has completed (or if it returns no values, such as Wait).
end

--# CoroutineSequence
coroutine.sequence = class() --v1.1 by Yojimbo2000. Just uses a single thread for each AI! 

function coroutine.sequence:init(...)
    self.r={...} -- remember the functions created (also saves the states of variables therein)
    self.c = 1 --counter
    self.routine = coroutine.create( 
    function(f)
        while f do --nb a single thread for each ai, which a variety of behaviours can be passed to
            f = coroutine.yield(f()) 
        end --this recycled coroutine idea comes from http://www.lua.org/gems/sample.pdf
    end)
    
end

function coroutine.sequence:resume() 
    local _,t = coroutine.resume( self.routine, self.r[self.c]) 
    if t == nil then --nb the recycle routine always returns status "suspended", ok "true", so only way to check whether a function has finished is with nil output. Would be nice to fix this
        if self.c < #self.r then 
            self.c = self.c + 1 
        else
            self.c = 1 --loop endlessly
        end
    end
    return t  --ok is discarded, these will be nil if routine has completed.
end

--# AIBehaviours
function walkLeft(startPos, endPos, speed) --example of a coroutine factory  
    local size = 1 --put this line inside the func if you want it to reset on each run through
    return function() --nb func has no runtime args....
        local x, dx, sp = startPos.x, endPos.x, speed --...because we bake the values declared in the coroutine.sequence command into the func...
        while x>dx do
            x = x - sp
            size = math.min(2,size * 1.001) --whenever Ai walks left, they grow a little in size
            coroutine.yield({x=x, size=size}) --return a table of values
        end
    end
end

function walkRight(startPos, endPos, speed) 
    return function() 
        local x, dx, sp = startPos.x, endPos.x, speed 
        while x<dx do
            x = x + sp
            coroutine.yield({x=x})
        end
    end
end

function wait(delay, callback, entity, ...) 
    local args = {...} --have to do this here, cannot call varargs inside a nested function
    return function() 
        local frames, t = delay * 60, 0 
        local callback = callback        
        while t<frames do
            t = t + 1
            coroutine.yield({}) --nb behaviours must not return "nil" as they'll trigger the next function. So return an empty table instead
        end
        if callback then callback(entity, unpack(args)) end
    end
end

function toggleGrey(col)
    local col = col --saves original color
    local toggle = 1 --remembers a toggle state
    local grey = color(128, 128)
    return function()  
        local tone
        if toggle == 1 then tone = grey else tone = col end
        coroutine.yield({col=tone})
        toggle = 3 - toggle --toggles between 1 and 2
    end
end

virtual={} --some virtual methods

function virtual:throwBomb(note) 
  --  print (note)
    sound(SOUND_EXPLODE, 4501)
end


--# PatrollingAI
PatrollingAI = class()

PatrollingAI.mesh = mesh()
PatrollingAI.mesh.texture = readImage("Planet Cute:Character Cat Girl")
local w,h = 50, 85

function PatrollingAI:init(startPos, endPos, col)
    
    local speed = 1 + math.random() * 4
    
    local toggle = toggleGrey(col)
    
    self.behaviour = coroutine.sequence(
    walkLeft(startPos, endPos, speed),
    toggle, 
    wait(1),
    walkRight(endPos, startPos, speed),
    toggle, --ie this is the same instance of toggleGrey function as above, so state is remembered
    wait(1, virtual.throwBomb, self, "BANG!") --callback could be to this objects method, ie self.doSomething
    )
    
    self.pos = vec2(startPos.x, startPos.y) --nb as vec2 is a kind of table need to make sure pos value is independent (not shallow) copy of startpos. Otherwise, walkRight (below) doesnt work.
    self.size = 1
    self.rect = PatrollingAI.mesh:addRect(self.pos.x, self.pos.y, w, h)
    self:color(col)
end

function PatrollingAI:draw()
    local t = self.behaviour:resume() --nb t will be nil if routine has finished
    if t then  --so check there is a value before assigning it
        self.pos.x = t.x or self.pos.x
        self.size = t.size or self.size
        if t.col then self:color(t.col) end
    end
    PatrollingAI.mesh:setRect(self.rect, self.pos.x, self.pos.y, w*self.size, h*self.size)
end

function PatrollingAI:color(col)
    PatrollingAI.mesh:setRectColor(self.rect, col)
end

I should add that the inspiration for this came from here:

http://www.randygaul.net/2014/01/01/component-based-design-lua-components-and-coroutines/

Though he doesn’t discuss the backend (and seems to imply that his example is pseudocode?)

looks very interesting.

I don’t know why coroutines aren’t used more to be honest. You can make very easy to write, decoupled code with them.

The link above mentions concerns about performance (without giving any references sadly), so that’s why I was keen to try this stress test with a profiler. Making each sprite bigger does slow things down a bit, but if you take that line out of the walk left routine, I can have 500+ ai (which is way more than you’d ever want in a game) and keep 60 fps (iPad air 1). The recycling routine ensures that each entity only uses a single thread, so that keeps things under control (if you change the order of the tabs, you can use the older, non-recycling version. It uses way more memory, but seems to have the same FPS).

AI behaviour is an obvious application for this, but you could actually do quite a lot of game logic with this, I think.

I’m going to add this to my entity/ component engine, which is shaping up quite nicely.

I haven’t examined it in depth, but I’m thinking I could do the same thing with a list of behaviours and a state variable, which Is way simpler and probably faster. I don’t see the benefits of using coroutines in this case.

This is what I was thinking of.

self.behaviour = {walkLeft,{startPos, endPos, speed}}, 
   { wait,{1, throwBomb, self, "BANG!"}},
   {walkRight,{endPos, startPos, speed),wait(1)}}}

self.current=1 
self.b=self.behaviour[self.current]
self.b[1](self.b[2]) --start first behaviour

--update current behaviour
function self:Behave()
    t=self.b[1]
    if not t then --change behaviour
        self.current=self.current+1
        self.b=self.behaviour[self.current]
        t=self.b[1](self.b[2])
    end
    --use the return values in table t
end

--returns nil when behaviour finishes
function walkLeft(startPos, endPos, speed) 
    return function() 
        local x, dx, sp = startPos.x, endPos.x, speed 
        if x>dx then
            x = x - sp
            size = math.min(2,size * 1.001) 
            return {x, size}
        end
    end
end

coroutines are awesome. I agree that they should be used more in Codea.

I use them in my Penrose Tile project: http://youtu.be/HMe4YefCE2Q They are a great way to split up a series of actions over a number of frames without needing a central controller that remembers everything’s state.

I can see the value in something like tiling, but if you’re using them for AI in a game, an entity’s behaviour is quite often going to get interrupted by the player, so animation sequences have to be capable of being cancelled easily, which means you do need a central controller and state memory to manage that anyway.

(And my controller above is about three lines of code, so it’s hardly a burden).

@LoopSpace very nice video. The colors are sweet. Would you share the project?

@Jmv38 - This looks like it

https://gist.github.com/loopspace/10527853

@yojimbo2000 - I think the point I’m trying to make is that you already have a coroutine in Codea (the draw function) that does what you need. If all your coroutine is doing is incrementing an x value at every frame, the draw function can manage that perfectly well, without any help, and with minimal code.

And I think that’s one reason why we tend not to need coroutines much in Codea - because we already have one built in.

@Ignatz thanks. Do you know a tool to load these projects described in multiple github files?

no, you’ll have to ask a github guru

There was one by Briarfox, i think, that was creating this project tabs list. Dont remember where i’ve put it nor if it still works, though…

bravo

@Ignatz you could be right, that we don’t actually need the coroutines, and maybe the most interesting/useful thing about the code I posted is the behaviour factories. But I think we need to do some testing before declaring one method to be easier or faster. I put together a quick comparison. It’s currently set to the state pointer method (you can switch back to the coroutine method by changing the order of the 2 behaviour/sequencer tabs). This is just a very quick test, and clearly the state pointer code I’ve implemented needs more work, eg I haven’t yet got the restart to work, or one-shot behaviours [EDIT both now fixed]. This could suggest that although a state pointer takes fewer lines to initiate, it needs more lines to recycle/ restart.

Of course these kinds of early, proof-of-concept tests are going to be somewhat contrived in terms of the behaviours I’ve programmed, so a somewhat more realistic testing scenario will be needed at some stage.

[EDIT v1.2 coroutine and state-pointer methods, now have feature parity, change order of tabs to compare performance]


--# Main
-- Coroutine scheduler, for programming AI and other behaviours. by Yojimbo 2000
-- Coroutine recycling function from http://www.lua.org/gems/sample.pdf
-- points to note:
-- a. the recycler just creates and reuses one thread per entity, to which various functions are passed. Cool huh?
-- b. the function-within-a-function serves as a state saver

function setup()
    ai = {}
    local cent = vec2(WIDTH, HEIGHT) * 0.5
    for i=1,500 do
        local startP = vec2(cent.x + math.random()*cent.x, math.random()*HEIGHT)
        local endP = vec2(math.random()*cent.x, startP.y)
        local col = color(math.random(255),math.random(255),math.random(255))
        ai[i] = PatrollingAI(startP, endP, col)
    end
    profiler.init()
    strokeWidth(5)
end

function draw()
    background(40, 40, 50)
     
    for i=1, #ai do
        ai[i]:draw()
    end
    profiler.draw()
    PatrollingAI.mesh:draw()
end

profiler={}

function profiler.init(quiet)    
    profiler.del=0
    profiler.c=0
    profiler.fps=0
    profiler.mem=0
    if not quiet then
        parameter.watch("profiler.fps")
        parameter.watch("profiler.mem")
    end
end

function profiler.draw()
    profiler.del = profiler.del +  DeltaTime
    profiler.c = profiler.c + 1
    if profiler.c==10 then
        profiler.fps=profiler.c/profiler.del
        profiler.del=0
        profiler.c=0
        profiler.mem=collectgarbage("count", 2)
    end
end

--# CoroutineSequence
coroutine.sequence = class() --v1.1 by Yojimbo2000. Just uses a single thread for each AI! 

function coroutine.sequence:init(...)
    self.r={...} -- remember the functions created (also saves the states of variables therein)
    self.c = 1 --counter
    self.routine = coroutine.create( 
    function(f)
        while f do --nb a single thread for each ai, which a variety of behaviours can be passed to
            f = coroutine.yield(f()) 
        end --this recycled coroutine idea comes from http://www.lua.org/gems/sample.pdf
    end)
    
end

function coroutine.sequence:resume() 
    local _,t = coroutine.resume( self.routine, self.r[self.c]) 
    if t == nil then --nb the recycle routine always returns status "suspended", ok "true", so only way to check whether a function has finished is with nil output. Would be nice to fix this
        if self.c < #self.r then 
            self.c = self.c + 1 
        else
            self.c = 1 --loop endlessly
        end
    end
    return t -- _ is discarded, t will be nil if routine has completed.
end

--# CoroutineSequence2
coroutine.sequence = class() --fork. attempt to create same effect with a state pointer (no coroutines).

function coroutine.sequence:init(...)
    self.r={...} -- remember the functions created
    self.c = 1 --counter
end

function coroutine.sequence:resume() 
    t = self.r[self.c]()
    if t == nil then --nb the recycle routine always returns status "suspended", ok "true", so only way to check whether a function has finished is with nil output. Would be nice to fix this
        if self.c < #self.r then 
            self.c = self.c + 1 
        else
            self.c = 1 
        end
    end
    return t -- _ is discarded, t will be nil if routine has completed.
end
--# AIBehaviours
function walkLeft(startX, dx, sp) --example of a coroutine factory  
    local size = 1 --put this line inside the func if you want it to reset on each run through
    return function() --nb func has no runtime args....
        local x = startX --...because we bake the values declared in the coroutine.sequence command into the func...
        while x>dx do
            x = x - sp
            size = math.min(2,size * 1.001) --whenever Ai walks left, they grow a little in size
            coroutine.yield({x=x, size=size}) --return a table of values
        end
    end
end

function walkRight(startX, dx, sp) 
    return function() 
        local x = startX
        while x<dx do
            x = x + sp
            coroutine.yield({x=x})
        end
    end
end

function wait(delay, callback, entity, ...) 
    local args = {...} --have to do this here, cannot call varargs inside a nested function
    return function() 
        local frames, t = delay * 60, 0   
        while t<frames do
            t = t + 1
            coroutine.yield({}) --nb behaviours must not return "nil" as they'll trigger the next function. So return an empty table instead
        end
        if callback then callback(entity, unpack(args)) end
    end
end

function toggleGrey(col)
    local toggle = 1 --remembers a toggle state
    local grey = color(128, 128)
    return function()  
        local tone
        if toggle == 1 then tone = grey else tone = col end
        coroutine.yield({col=tone})
        toggle = 3 - toggle --toggles between 1 and 2
    end
end

virtual={} --some virtual methods

function virtual:throwBomb(note) 
  --  print (note)
    sound(SOUND_BLIT, 4501)
end


--# AIBehaviours2
-- use return instead of yield, if not while
function walkLeft(startx, dx, sp) --example of a coroutine factory  
    local x, size  = startx, 1  --create x as a copy of startx    
    return function() --nb func has no runtime args, uses local env of nesting function         
        if x>dx then
            x = x - sp
            size = math.min(2,size * 1.001) --whenever Ai walks left, they grow a little in size
            return {x=x, size=size} --return a table of values          
        end
        x = startx --reset value (but DONT RETURN IT) ready for next loop through   
    end
end

function walkRight(startx, dx, sp) 
    local x = startx
    return function() 
        if x<dx then
            x = x + sp
            return {x=x}
        end
        x = startx
    end
end

function wait(delay, callback, entity, ...) 
    local args = {...} --have to do this here, cannot call varargs inside a nested function
    local frames, t = delay * 60, 0 
    return function() 
        if t<frames then
            t = t + 1
            return {} --nb behaviours must not return "nil" as they'll trigger the next function. So return an empty table instead
        end
        if callback then callback(entity, unpack(args)) end
        t = 0 --reset
    end
end

function toggleGrey(col) 
    local toggle = 1 --remembers a toggle state
    local grey, tone = color(128, 128), nil
    return function()     
        if tone == nil then --make sure this just runs once
            if toggle == 1 then tone = grey else tone = col end
            toggle = 3 - toggle --toggles between 1 and 2
            return {col=tone}
        end
        tone = nil --reset
    end
end

virtual={} --some virtual methods

function virtual:throwBomb(note) 
  --  print (note)
    sound(SOUND_BLIT, 4501)
end


--# PatrollingAI
PatrollingAI = class()

PatrollingAI.mesh = mesh()
PatrollingAI.mesh.texture = readImage("Planet Cute:Character Cat Girl")
local w,h = 50, 85

function PatrollingAI:init(startPos, endPos, col)
    
    local speed = 1 + math.random() * 4
    
    local toggle = toggleGrey(col)
    
    self.behaviour = coroutine.sequence(
    walkLeft(startPos.x, endPos.x, speed),
    wait(1),
    toggle,
    walkRight(endPos.x, startPos.x, speed),
    wait(1, virtual.throwBomb, self, "BANG!"), --callback could be to this objects method, ie self.doSomething
    toggle --ie this is the same instance of toggleGrey function as above, so state is remembered
    )
    
    self.pos = vec2(startPos.x, startPos.y) --nb as vec2 is a kind of table need to make sure pos value is independent (not shallow) copy of startpos. Otherwise, walkRight (below) doesnt work.
    self.size = 1
    self.rect = PatrollingAI.mesh:addRect(self.pos.x, self.pos.y, w, h)
    self:color(col)
end

function PatrollingAI:draw()
    local t = self.behaviour:resume() --nb t will be nil if routine has finished
    if t then  --so check there is a value before assigning it
        self.pos.x = t.x or self.pos.x
        self.size = t.size or self.size
        if t.col then self:color(t.col) end
    end
    PatrollingAI.mesh:setRect(self.rect, self.pos.x, self.pos.y, w*self.size, h*self.size)
end

function PatrollingAI:color(col)
    PatrollingAI.mesh:setRectColor(self.rect, col)
end

OK, I now understand why I couldn’t get the state-pointer code above to reset, and I think it’s key to why using a coroutine is useful.

With the state-pointer system suggested by @Ignatz , each frame you run the function from the very beginning. That means if the reset code local x= startx etc is inside the function loop, it resets every frame, and you don’t get any animation.

So with the state-pointer code above I put the reset code local x = startx before the nested function, which means it doesn’t run once the function is baked, and the reset/recycle code, as it stands, does nothing, and I can’t easily reuse the function once it has completed [EDIT put the reset code after the if loop with the return call].

But with a coroutine, each resume doesn’t fire the function from the beginning, just from the start of the while loop. This means you can put reset code inside the function (so with walkLeft I reset position before the while loop, but not size, so the sprite can continue to grow), and once the behaviour is dead, you can fire it again using the coroutine recycler, and it resets automatically, without any extra actions.

So the advantage of using a coroutine, (and coroutine.yield rather than return), is that you can have extra bits of code either side of the while loop, which only run when the coroutine is either started or has finished, but which don’t execute every frame.

I don’t see a way to implement this kind of behaviour reset/recycle function easily using the state-pointer system, short of rebaking the behaviour functions (ie running the self.routine = sequence( ... function again) on each loop of the sequence. I’d be happy to be proven wrong on this though. EDIT (see post below)

OK, as usual I spoke too soon, ignore most of my post above. The way to get the state-pointer behaviour functions to reset is (so obvious when you realise it), to put the reset code after the if statement with the return.

Like this:

function walkRight(startPos, endPos, speed) 
    local x, dx, sp = startPos.x, endPos.x, speed 
    return function() 
        if x<dx then
            x = x + sp
            return {x=x}
        end
        x, dx = startPos.x, endPos.x --NEW. Reset positions.
    end
end

think I must be going doolally not to have realised this 8-}

In terms of modularization though, the behaviour baking technique (ie a function that returns a function) is very interesting, as it creates an independent environment, with local variables, which are remembered and can be recycled, regardless of whether it’s a coroutine thread or a state-pointer that’s running them.

[EDIT] My post above now has updated code, with feature parity between the coroutine and state-pointer systems. Performance seems similar, but I agree now with @Ignatz that the state-pointer is simpler. I think at first that I thought that the independent local environment that each behaviour has was something created by the coroutine, whereas in fact it is created by the function-baker.

@Ignatz thanks, I’d seen closures in other’s code, but hadn’t really realised what they were, and I had no idea about lexical scoping, external local variables etc. Just reading this now:

http://www.lua.org/pil/6.1.html

and I see you have a tutorial on them too, I’ll look at that next.

I had no idea they created independent instantiations. They’re almost like mini-classes. This is definitely going to form an important part of my game engine.