6 ms·
I've done this kind of thing in Lua before, but what troubles me about it is the fact that a lot of the state of your game ends up encoded in the Lua stack. Wh
by SomeCallMeTim 10y ago
I've done this kind of thing in Lua before, but what troubles me about it is the fact that a lot of the state of your game ends up encoded in the Lua stack.
What happens when the user wants to save? There's Pluto [1], which is somewhat slow for complex state. And it doesn't work on LuaJIT code -- and I tend to like to run games on LuaJIT.
And it's opaque: If you're debugging some kind of interaction in the game, you can't just look at the save state to see what's where.
I used to be a big fan of Lua, and this kind of AI and behavior coding was a component of that. But ... I think that, on balance, actually creating a data format that represents the state and "coding" using that plus state machines is actually a more robust and useful way to accomplish the same thing.
[1] http://lua-users.org/wiki/PlutoLibrary http://lua-users.org/wiki/PlutoLibrary
- stevekemp 10y agoI've certainly hit situations in the past where the Lua stack became too large, and caused all kinds of problems in debugging. But despite that I think coroutines have their place, and this was a nice example that I found when I was looking for inspiration. (I've been working on a Lua-scripted mail client for the past couple of years, and I'm starting to think more seriously about async-behaviour.)
- SomeCallMeTim 10y agoCoroutines are awesome, don't get me wrong. BUT, contrary to what I believed when I was still working with Lua, making the coroutine explicit (like async/await in C# and JavaScript ES2017) is actually even better. In particular, when your coroutines are explicit, you can say "queue up this task, this task, and this task, and continue here when they're ALL done." So when you're dealing with several tasks that might take different amounts of time to complete, they can do the work in parallel. I went from being a major Lua fanboy and posting frequently to the mailing list to drifting away from involvement to abandoning the language, in a relatively short period of time. I even wrote a blog entry about it. [1] I still think Lua has better "bones" than JavaScript, but with TypeScript, async/await, and JavaScript JIT engines everywhere, TypeScript just wins for me. [1] https://realmensch.org/2016/05/28/goodbye-lua/ https://realmensch.org/2016/05/28/goodbye-lua/
- daurnimator 10y agoThe problem with making coroutines (like async/await in JS) is http://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/ http://journal.stuffwithstuff.com/2015/02/01/what-color-is-y... This is one of the reasons I love lua's coroutines.
- unscaled 10y agoI assume this is exactly what SomeCallMeTim meant. Yes, explicitly awaitable futures/promises are slightly less ergonomic than coroutines, but this is a very reasonable trade-off. You give up: 1. Extra cognitive load every time you need to await. 2. You've got to be more careful when designing some generic higher-order functions (you need to decide which color their function arguments should be). In return you get: 1. Explicit concurrency. No function that you call unexpectedly suspends behind your back. In some cases this explicitness could be actually important (e.g. when using per-thread/process shared memory). 2. Futures/promises/tasks are first-class values. You could pass them along, save them for later, await on all or any of them and so forth. 3. Theoretically speaking, promises can be more memory efficient than coroutines since you don't need to allocate a full stack for each of them - just the state you need. "What Color Is Your Function" is misleading: there is no strictly superior solution for this tradeoff. Futures/promises encode concurrency as a monad and like every monad they are viral and once you start propagating them around, they can be a pain. But the alternative for not encoding some concern of your program as a monad is to let it pass through an implicit side-channel (I/O, error-handling, global state, built-in scheduler) that is harder to control and can't be treated as first-class object in the language. Go itself chose to go the explicit with error-handling. It doesn't even uses Monads, so it gets the worst of both worlds in my opinion, but the designers' main concern with exceptions which they managed to avert is still valid: with (unchecked) exceptions you never know which function you call is going to raise an error on you somewhere down the line. So yes, with Go you'll never have with unexpected throws, but you'll have to deal with unexpected goroutine suspension. In Go all of your functions will be happily colorless with regards to concurrency but will be colored with regards to error-handling. Again, both choices here are valid, but they are not so brain-dead simple or universal as this article is trying to put them.
- deleted 10y ago[deleted]
- buzzybee 10y agoOn the other hand, it's certainly more engineering to have to build a custom format. I implemented a story engine that compiles to behavior trees, and then subsequently to a small instruction set in this past year and, well, we're talking about three months to go from having no technology to having a pipeline capable of complex scenarios. But I came out of it with a much more competent strategy for addressing assets than anything I've done in the past, so I'm not complaining. It is the core technology of the game(choice driven, text simulation) so it needed a big investment. And the game is going to have content updates, which makes save games terrifying; In the end, I designed it so that it has episodes and restarting the game starts it from that episode so that I don't have to worry about massive amounts of persistent state getting permanently corrupted.
- gravypod 10y agoYou could do this in C with something like libdill as they give you a handle to a coroutine. All you need to do then is create a state_t that holds the coroutine reference managing the object, the channels for it, and all of the state needed for the computation. During saving you prune dead and save only the computation state. During loading you recreate all of your Objects. It wouldn't be all that slow and I think it would get over most of the overhead in some implementations I've seen of UI/Interaction code for games.
- SomeCallMeTim 10y agoI actually hacked together my own "coroutine" scripting library in C once upon a time. Lots of macros. Same problem though. When I said slow, I meant slow to save the entire state of the Lua VM. You can't even do that in C, so it doesn't really apply to the problem I was pointing out.
- gravypod 10y agoAll saveing your object state should be is memcpy'ing your array of all objects into some sort of static memory and on load looping through every object and starting a coroutine for every object. I don't think that's too much overhead.
- SomeCallMeTim 10y agoSorry, but you're totally missing the key point. Say you have an AI that's made 3 decisions and you're in the MIDDLE of a coroutine where it's waiting for the next criteria that will shift it to its next state. So the AI code looks like: local player = lookForAPlayer() if navigateToPlayerLocation() then local quest = givePlayerQuest(); while (!quest:done()) do waitWithStandardReply("Waiting for you to finish the quest!"); end if (quest:resolved()) then giveQuestReward(); while (true) do waitWithStandardReply("Thanks for finishing that quest for me!"; end else -- quest abandonded while (true) do waitWithStandardReply("Thanks for nothing!"); end end end This is rough example code, and not how I'd probably handle this case specifically, but it gives you the general idea. Each of those functions (except the quest member functions) has, somewhere inside it, one or more "yield()" statements, and some logic to make its goal happen incrementally. Yes, it's awesome to be able to code AI logic that way. But how do you save the coroutine state? The actual execution state of the coroutine itself, including what line it will resume next, the values of any bound variables, the values of any local variables, are all part of the state of the AI in that case. In C, you've got functions that may be relocated somewhere else in RAM and potentially stacks to save (if you're using a "real" coroutine library), so just doing a memcpy won't work. What would you memcpy to save the line that the script is paused on? In Lua, it can be done with Pluto, but then you can't use LuaJIT. I see libdill has the ability to use a user-supplied stack, but it doesn't appear to have a "resume" call that takes such a stack memory block, so the best that would happen is that the coroutine would start over from the beginning. Meaning that if you saved the above coroutine in the middle of a quest, the character involved would offer you a new quest when you saw them instead of accepting the result of a successfully allocated quest. If your object states are all being handled in traditional objects, then you're not using the coroutines in the way described by the article.
- pcwalton 10y ago> I think that, on balance, actually creating a data format that represents the state and "coding" using that plus state machines is actually a more robust and useful way to accomplish the same thing. Manually coding up a "state machine" is kind of reinventing the Lua interpreter, no?
- mmjaa 10y agoAs a long-term Lua fan, I'm with you. I think that the OP is really complaining about not understanding the Lua VM well enough to push it to its limits - in fact, coding up a 'proper state machine' that can compete with what the Lua VM, itself, has to offer is going to be very, very difficult. That said, I'm in favour of other approaches to this problem - using (i.e. generating) Lua bytecode itself as a way of maintaining state, and then pushing things around while having a good understanding of llimits.h seems to be quite important ..
- SomeCallMeTim 10y agoSorry, but you're pretty profoundly wrong. I have written an entire (popular) game engine that tightly integrated Lua and used custom-generated C++ bindings. I had to dig pretty deep into the Lua VM to accomplish that. Heck, I found and fixed a bug in the LuaJIT 1.x series -- my fix integrated by Mike Pall, with a credit on the changes page. The thing is if you encode game state into the program state, like using coroutines for complex, long-running AI tasks, for instance, then there's no way to save or restore that state if you're using LuaJIT. And Pluto saves the state in an opaque manner. And as another commenter pointed out, if you want to update the game with new behaviors, god knows how that will interact with save states that have huge call stacks in them. And bug fixes? Yeah, since you're saving the whole program state at this point, bug fixes won't be incorporated in the restored image.
- optionalparens 10y agoAgreed. I've seen things like this tried in Smalltalk images and it was usually a disaster long-term, meaning that the state was simply thrown out and a new image spun up because you're SOL. Either that, or you need tons of logic to handle the old program state and it becomes a convoluted mess. Most of the games and engines I've ever worked on that have dialog systems expressly avoid encoding things into the program state as much as reasonably possible. This is not only for restoring state, but also making for a saner experience editing and changing things during development. Very rarely does the format you encode things like dialog stay the same during a dev cycle unless you're using a canned engine and don't make any serious changes. Using Lua program state itself seems like a way to drive yourself mad by the end of the dev cycle, and make it incredible unfriendly to work with writers and other content creators. People who work with asset pipelines often do a lot of cruel and unusual data conversions and munging to bridge how different people and systems need to work, but eventually things in the actual game engine are best kept as simple data that can be read in and reproduced at any time. Moreover, as a developer, I should be able to load up any state of my game at any point in the game just by providing the necessary input data as possible, and like a pure function, it should produce the same game state every time if possible. Let's also not forget other things like internationalization and localization. As far as interfacing with Lua, I've mostly done it in the last few decades in a simple way where we handed entity component-style data that is just pure data, i.e. no function pointers and crazy stuff, and either let Lua do the rest with that state, or send pure data back to the C++ when needed. As for the data format itself, I've seen everything from XML to custom binary formats to CSV and Excel used for dialog. I think I'd sooner murder someone if game dialog lived in code, even in a Lua script.
- jamesu 10y agoAn additional problem is what happens when you need to patch the game? If you need to modify some form of quest or dialogue script written as a coroutine, how do you migrate the state? What if you've changed the script 5 times in a series of patches, and the user is still on version X? Then again you still have this problem even if you power your quest/dialog with data instead. Without sufficient information it's practically impossible to make a smooth migration.
- leggomylibro 10y agoThat is definitely an issue with this sort of thing; I think that you'd need to organize your conversations in a tree structure, and have 'save/load' methods for each node that could transcribe between a serialized data format like json or yaml, and functions in lua. With a format like that, you have the option of writing your dialogues in code or plain text, and easily switching between the two. The only major downside is that you can't easily define arbitrary behavior that way; every unique action that a dialogue event could trigger would need to be transcribed in your 'save/load' methods, which may feel limiting if you have a lot of hard-to-generalize logic.