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!
Using coroutines to create an RPG dialogue system
Re: Using coroutines to create an RPG dialogue system
Interesting article. Coroutines are definitely a nice way to avoid "callback hell."
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).
Or, if that still feels like too much redundancy,
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.
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.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
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
)()
Code: Select all
Chain(
Bind(say, "Hello there!"),
Bind(say, "How's it going?"),
Bind(say, "Well, nice talking to you")
)()
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"
)()
Last edited by airstruck on Thu Mar 31, 2016 11:19 pm, edited 2 times in total.
Re: Using coroutines to create an RPG dialogue system
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.
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.
Re: Using coroutines to create an RPG dialogue system
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
Sorry I took a while to respond, thanks for sharing
Pevzi: let me know when your game is available in English, I'd love to play it
Sorry I took a while to respond, thanks for sharing
Re: Using coroutines to create an RPG dialogue system
Can someone explain why the tutorial second and third part does not work?
Here is the love file of the above code.
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.
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...
(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.)
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
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.
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)
Re: Using coroutines to create an RPG dialogue system
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.
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 285 times
Re: Using coroutines to create an RPG dialogue system
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?pgimeno wrote:The error message could certainly be more friendly, though.
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.Pangit wrote:Can someone explain why the tutorial second and third part does not work?
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.
Re: Using coroutines to create an RPG dialogue system
Truth.pgimeno wrote: The error message could certainly be more friendly, though.
Thank you for taking the time to do this, this is great - much appreciated.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.
Re: Using coroutines to create an RPG dialogue system
Entering the rabbit hole... I looked at it and thought WTF is this? Its almost as helpfull as the nogame message lol.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?
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.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 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.
Re: Using coroutines to create an RPG dialogue system
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").Pangit wrote:my simplistic understanding of co-routines in lua is they are a kind of poor man's thread.
Who is online
Users browsing this forum: Bing [Bot], Google [Bot] and 0 guests