Page 2 of 2
Re: Using coroutines to create an RPG dialogue system
Posted: Sun Jul 10, 2016 4:27 am
by zorg
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.
Re: Using coroutines to create an RPG dialogue system
Posted: Sun Jul 10, 2016 10:02 am
by pgimeno
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:
Re: Using coroutines to create an RPG dialogue system
Posted: Sun Jul 10, 2016 10:14 am
by airstruck
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
Re: Using coroutines to create an RPG dialogue system
Posted: Sun Jul 10, 2016 5:54 pm
by Inny
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.
Re: Using coroutines to create an RPG dialogue system
Posted: Sun Jul 10, 2016 6:41 pm
by airstruck
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.
Re: Using coroutines to create an RPG dialogue system
Posted: Sun Jul 10, 2016 8:01 pm
by Inny
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.
Re: Using coroutines to create an RPG dialogue system
Posted: Sun Jul 10, 2016 9:34 pm
by airstruck
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.
Re: Using coroutines to create an RPG dialogue system
Posted: Sun Jul 10, 2016 10:00 pm
by Inny
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.
Re: Using coroutines to create an RPG dialogue system
Posted: Mon Jul 11, 2016 2:33 am
by airstruck
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.