Using coroutines to create an RPG dialogue system

General discussion about LÖVE, Lua, game development, puns, and unicorns.
geckojsc
Prole
Posts: 4
Joined: Thu Aug 14, 2014 11:42 pm

Using coroutines to create an RPG dialogue system

Post by geckojsc »

Hey folks, I've been working on an RPG recently and came up with a novel solution for dialogue scripting, using coroutines and setfenv. I was considering turning it into a library but realised there are too many parts that are specific to the game at hand, so I decided to write a blog post/tutorial about it instead.

http://blog.geckojsc.com/2016/03/using- ... e-rpg.html

The main idea is that the function say(str) contains a call to coroutine.yield, so the dialogue can be written as a simple Lua script which suspends while the game displays the text and waits for the player to press OK.

I'm wondering what people think about this approach, and whether anyone has used a similar system before, or what other solutions are available?

Hope somebody finds it interesting/informative!
User avatar
airstruck
Party member
Posts: 650
Joined: Thu Jun 04, 2015 7:11 pm
Location: Not being time thief.

Re: Using coroutines to create an RPG dialogue system

Post by airstruck »

Interesting article. Coroutines are definitely a nice way to avoid "callback hell."
geckojsc wrote:I was considering turning it into a library but realised there are too many parts that are specific to the game at hand
For what it's worth, there are more generalized ways of "flattening" code with heavily nested callbacks; think promises/futures. I've experimented with two different approaches to that in Lua, one using chained functions and another using coroutines. The coroutines solution probably won't be interesting to you, but since you asked for other solutions, here's how a chained functions version of your nested callback example could look.

Code: Select all

Chain(
    function (go)
        say("Hello there!", go)
    end,
    function (go)
        say("How's it going?", go)
    end,
    function (go)
        say("Well, nice talking to you", go)
    end
)()
It's hairy, but the code is "flat" instead of nesting deeper and deeper, and it can be cleaned up pretty nicely with a bind function (as in Boost or JS bind).

Code: Select all

Chain(
    Bind(say, "Hello there!"),
    Bind(say, "How's it going?"),
    Bind(say, "Well, nice talking to you")
)()
Or, if that still feels like too much redundancy,

Code: Select all

local function S (phrase) return Bind(say, phrase) end

Chain(
    S "Hello there!",
    S "How's it going?",
    S "Well, nice talking to you"
)()
One nice thing about a generic approach like this is you can put tweens or anything else that takes a callback in the chain and keep everything flat.
Last edited by airstruck on Thu Mar 31, 2016 11:19 pm, edited 2 times in total.
pevzi
Prole
Posts: 27
Joined: Tue Apr 02, 2013 4:09 pm
Contact:

Re: Using coroutines to create an RPG dialogue system

Post by pevzi »

I used a similar approach in my game to implement a dialogue system (was somewhat inspired by airstruck's aforementioned library by the way). Coroutines are really well-suited for this kind of task.
PS: if you happen to be interested, the game is not really finished yet, I'm planning to expand it and translate it into English one day.
geckojsc
Prole
Posts: 4
Joined: Thu Aug 14, 2014 11:42 pm

Re: Using coroutines to create an RPG dialogue system

Post by geckojsc »

Airstruck: that's really awesome, both that you generalised the idea of function chaining in two different ways, and how concise you were able to get the syntax in the end.
Pevzi: let me know when your game is available in English, I'd love to play it :D

Sorry I took a while to respond, thanks for sharing :)
User avatar
Pangit
Party member
Posts: 148
Joined: Thu Jun 16, 2016 9:20 am

Re: Using coroutines to create an RPG dialogue system

Post by Pangit »

Can someone explain why the tutorial second and third part does not work?

Code: Select all

text = nil
routine = nil

function say(str)
    text = str
    coroutine.yield()
    text = nil
end

function run(script)
    -- load the script and wrap it in a coroutine
    local f = loadfile(script)
    routine = coroutine.create(f)
    
    -- begin execution of the script
    coroutine.resume(routine)
end

function love.update(dt)
    if not text then
        -- player movement code
    end
end

function love.draw()
    -- code to draw the world goes here

    if text then
        love.graphics.print(text, 10, 10)
    end
end

function love.keypressed(key, isRepeat)
    if text and key == "space" then
        if routine and coroutine.status(routine) ~= "dead" then
            -- execute the next part of the script
            coroutine.resume(routine)
        end
    end
end

say("Hello there!")                -- the script suspends once here
say("How's it going?")             -- it suspends again here
say("Well, nice talking to you!")  -- it suspends for the 3rd time here
Here is the love file of the above code.
part2.love
(581 Bytes) Downloaded 237 times
This is the second part of the tutorial.

The first part - works fine. but when you try and use the coroutines I get the following blue screen.
error4.jpg
error4.jpg (21.48 KiB) Viewed 9549 times
Here is the working first part of the tutorial. While its ugly - at least it works. However the magic is in what you can do in parts two and three...

Code: Select all

text = nil
callback = nil

function say(str, cb)
    text = str
    callback = cb
end

function love.update(dt)
    if not text then
        -- player movement code
    end
end

function love.draw()
    -- code to draw the world goes here
    
    if text then
        love.graphics.print(text, 10, 10)
    end
end

function love.keypressed(key, isRepeat)
    if text and key == "space" then
        text = nil
        if callback then
            -- execute the next part of the script
            callback()
        end
    end
end

say("Hello there!", function ()
    say("How's it going?", function ()
        say("Well, nice talking to you!")
    end)
end)
part1.love
(495 Bytes) Downloaded 249 times
(I have included a love file of the first part - only thing i have added is a escape function so you can exit the program.)
User avatar
pgimeno
Party member
Posts: 3544
Joined: Sun Oct 18, 2015 2:58 pm

Re: Using coroutines to create an RPG dialogue system

Post by pgimeno »

The coroutine is not created. The say("Hello there!") etc. are supposed to be in a separate file that you run with run(filename). Pay special attention to the part that says "And finally, we can write a script that looks like this:"

I believe that "a script" means "a Lua file".

The error message could certainly be more friendly, though.

There's a problem with the tutorial. loadfile should be replaced with love.filesystem.load in order to get it to work with files inside the .love file.

Attached is an example with that change.
Attachments
part2-fixed.love
(779 Bytes) Downloaded 234 times
User avatar
airstruck
Party member
Posts: 650
Joined: Thu Jun 04, 2015 7:11 pm
Location: Not being time thief.

Re: Using coroutines to create an RPG dialogue system

Post by airstruck »

pgimeno wrote:The error message could certainly be more friendly, though.
It's strange seeing that error message in this situation, I'd expect "attempt to yield from outside a coroutine" instead. That's what you'd normally get from executing coroutine.yield outside of a coroutine (try it in the REPL). Does Love wrap everything in a coroutine for some reason?
Pangit wrote:Can someone explain why the tutorial second and third part does not work?
I would advise against using Lua's coroutines if you don't know exactly what you're doing. From what I've seen, even seasoned programmers often are confused by them or don't really get how they work. Even with a good understanding of coroutines, you'll inevitably get bitten by the "yield across C boundary" thing at some point. Aside from not being well understood, and not playing well with C code, coroutines also don't mesh well with JIT-compiled code.

Instead of coroutines, I'd recommend a more "mundane" approach, using something akin to promises/futures to set up a series of async operations. I touched on this in an earlier comment, and linked a generic solution that should work well for this kind of thing. I hope I've been direct enough now about why I think that's a better solution in this case.
User avatar
Pangit
Party member
Posts: 148
Joined: Thu Jun 16, 2016 9:20 am

Re: Using coroutines to create an RPG dialogue system

Post by Pangit »

pgimeno wrote: The error message could certainly be more friendly, though.
Truth.
pgimeno wrote: There's a problem with the tutorial. loadfile should be replaced with love.filesystem.load in order to get it to work with files inside the .love file.

Attached is an example with that change.
Thank you for taking the time to do this, this is great - much appreciated. :D
User avatar
Pangit
Party member
Posts: 148
Joined: Thu Jun 16, 2016 9:20 am

Re: Using coroutines to create an RPG dialogue system

Post by Pangit »

airstruck wrote: It's strange seeing that error message in this situation, I'd expect "attempt to yield from outside a coroutine" instead. That's what you'd normally get from executing coroutine.yield outside of a coroutine (try it in the REPL). Does Love wrap everything in a coroutine for some reason?
Entering the rabbit hole... I looked at it and thought WTF is this? Its almost as helpfull as the nogame message lol.
airstruck wrote: .. you'll inevitably get bitten by the "yield across C boundary" thing at some point. Aside from not being well understood, and not playing well with C code, coroutines also don't mesh well with JIT-compiled code.
I had not really thought about this but if its so badly understood and supported, my simplistic understanding of co-routines in lua is they are a kind of poor man's thread. Point taken about steering clear of the use of coroutines, it is a shame as that was a neat way to get scripting of the messages in game working.

I am guessing somewhere either on a dev position paper, or a mailing list archive there has to be more information somewhere on how it was implemented in lua. But I'm probably wrong. I get the impression that most of the stuff under the hood is a brutal hack just to get it to work.

Anyway thanks a bunch for your advice, that was useful and I got some new things to think about and investigate. :cool:
User avatar
airstruck
Party member
Posts: 650
Joined: Thu Jun 04, 2015 7:11 pm
Location: Not being time thief.

Re: Using coroutines to create an RPG dialogue system

Post by airstruck »

Pangit wrote:my simplistic understanding of co-routines in lua is they are a kind of poor man's thread.
They are the same as what other languages (ES6, Python, PHP, etc.) usually call generators. They're basically just functions with "yield" instead of return. Yield works like return in that control is returned to the point where the "function" was called, but is unlike return in that the "function" (or generator, or coroutine) is suspended in the state it was in when it yielded, and calling it again will resume it with that state from that point. Normally this is a great and useful feature, but being unable to yield across C functions makes it less useful in a framework like Love (or even plain Lua; consider "require" and "pcall").
Post Reply

Who is online

Users browsing this forum: No registered users and 63 guests