Lua, YAML, Goblinson Crusoe

Published December 23, 2012
Advertisement
I love Lua, that shouldn't be a surprise to anybody who reads this blog. I am possessed of a chaotic, ad-hoc personality, so the ad-hoc nature of Lua coding suits me just fine. I love the fluidity and flow of working with a dynamically typed language, and the ability that it gives me to rapidly prototype and iterate on various data structures, code flow schemes and game designs without having to constantly recompile. Lua is awesome for quickly prototyping a game to see how it is going to work.

However, even with LuaJIT, profiling Goblinson Crusoe shows a distressing amount of time being spent in the various book-keeping tasks of Lua: garbage collection, memory allocation, etc... It's not unmanageable at the moment, given the relatively low number of objects in play at any given time, but I can foresee a time when it might be a problem. A bigger game, perhaps, with more stuff going on.

The main areas I really benefit from Lua are message passing (the backbone of object-to-object communication in GC), data structuring (iterating on the structure of a component), and data description and streaming (save/load, entity construction, etc...).

1) Message Passing - The backbone of communication in GC. Messages are constructed as ad-hoc Lua tables populated with relevant data, then sent through the central object handler via a sendMessageTo(receiver_id, message) call. It's like handing a note to someone; the sender doesn't really need to know what the receiver does with the message, or even if the receiver handles it at all. However, the data of the message can't really be nailed down with any kind of concrete type. For example, a ApplyRawDamage message would contain the amount of damage to apply to a unit, as well as the type (fire, electricity, etc...). Whereas an UpdateLogic message doesn't need any data at all; just the fact of sending it to an object causes it to do a Think() cycle followed by an action cycle.

2) Data Structuring - Especially during development of the combat system of GC, I went through many different iterations of many of the components in GC. I continue to iterate on a lot of things, though most of the main structures are nailed down now. Being able to skip the compilation step was a great time saver.

3) Data Description - An object in GC can be loaded simply by loading a save Lua file that consists of a table of tables. The top-level table describes the overall structure of the object, or which components make it up. The sub-tables describe each component. So a tree might be loaded from a table similar to this:

return{ {component="StaticSprite", sprite="Tree15", scale=1}, {component="HarvestableResource", type="Aspen Planks", quantity=Random(2,4)}}

It is easy enough to just call dofile() with the data filename to obtain a parsed and compiled Lua table with the data, then create an empty object and iterate the component list, constructing components as specified in the file and adding them to the object.


Recently, I've been exploring ways of duplicating this system in C++, hedging my bets against the day when I might find Lua to be inadequate. (I'm still not sure that day will come, to be honest; Lua, especially with LuaJIT, is pretty awesome.)

One of the biggest hangups for me has been the message passing. In my earlier days, I would often implement this type of system using the dreaded void *. Which, of course, really isn't the best idea. void * is the Wild West of variant data types, a Wild West where striking gold or getting shot by an Indian are both of equal probability. My efforts along these lines as a novice were thankfully short-lived, once I realized the sheer potential for bugs this provided. In the ten or so years since, I have never used a void *. I've been cured of it, you could say.

I've implemented roll-your-own variant classes using templates, unions, and all sorts of other schemes. I've even used a string-encoding scheme, where all data was encoded into a stringstream, something I saw once here on the forums. Perhaps the safest and cleanest was using a map of boost::any keyed on a string.

Unfortunately, rolling your own variant/any class also requires rolling your own file parser and streaming code, something which I have little patience for. I went through a phase during my early development where I was interested in parsing and writing languages and all that, but it wore off within a matter of months. I just don't really have any enthusiasm for writing any more parsing code, especially when I'm doing it to replace Lua but am not convinced I really need to, and certainly can't do it as efficiently as Lua.

My latest experiment, however, has been with YAML. Specifically, the yaml-cpp library. The cool thing about using YAML (or even JSON, though I don't like JSON syntax; seeing a readable data file full of " " quoted strings just hurts my eyes) is that the file read/parse/write code is already written. The yaml-cpp Node class provides an easy interface for constructing ad-hoc tables as well, allowing me to directly set or read key/value pairs. In all my efforts, using yaml-cpp has come the closest to feeling like Lua, as far as the data description and message passing. yaml-cpp drops in and replaces all of my configuration, data loading and message passing with relative ease.

It has gone well enough, in fact, that I have considered making the leap back to pure C++ even though I haven't hit the theoretical object limit in GC-Lua yet. I do see that limit coming (maybe not in this game, but surely in the next). I could write a tool to convert the Lua data tables for objects over to a YAML format (for many of them, such a fix would be almost as simple as converting = to : and stripping the return keyword from the top of the file) easily enough. And since the prototypes for most of the key components are nailed down now, then I don't benefit quite so much from Lua's lack of re-compilation requirements. I could port most of my components straight across and be done.

Preliminary profiling indicates a noticeable improvement over GC-Lua, but it's still a bit too early to tell. But just skipping the garbage collection cycle has made quite a bit of difference. To be fair, though, I could probably do a large pass of minor refactoring to reduce or eliminate a lot of waste in GC-Lua, where temporaries are created and collected unnecessarily, especially in inner loops and whatnot. Still, as I ease back into GC after one of my many hiatuses (hiatii?) it is something for me to think about.
0 likes 1 comments

Comments

MARS_999

I kind of been doing this with C++ and XML files lately, putting a bunch of stuff to control objects in a xml file and loading them after the interface is done in C++, neat isn't it.... Think that is what you are kind of doing....

December 24, 2012 01:06 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement