## Using coroutines to create an RPG dialogue system

General discussion about LÖVE, Lua, game development, puns, and unicorns.
zorg
Party member
Posts: 2654
Joined: Thu Dec 13, 2012 2:55 pm
Location: Absurdistan, Hungary
Contact:

### Re: Using coroutines to create an RPG dialogue system

airstruck wrote:They are the same as what other languages (ES6, Python, PHP, etc.) usually call generators.
Technically, generators like in python are a tad more limited than lua's coroutines. Also, luaJIT alleviates the "call across C boundary" issue a bit, but not fully.

That said, it's true that it's a hassle, so if one doesn't want to, they can make do without them.
Me and my stuff True Neutral Aspirant. Why, yes, i do indeed enjoy sarcastically correcting others when they make the most blatant of spelling mistakes. No bullying or trolling the innocent tho.

pgimeno
Party member
Posts: 1739
Joined: Sun Oct 18, 2015 2:58 pm

### Re: Using coroutines to create an RPG dialogue system

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?
Well, this file causes the "yield across C-call boundary" both with LÖVE and luajit:

Code: Select all

coroutine.yield()


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

Ah, well that explains it. It throws "attempt to yield from outside a coroutine" in PUC Lua, I assumed it would do the same in LuaJIT but should have checked. I just figured it was something screwy with Love, naturally

Inny
Party member
Posts: 652
Joined: Fri Jan 30, 2009 3:41 am
Location: New York

### Re: Using coroutines to create an RPG dialogue system

If people are having trouble with coroutines (and believe me, I love coroutines), then there's a pretty normal way to run a menu system with just closures/continuations. I'll try to demonstrate with code since that always speaks louder.

Code: Select all

-- assuming our gamestate's basic functions, init, update, draw, etc., I'll show minimal stubs
function MyGameState:update()
self.runner = self.runner()  -- Note here, always continuing with the last returned function
end

function MyGameState:init()
self.runner = self:runMenu() -- where it first comes from
end

:withChaining()
:andOtherFeatures()
:finalizer()

local function loop()
if love.keyboard:isDown("space") then
end
else
end
return loop
end
return loop
end

-- follows same structure as runMenu
end

In this example, the runMenu method uses a closure to capture all of the local variables it needs, manage the entities list, do the menu specific code, etc. What's really happening is the nested loop function is behaving like a while loop would in a coroutine, passing itself up to the update function as the next place to resume.

But again, I love me some coroutines, but clearly the C yield-boundary is a limitation and it's always important as a programmer to know the limitations of your favorite features so you don't try to use them where they don't belong.

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

Inny wrote:there's a pretty normal way to run a menu system with just closures/continuations
Interesting approach. I'd like to see an example of that being used in a in a game, do you know of one? It's a little hard for me to tell from your example which parts are meant to be repeated for different menus and which parts are meant to be reusable. I suspect it could be simplified a lot by leveraging some kind of promise-like pattern, but maybe I'm misreading it.

Inny
Party member
Posts: 652
Joined: Fri Jan 30, 2009 3:41 am
Location: New York

### Re: Using coroutines to create an RPG dialogue system

airstruck wrote:Interesting approach. I'd like to see an example of that being used in a in a game, do you know of one? It's a little hard for me to tell from your example which parts are meant to be repeated for different menus and which parts are meant to be reusable. I suspect it could be simplified a lot by leveraging some kind of promise-like pattern, but maybe I'm misreading it.
To expand on it a bit, here's some incomplete snippets from my personal one-hour-a-week-for-fun project

Code: Select all

function states.action_menu:run_action_menu()
local W, H = 18, 6
local sw, sh = graphics.max_window_size()
:at(math.floor((sw-W)/2), sh-H+1):size(W, H)
:option("Talk", "talk")
:option("Item", "use")
:option("Equip", "equip")
:option("Search", "search")
:option("Attack", "attack", math.floor(W/2)+1, 1)
:option("Magic", "mag")
:option("Status", "stats")
:option("System", "system")
:build()

local function loop()
if input.tap.cancel then
elseif input.tap.action then
if command == "talk" then
if self.callback then self.callback() end
elseif command == "stats" then
end
else
graphics.set_dirty()
end
end
return loop
end
return loop
end

local sw, sh = graphics.max_window_size()

local W, H = 20, 12
local stats_window = systems.drawboxes.assembly.new_window_builder()
:at(math.floor((sw-W)/2), math.floor((sh-H)/2)):size(W, H)
:text(("Level      %7i"):format(99), 1, 1)
:text(("Experience %7i"):format(99999), 1, 2)
:text(("Next Level %7i"):format(9999), 1, 3)
:text("-", 1, 4)
:text(("Hit Points %3i/%3i"):format(999, 999), 1, 5)
:text("", 1, 6)
:text("-", 1, 7)
:text("-", 1, 8)
:text("-", 1, 9)
:text(("Carry Weight %2i/%2i"):format(99, 99), 1, 10)
:build()

local function loop()
if input.tap.cancel then
end
return loop
end
return loop
end

I haven't had the chance to pare this down to its bare minimums yet, but it works for me.

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

Inny wrote:here's some incomplete snippets
Thanks for sharing, there's just too many unknowns in there to really comment on it though (for example where does input.tap.action come from, what does menu_open do, what happens in init and update, etc.). I see what you're getting at, more or less, but I think there are cleaner ways to handle it. You mentioned that this is a "pretty normal way to run a menu system," I took that to mean that you'd seen this done someplace else, more than once. Is there an example of this being used in a full game that we could download and look at?

I'm not trying to knock this approach, by the way, just trying to figure out what its merits are and how it compares to other solutions.

Inny
Party member
Posts: 652
Joined: Fri Jan 30, 2009 3:41 am
Location: New York

### Re: Using coroutines to create an RPG dialogue system

Oh, sorry, I used "normal" to mean "made of normal parts", not like "everyone in the love2d world uses it." Actually this technique is probably more used in the javascript world and not so much within love/lua, e.g. you would build out some DOM and bind a bunch of callbacks to their event handlers.

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

Inny wrote:Actually this technique is probably more used in the javascript world and not so much within love/lua
Fair enough, I've seen it there too. I think part of the reason promises and other generalized async patterns exist is to avoid that pattern, honestly (especially in JS). It can be hard to read and maintain in my experience. It's just a matter of preference, I guess, but I'd probably do something like this (assuming Chain function mentioned earlier exists, or you could do something similar using a promises implementation):

Code: Select all

function MyGameState:MenuChain (menu)
return Chain(
-- Push menu onto stack and slide it onto screen.
function (go)
-- A method that takes a callback, and starts a tween.
-- The callback will run when the tween is finished.
end,
-- Await user input.
function (go)
-- A method that takes a callback and awaits menu input.
-- The callback will run when input is received.
-- A command (user action) object is passed to the callback.
end,
-- Handle user input.
function (go, command)
-- If user is closing this menu, go to next link in chain now.
if command.id == 'close' then
go()
return
end
-- If it's a submenu, return a new menu chain for it.
end
-- Command's ID not recognized; it's not a menu-related command. Let it do its own thing.
end,
function (go)
end,
-- Pop menu off the stack.
function (go)
end
)
end

return Chain(
function (go)
-- A method that takes a callback and starts executing a command.
-- The callback will run when the command is finished.
command:execute(go)
end
)
end

function MyGameState:update (dt)
end
end

function MyGameState:draw ()
end
end

function MyGameState:init()

:addItem { id = 'close', title = 'Close' }
title = 'Turn orange',
execute = function (cb)
orangeTween:start(cb)
end
}
end