Howdy, Stranger!

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

In this Discussion

Cutscenes with Coroutines

in Code Sharing Posts: 423

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

Comments

  • SimeonSimeon Admin Mod
    Posts: 4,352

    I really like your system. It reminds me of the NPC scripting system in NeverWinter Nights. You basically created a queue of actions for an NPC to follow. Love how naturally this is recreated with coroutines.

  • Posts: 423

    @Simeon I'm really glad you liked it!

Sign In or Register to comment.