Just thought I would share this little piece of code. One might use it to create scripted cutscenes in games.
If you want to create a cutscene there are basically 3 ways of doing it:
(1) Using callback functions and fire events after each other walk(hero, x, y, say(hero, "Hello", walk(hero, x, y, ...))
but you will end up with very deep nesting
(2) Using ‘finite state machines’ … but you will have to create a lot of variables to hold states of different events and a ton of if
nested condition checks
(3) Using Lua’s coroutines. This is what I offer here.
Boring explanation:
Here’s how it works. Each time you make a call to exec()
you have to pass a function. This function gets wrapped into a coroutine. Since it’s now technically a coroutine you can interrupt it at any point in execution. Good example is the wait()
function.
If we would call exec(wait, 3)
twice we would have effectivly created a queue of threads to work through. These threads are basically just plain functions. Technically both wait() calls would run after each other and block Codea’s main thread for 6 seconds, because of their while loops. But since they are coroutines they run on separate threads and we can run each one of them until the thread is ‘done’ and only then continue with the next.
As already told we can also interrupt each of the them after each frame (which is what happens in the while loop through coroutine.yield()). While a coroutine is interrupted it hands back controll to Codea’s main thead and lets it do its stuff like drawing, tweens, etc.
Essentially we are replicating an asynchronous behaviour here (just like with http requests) - but since we have a queue, we work our way through each thread separately, having remaining threads wait.
I made it so that each function (say, wait, move) receives its own thread as the first parameter (self reference). This way I can check if a function is actually a coroutine and if it is I make use of coroutine.yield - otherwise I call it as normal function, without the while loop. This is useful when you want to move() two objects at same time but want to use the same move() function that you use for the exec() coroutine.
I don’t know if its the best way of implementing cutscenes, but seems to work good so far.
Let me know if you have any improvements on the code.
--# Main
-- Scripted Cutscenes
local thread_queue = {}
local function exec(func, ...)
local params = {...}
local thread = function(self) func(self, unpack(params)) end
table.insert(thread_queue, coroutine.create(thread))
end
local function thread_update()
if #thread_queue > 0 then
if coroutine.status(thread_queue[1]) == "dead" then table.remove(thread_queue, 1)
else coroutine.resume(thread_queue[1], thread_queue[1]) end
end
end
local function wait(self, time)
local term = ElapsedTime + time
while ElapsedTime <= term do
if type(self) == "thread" then
coroutine.yield()
end
end
end
local function move(self, obj, x, y, speed)
local done
local report = function() done = true end
tween(speed or .1, obj, {x = x, y = y}, tween.easing.sineIn, report)
if type(self) == "thread" then
while not done do coroutine.yield() end
end
end
local function say(self, msg)
print(msg) -- this method is equal to "print" but doesn't write the thread self reference into console
end
function setup()
hero = {x = 60, y = 80, c = color(0, 255, 0)}
enemy = {x = 0, y = 0, c = color(255, 0, 0)}
exec(say, "Begin...")
exec(wait, .5)
exec(print, "Waited for .5 seconds")
exec(wait, 1)
exec(print, "Waited for 1 second")
exec(print, "Move hero and enemy after each other")
exec(move, hero, 120, 80)
exec(move, enemy, 60, 0)
exec(function()
hero.c = color(15, 215, 130)
enemy.c = color(120, 10, 200)
end)
exec(print, "Both have traveled and changed their color")
-- move hero and enemy simultaniously
exec(function()
move(nil, hero, hero.x + 100, hero.y, 3) -- set first param (thread reference) to nil
move(nil, enemy, enemy.x + 100, enemy.y, 3)
print("Both will now be translated simultaniously...")
end)
exec(say, "Done.")
end
function draw()
thread_update()
background(40, 40, 50)
fill(hero.c)
rect(hero.x, hero.y, 60, 80)
fill(enemy.c)
rect(enemy.x, enemy.y, 60, 80)
end