Using coroutines to create an RPG dialogue system

General discussion about LÖVE, Lua, game development, puns, and unicorns.
User avatar
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

Post by zorg » Sun Jul 10, 2016 4:27 am

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 :3True 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.

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

Re: Using coroutines to create an RPG dialogue system

Post by pgimeno » Sun Jul 10, 2016 10:02 am

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()

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 » Sun Jul 10, 2016 10:14 am

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 ;)

User avatar
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

Post by Inny » Sun Jul 10, 2016 5:54 pm

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.

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 » Sun Jul 10, 2016 6:41 pm

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.

User avatar
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

Post by Inny » Sun Jul 10, 2016 8:01 pm

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.

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 » Sun Jul 10, 2016 9:34 pm

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.

User avatar
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

Post by Inny » Sun Jul 10, 2016 10:00 pm

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.

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 » Mon Jul 11, 2016 2:33 am

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.

Post Reply

Who is online

Users browsing this forum: Bing [Bot], Google [Bot] and 2 guests