Twisty Little Passages

Hey, wow, it’s been a whole week already since that thing I did! Following last week’s introduction to my new development documentation plan, I put up another video on Wednesday and another in-dev build on Saturday. I feel like this is a schedule I should be able to maintain. It’ll be crunchy, and I can’t promise I won’t miss a week here or there, but I feel like I’ve streamlined the process well enough that I won’t be spending all my time preparing this content and leaving no room for development.

Still pretty much the state of the game.

This week was the first time since early April that I was really able to dig into some Gunmetal Arcadia development tasks. Whatever momentum I had had on content production has been cut short, and in the interim, I’ve decided to focus on some core systems that don’t exist yet. I’m not talking about moment-to-moment gameplay systems; by and large, those have been finished since late March, barring whatever one-off work is required for weapons, items, abilities, enemies, bosses, NPCs, or environments that I haven’t designed or built yet. No, I’m talking about systems that define how a single session of Gunmetal Arcadia is structured, how persistent data is carried over across multiple sessions, how daily challenges will be implemented, and so on.

I’ve started by working towards making random number generation higher quality and easier to wrangle. I’ve usually just assumed that the built-in C++ rand() function will be good enough for my purposes, but in light of my efforts to serialize the complete game state, I need a better solution, and that means rolling my own RNGs. Well, not my own own; I’m using a bog standard Mersenne Twister implementation (MT19937). This guarantees consistent behavior on all platforms, facilitates the use of multiple RNG streams, and allows me to save and restore the state of RNGs across multiple runs. That last one is what I’ve been pursuing this week. The state of a Mersenne Twister can be stored as 625 integers (624 for the internal state and one index into this state array), which is a relatively small amount of data but too heavy to fit nicely into my existing serialization system. My saved game data consists of nested key-value pairs, all represented as strings. This is convenient for a number of reasons (easy conversion to and from XML, etc.), but for anything more than a small handful of variables, it’s too unwieldy. I ran into this limitation previously when I was implementing speedrun ghosts for Super Win the Game. For these, I ended up doing a one-off implementation in which a speedrun component managed its own file I/O. That was sufficient for a quick post-launch feature, but for Gunmetal Arcadia, I need to build with the future in mind, and that means spinning this into a real system.

I’ve already written extensively about my entity serialization in past entries, but the high-level overview is:

  • When entities’ components are destroyed, they write out any data necessary to restore them to their current state. This data is saved in the serializer, keyed by the entity’s GUID. When the entity is rebuilt, this data is retrieved and applied to the newly constructed components.
  • Upon request, the serializer may write out all its stored data to disk. This data represents the entirety of the player’s saved game. Once loaded back into memory, this data can be retrieved and used to rebuild entities as normal.

In general, I think of this pattern as going: [live data] ↔ [serial data in memory] ↔ [serial data on disk]. In extending this system to account for arbitrary binary data, I had to decide whether or not to follow this same pattern exactly. What I’ve done for now is, rather than storing potentially large amounts of binary data for inactive entities in memory all the time, I’m writing the binary data to disk immediately but in a temporary location. Only once the serializer is prompted to save the player’s game is this file copied to the normal saved game folder. In this way, the behavior mirrors that of the ordinary nested key-value data path, but without the need to keep this data in memory. This is still a very young system, and it feels like it’ll take a few more revisions before I’m totally happy with it, but at least in this first test case of saving and restoring the states of random number generators, it’s proved successful.