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