## Using coroutines to create an RPG dialogue system

General discussion about LÖVE, Lua, game development, puns, and unicorns.
zorg
Party member
Posts: 2483
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: 1441
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()

Thrust II Reloaded - GifLoad for Löve - GSpöt GUI - My NotABug.org repositories - portland (mobile orientation)
The MS-Github repositories I had have been closed after the acquisition announcement and will be removed in the near future.

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

function MyGameState:runMenu()
local menu = MyMethodForBuildingMenus()
:withChaining()
:andOtherFeatures()
:finalizer()
-- add to your entities here possibly

local function loop()
if love.keyboard:isDown("space") then
local command = whateverEntitySystem:getOption(menu)
if command == "submenu" then
-- remove menu from entities
return self:runSubMenu()
end
else
-- call your entity system for updating generic menus
end
return loop
end
return loop
end

function MyGameState:runSubMenu()
-- 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()
local main_menu = systems.menubox.assembly.builder.new()
: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 menu_close_then = menu_open(self.entities, main_menu)
local function loop()
if input.tap.cancel then
menu_close_then(gamestate.pop)
elseif input.tap.action then
local command = main_menu.menu_config[main_menu.menu_selection].id
if command == "talk" then
if self.callback then self.callback() end
elseif command == "stats" then
return menu_close_then(self.run_stats_window, self)
end
else
local cmd = systems.menubox.update(self.entities)
if cmd == 'advance' then
graphics.set_dirty()
end
end
return loop
end
return loop
end

function states.action_menu:run_stats_window()
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 menu_close_then = menu_open(self.entities, stats_window)
local function loop()
if input.tap.cancel then
return menu_close_then(self.run_action_menu, self)
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)
self.menuStack[#self.menuStack + 1] = menu
-- A method that takes a callback, and starts a tween.
-- The callback will run when the tween is finished.
menu:slideIn(go)
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.
menu:awaitInput(go)
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.
if command.id == 'submenu' then
return self:MenuChain(command.submenu)
end
-- Command's ID not recognized; it's not a menu-related command. Let it do its own thing.
return self:MenuCommandChain(command)
end,
-- Slide menu off screen.
function (go)
menu:slideOut(go)
end,
-- Pop menu off the stack.
function (go)
assert(self.menuStack[#self.menuStack] == menu)
self.menuStack[#self.menuStack] = nil
end
)
end

function MyGameState:MenuCommandChain (command)
return Chain(
function (go)
if not command.execute then error 'bad menu command' end
-- 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)
local topMenu = self.menuStack[#self.menuStack]
if topMenu then
topMenu:update(dt)
end
end

function MyGameState:draw ()
for _, menu in ipairs(self.menuStack) do
menu:draw()
end
end

function MyGameState:init()
self.menuStack = {}

local menu = Menu(self)
:addItem { id = 'close', title = 'Close' }
:addItem { id = 'submenu', title = 'Crafting',
submenu = self:CraftingMenu() }
:addItem { id = 'submenu', title = 'Inventory',
submenu = self:InventoryMenu() }
:addItem {
title = 'Turn orange',
execute = function (cb)
orangeTween:start(cb)
end
}
:addItem {
title = 'Quit',
execute = function ()
os.exit()
end
}

self:MenuChain(menu)()
end
This example is also incomplete, of course. I'll probably be pushing some code to github soon that does something similar, will try to remember to post a link here.

### Who is online

Users browsing this forum: No registered users and 2 guests