A couple of the very first blogs on this site were dedicated to source control management for this project, so I guess a nice way to bookend development on Gunmetal Arcadia Zero would be to return to that topic.
My general-purpose game engine/framework exists in a Subversion repository of its own, which is included as an external of the derived game’s repository. Starting with Super Win the Game, I’ve been making branches of the engine for shipping. This serves two purposes: it preserves the state of the engine as it was when I launched a particular game such that future engine work won’t unexpectedly break that game, and it allows me to make one-off changes to the engine for that game specifically when needed.
Last week, I branched my engine for shipping Gunmetal Arcadia Zero. I’ve already made a handful of checkins to that branch to address issues like the exploit mentioned in Episode 34. Some of these changes may get merged back into the main branch (or “trunk” in the SVN vernacular), but I can feel safe in the knowledge that, should I need to make awful hardcoded hacks to the engine to fix Zero bugs after launch, I can do so without upsetting other projects.
I don’t recall whether I mentioned it at the time, but last August, when I spun Zero off into its own separate game, I branched the Gunmetal Arcadia Subversion repository for that game. Since then, all Zero development has occurred in its own game branch, while also touching main engine source.
Now that Zero is effectively complete and I’m returning to the roguelike Gunmetal Arcadia, I’m faced with how to handle merging these changes back in. Truth be told, there’s very little work that’s gone into Zero that I think I absolutely can’t reuse for the roguelike, so for starters, I’ve taken everything. Where conflicts arose (mostly in binary content of test maps), I’ve taken changes from the Zero branch. In essence, I’m picking up development on the roguelike Gunmetal Arcadia not where I left it off last August, but at the very end of Zero development today.
I’ve been focused on development the last few months, so I haven’t had a whole lot of time to think about event presence this year, but as of yesterday, I’ve signed up for a booth at the local Let’s Play Gaming Expo June 18-19. I missed this one last year, as I already had obligations around the same time, but it’s now in its second year, and it sounds like it’s gonna be a lot of fun! I’ll be showing Gunmetal Arcadia Zero, of course, and I may try to expand the merch selection this year with t-shirts or soundtrack CDs. We’ll see how it goes!
After Wednesday’s video, I’m taking the next week off from development and blogging. I’ll return the week of May 9.
I made a build of Gunmetal Arcadia Zero on Friday that I’m calling “RC0.” (I’m not sure how widely used that term is, so for those unfamiliar, that’s “Release Candidate 0,” or the first believable shipping version of a product.) In truth, that build had a number of must-fix issues that I’ve addressed over the weekend, but it’s indicative of the state the game is in nevertheless. This thing is almost done. I’ll be spending the next couple of weeks tuning and polishing, fixing bugs, and preparing promotional media, and then I’m kicking this game out the door.
I’m long past the point of developing exciting new features, and I already talked a little bit about my concerns in continuing to document my development as I transitioned to a world of content production. Now I’ve reached the end of content production, and I find myself wondering how best to write about shipping a game.
As I’m not targeting consoles, my shipping process for Gunmetal Arcadia Zero is somewhat different than games I’ve worked on in the past. There’s no certification criteria to meet, and that’s nice, but on the other hand, I’m targeting three separate operating systems and an unknowable number of hardware configurations, and without the means for wider compatibility testing, the best I can do is make sure the game runs on my personal devices and try to fix bugs reported in early builds. On the bright side, I’ve refined my engine with each release, fixing what bugs I can reproduce, and though I’ll probably never have complete peace of mind with regard to the Mac and Linux versions, I feel like I’ve minimized the likelihood of complete and total incompatibility.
So Gunmetal Arcadia Zero will ship soon, and then development on Gunmetal Arcadia proper can resume. Remember that game? The eponymous Gunmetal Arcadia, the one I started blogging about way back in October 2014? The vision I had of that game prior to the introduction of Zero had a number of unsolved problems, and some of those have been occuping my mental bandwidth recently. (Importantly, I still don’t have a concrete plan for procedural level generation.) But in returning to that game in the context of a completed Zero, I’ll also have to ask questions of that existing vision. Does it make sense to keep plugging away at those same goals? What can I take away from the experience of shipping Zero that might affect the roguelike game’s design? Should I alter its design to further differentiate it from Zero? To accentuate the things that work about Zero and mitigate those that don’t? Do I even know which is which yet? Zero‘s not even out and I’m already starting to think of it in postmortem terms.
In On Writing, Stephen King advises setting finished manuscripts to the side for weeks or months after their completion, returning to make edits only after that time has elapsed. I’m not sure that’s entirely applicable to game development, but I do think there is something to be said for stepping away from a finished work for some time before making final alterations and sending it out into the wild. To that end, I’m thinking about taking a short break from this game after this week to work on something silly and fun. Maybe I’ll ever revisit that standalone CRT sim I was toying with last year. I just missed Ludum Dare, but maybe I’ll do something jam-scale on my own. As I was nearing the end of development on Super Win the Game, I participated in Ludum Dare to blow off some steam, which turned out to be an unexpected win when I was later able to reuse some of that work for a prototype of another game.
Upcoming tasks for the week of April 18, 2016:
- Monday: Promotional materials, day 1
- Tuesday: Promotional materials, day 2
- Wednesday: Playtesting and tuning, day 1
- Thursday: Playtesting and tuning, day 2
- Friday: Record Ep. 35, write blog, addl. work as time allows
I don’t have any tasks scheduled for this weekend. I guess I’m done with crunch? Maybe I’ll read a book or something.
Welp, it’s been fun, but Dark Souls III comes out this week, which means Gunmetal Arcadia Zero is paused indefinitely. (Not really but yeah but not really but yeah.)
The last few weeks’ or months’ worth of blogs have been iterative progress updates, and I’ve been itching to do an in-depth, technical, tutorial-style blog for a little while now. So today I’ll be talking about random number generation in Gunmetal Arcadia Zero. I’ve written about some of these concepts in the past, but I don’t think I’ve ever done a complete top-to-bottom review of the entire scope of this facet of the game, and certainly not since making some important changes for the vertical slice build.
Let’s start with the obvious. Some things in this game should be random, enemy behavior and loot drops being the prime examples. But how should they be random, and how random should they be?
(As an aside, I should note that I’ll be using “random” to mean “pseudorandom” throughout this blog. I’ll leave the substitution to the reader’s mind.)
I made the decision at some point that enemy behaviors, while “random” in the sense that two instances of the same enemy may choose different actions than each other, should be consistent across each play session. That is to say, when you see a particular slime blob, it will behave exactly as it has in previous sessions, all else being equal. Enemies who react to cues from the player or their environment may diverge from normal patterns based on this input, but they will still behave consistently and reproducibly. This decision was based partly on how enemies tended to behave in classic games and partly on the assumption that it would make the game more palatable for speedrunning.
Loot drops, on the other hand, wouldn’t be very interesting if they exhibited this same consistent, reproducible behavior. These need to be more random, such that the player can’t predict what they will receive for killing an enemy or destroying an environmental object.
In both cases, however, the ability to completely save and restore the state of random number generators is important. Consider the case of an attract loop recorded from a previous game session. If loot drops changed each time the attract loop were played, the results could be disastrous. Imagine if a torch that had originally dropped a new type of subweapon now dropped a coin, throwing the rest of the session out of whack when the player attempted to use that subweapon and failed.
So, with these use cases in mind, we want to be able to generate numbers in two ways: deterministically (consistently and reproducibly) and non-deterministically (unpredictably), and we also need to be able to serialize RNG state in both cases.
I chose to use Mersenne twisters for random number generation in Gunmetal Arcadia Zero. I won’t go too much into the basics of how Mersenne twisters work, as that topic has been covered frequently and in greater depth than I would be capable here, but the important things to understand are:
- A Mersenne twister must be seeded with an input number in order to produce valid results.
- Seeding the twister with the same value will always guarantee the same results.
- The internal state of a Mersenne twister can be saved and loaded such that a position in a “stream” of random numbers may be maintained.
- A typical implementation of a Mersenne twister regenerates its internal representation every 624 times a random number is extracted.
The first three points are true of most random number generators; the last has some ramifications on serialization of RNG state in Gunmetal Arcadia Zero specifically. The simplest way to save and load a Mersenne twister is to copy its entire internal state to and from a file on disk. In fact, this is how my implementation worked for a while, and for an application with a single RNG (or only a few), this would be acceptable. The problem I ran into is that I create one or two Mersenne twisters for each entity in the game that deals in random numbers, including dynamically generated things like slimes that spawn from eggs. When each of these wrote its own state to file, I wound up with an unmanageable amount of file bloat on disk which would have made it difficult to port saved games from machine to machine, whether manually or via a cloud save system.
In the future, a system that aggregates all active RNGs and publishes all their internal states to a single file might be a good solution, but what I’ve chosen to do in the meantime is to serialize a minimal amount of data to the normal saved game file which can be used to restore the Mersenne twister to its previous state at the expense of some extra cycles.
As noted previously, a Mersenne twister must regenerate its internal state every 624 extractions. So if we keep track of (1) its seed, (2) the number of times we’ve regenerated the set, and (3) the current index (0-623), we can restore its previous state from scratch. In theory, doing this regeneration step this is more expensive than saving and loading the entire set of 624 values, but in practice, I rarely extract more than a handful of values from any given twister, meaning we only have to redo the initial seeding in most cases.
Now that we’ve covered the technical side of things, let’s look at the really interesting stuff, which is how we produce the seeds that are going to ultimately be responsible for the uniqueness of the random number streams.
As I’ve said, one of my goals is to allow multiple instances of the same enemy type to behave differently. Each enemy gets its own random number generator, so in order to get different results out of each, we need to seed them differently.
My solution was to lean on an existing piece of data: entity GUIDs. My editor automatically assigns a 31-bit GUID to any entity placed in the map. (The reason this is 31-bit and not 32-bit is that the high bit is reserved for GUIDs created by the game itself, with half of that space being further reserved specifically for dynamically-generated entities.)
Beyond these reserved values, my GUIDs don’t currently have any constraints, considerations, or intrinsic meaning. But they must be unique, of course, and there are any number of methods I could use to generate unique values. At the moment, I’m using the millisecond value of the system timer truncated to 31 bits, which gives me a period of twenty-four days before values start reappearing. The likelihood of a collision is therefore trivially larger than zero.
So if every entity has a unique GUID (yes, I realize that’s redundant), then I can use that GUID to seed that entity’s Mersenne twister, and each one will produce a unique random number stream. So that’s one problem solved. But now there’s the problem of things like loot drops that don’t want a completely deterministic random number stream.
Before I get into my solution for that problem — it’s going to be a long one — let’s start by observing that the system clock has historically been considered a satisfactory way to seed a random number generator to produce unpredictable results, as exemplified by the prevalence of “srand(time(NULL))” in C/C++. This operates on the assumption that the system’s clock will never be in the same state as it was on a previous run, so the results should be unlike any seen before.
Time-based random seeding
When I first encountered the need for unpredictably random loot drops, my first idea was to use the time() function to seed these entities’ random number generators. However, this quickly proved a flawed solution, as time() returns the system time in seconds, meaning that each and every entity spawned within the same second would get the same seed, and would in turn produce the same random number stream. This briefly created a bug where all the torches in a given room would drop the same item as each other.
My fix for this issue was to hash the result of time() with the entity’s GUID, producing a value that would be unique at any given time and unique per entity. (For the curious, my “hashing function” was to simply XOR these two numbers together. It seems good enough.)
That worked well enough for a while, but when I finally got around to implementing attract loops, it fell apart once again. Because the time-based seed would be produced at the moment an entity were first spawned, the results would vary depending on when the attract loop were played back. This necessitated moving away from time() to an alternative solution which would provide the same element of randomness but in a deterministic fashion.
My game session now keeps track of something I call a “fixed time seed.” This is a value that can be used in place of time() but which follows some additional rules to better serve my needs.
The fixed time seed is generated at the start of each session. At this point, it is actually just the result of time(), cached off once when starting a new game. In this manner, I can have the benefits of time-based randomness, but I can also save and restore this value in order to predictably generate the same values.
Where this gets a little tricky is in determining when — and how — to reroll this fixed time seed. Consider the case in which the player kills some enemies, breaks some torches, sees a few random loot drops, and then dies and replays the same section again. Given the same fixed time seed, these RNGs would produce the same values, and the same loot would drop on this life. That’s not what I want; loot drops should be re-randomized on each life, and the easiest way to do this is to update the fixed time seed once again to the value of time() at the start of a new life.
Attract loops foil this once again, however. I didn’t want to rule out the possibility of an attract loop that contained a death and respawn, and as before, requesting a new time() value during an attract loop would lead to bad results, so in this case, I needed a way to deterministically update the fixed time seed such that it would give me different values on the next life, but so that they would also be the same different values every time.
My solution to this was to simply increment the fixed time seed by one at the start of a new life.
That was the extent of my RNG solution for a few months, but just recently, I began to notice how strange it felt to load a saved game repeatedly and see the same loot drops every time. This was consistent with the rules I had previously established and with my initial goals for wanting saved games to be a completely accurate snapshot of the game in progress, but it didn’t feel right as a player. So as of last week, the fixed time seed now gets reset to the value of time() whenever a saved game is loaded.
Numbers are hard, yo.
Upcoming tasks for the week of April 11, 2016:
- Monday: Dialog and vendor inventory pass on the entire game
- Tuesday: Cutscene work, intro and ending
- Wednesday: Cutscene work, intro and ending
- Thursday: Finalize all menus and options
- Friday: Record Ep. 34, write blog, addl. work as time allows
- Saturday: Audio pass on the entire game
- Sunday: Play and take notes
I had had cutscene work scheduled for last weekend, but after revisiting my schedule in light of wrapping the soundtrack and key art earlier than expected, I realized I could space my remaining tasks out a bit so I don’t have to crunch so much. So, that.
In case you missed it, I wrote up an impromptu Sunday blog with comments enabled to gather feedback on checkpoints and how to handle player state across death and game over. If you have any thoughts on those issues (especially if you’ve played the vertical slice build), please leave a comment!
Last week saw me move from creating tilesets to decorating and populating levels with environment art, enemies, and NPCs. I finished the Basil, Cilantro, and Tarragon levels (levels 3, 6, and 5, respectively), and as Cardamom (level 2) was already completed back in January for the vertical slice build, that leaves only Fennel (1) and Sage (4).
It will still be another week or two before I can really get a sense of the scope of the finished game (just in time to ship it, ha!), but I’m at least getting a better sense of the variety of levels than I could when everything was grayboxed and I just had to imagine how each environment would look.
I don’t usually blog about bugs because they’re not fun to write about and I suspect they aren’t fun to read about, but I ran into one this week that was worth mentioning. The bug manifested as “an NPC walks off screen but a second copy remains and can be interacted with as usual.” The catch was, this only happened when progressing through the game normally; launching directly into the map through the editor worked correctly.
That immediately raised some suspicions of what the level transition code was doing, and, skipping over some boring technical stuff, in short, when reaching the end of a level and transitioning to the next one, the new level would be loaded once, immediately discarded, and then loaded again. And if it weren’t for this odd edge case, I’d have never noticed! The only reason the NPC was appearing twice was because of a side effect related to suppressing entity spawning until the room is fully visible in order to appear more NES-like.
When entering a room, I queue up all the entities in the room for spawning, but I wait to actually spawn them until we’ve finished scrolling or fading in (with the exception of things like teleport exit markers that necessairly must exist earlier). Once the scroll or fade has finished, I spin through the list and spawn the queued entities.
So what was happening here was, I would load the level once, queue up all the things that needed to spawn, including this NPC, then immediately discard the level but not the queued entities. Then I’d load the same level again, queue up the same entities again, and spawn both queued copies once the fade-in had finished.
My first thought once I realized this was, “I guess I should throw out any entities queued for spawning when the level is destroyed.” And in all likelihood, that would’ve fixed the bug, but of course, it overlooked the larger issue of why the level was being loaded twice in the first place. That turned out to be more boring technical stuff. The game has an opportunity to load the incoming level in response to displaying the title card for that level, and in many cases, that is the correct behavior (as when loading a saved game or dying and restarting). However, in some cases, such as starting a new game or transitioning to the next level, other code is better equipped to handle the level load. In this case, I was already loading the next level as soon as possible in response to the previous level fading out, but I had forgotten to opt out of letting the title card also attempt to load it. So, both were happening.
A few weeks back, I showed some work I had done on the key art or cover art for Gunmetal Arcadia Zero. I’ve made a little more progress on that this week.
I hadn’t been happy with attempts to color the image digitally, but it wasn’t feeling good enough as a black and white outline, so I decided to give it a try with paint. In the worst case scenario, I figured, I could digitally remove the color and be left with a more interesting grayscale texture.
I printed out an image of the inked art I had done previously and, lacking actual watercolors, went over it with acrylics thinned with water. I used an Aquash brush which felt pretty good and matched the look and feel of the brush pen I had used previously.
Since I was using water on a sheet of plain printer paper, the sheet got a little warped and wavey, but after scanning it, I just cropped out everything outside the existing outline, desaturated the colors a bit, and overlaid it with the original art.
Next, I needed a background. I had the idea to do a sort of mottled wash of colors that would vaguely imply a landscape. I built up a few layers of faux watercolors, using greens and teals for the earth and purles and reds for the sky. I then worked outward from the corners with more opaque black paint to vignette the whole scene.
I got that scanned, but it wasn’t really clicking. On a whim, I inverted the image, and suddenly it made more sense. Dark teals for the sky appeared stormy and ominous. Dark reddish purples for the earth appeared blood-soaked and barren.
The colors in both the character and the background were a little too exaggerated, so after scanning and compositing, I brought everything closer together towards sepia tones and also added a digital vignette over everything, even the logo.
As before, I can’t say for sure whether this is the final shipping key art, but I’m pretty happy with it, and, you know, there’s not too much time left to make any more changes.
Upcoming tasks for the week of April 4, 2016:
- Monday: Decorate “Fennel” with environment art
- Tuesday: Populate “Fennel” with actors
- Wednesday: Decorate “Sage” with environment art
- Thursday: Populate “Sage” with actors
- Friday: Record Ep. 33, write blog, addl. work as time allows
- Saturday: Tweak intro cutscene and begin work on ending
- Sunday: Ending cutscene
This upcoming week’s schedule is noteworthy in that Thursday’s task concludes core development of Gunmetal Arcadia Zero. There’s still some ancillary work to be done beyond that, including tuning and balancing the entire experience and adding a few features like transitioning from the final level to the ending (and figuring out what to do with the saved game after the game is finished), but there shouldn’t be any new core gameplay content coming online after that point.
What are we down to now? Three weeks? That sounds about right.
I have another regularly scheduled blog for tomorrow, but in light of some work I’ve been doing today related to saved games, I wanted to bring up an issue that’s been at the back of my mind for the last couple of months.
I’ve enabled comments for this post, and anyone is welcome to contribute thoughts, but I’m especially interested to hear from those who have played the vertical slice build. (And if you haven’t played the vertical slice build yet, you can find that here.)
To establish some context, here’s a series of tweets I made earlier today:
There's not going to be a *real* New Game+ with rebalanced content, but it'll drop you back to the first level with all your gear intact.
— J. Kyle Pittman (@PirateHearts) April 3, 2016
Thinking about doing an extra blog post with comments enabled to discuss it.
— J. Kyle Pittman (@PirateHearts) April 3, 2016
The issue is this: when you die and continue from the last checkpoint, what state should your character be in? I’ll explain first how this works in the current build and why it works that way, then I’ll present a few alternatives I’ve considered along with pros and cons for each. None of these is objectively the correct decision, but I’m curious to hear opinions on these or any other options.
The way things work today is, when you first enter a level, a checkpoint is activated, and a snapshot is made of the current game state. If you die, you return to this exact state minus one life. If you run out of lives, you return to this same state with the default number of lives.
After killing a boss or a miniboss, a midlevel checkpoint is activated. Another snapshot of the game is recorded separate from the one at the start of the level. If you die after hitting this checkpoint, you return to that state, but when your lives are depleted, you go back to the start of the level.
The pros of this approach are:
- This is how things already work. This implementation is believed to be stable and bug-free. (And yes, with only a few weeks of development left, every alternative will necessarily have the inverse as a con.)
- This is roughly analogous to the way many classic games have worked, including Castlevania and Mega Man.
- There’s less risk of winding up in a state in which the game is saved in a very difficult scenario with little or no resources.
And here are the cons:
- Actions taken during the course of the previous life are lost when starting the next life.
- Checkpoints are infrequent, possibly requiring playing through the same segments repeatedly.
That first one is the big problem here. It’s really the only reason I’m considering making any changes to this system so late in development. It’s frustrating to play through a leg of the game, maybe buy some upgrades and find a subweapon you really like, only to die and have to do it all over again. It’s doubly frustrating when this happens in the first level and you have to replay introductory narrative/tutorial stuff, even as minimal as those elements are.
There are two options I’m considering to improve this experience. They are not mutually exclusive; I could do both, or either, or neither.
The first option is to increase the frequency of checkpoints. Rather than checkpointing only at the start of a level and after any boss fight, I could also checkpoint before boss fights, or on any room transition, or any time equipment is changed, or any number of other criteria.
The second option is to carry over changes to the player character’s equipment on death (and possibly on game over, although I haven’t yet thought through the ramifications of that one). In this way, any new gear you’ve found, any money you’ve collected or spent, any upgrades you’ve equipped would carry over to the next life. You’d still start from an earlier location, but you could forgo the frustration of having to retread all the same ground in pursuit of the same gear you had found previously.
In combination, these options provide us with four scenarios:
- Infrequent checkpoints, no carry-over. (This is how things currently work.)
- Frequent checkpoints, no carry-over.
- Infrequent checkpoints, carry-over.
- Frequent checkpoints, carry-over.
Let’s look at the pros and cons of each of these.
Frequent checkpoints, pros:
- Less potentially frustrating retreading of the same ground.
Frequent checkpoints, cons:
- Risks making the game too easy by virtue of being able to exploit checkpoints by ignoring damage from enemies and getting a cheap health refill by dying immediately after reaching the next checkpoint.
- Less frustrating loss of previous acquisitions.
- Feels more congruous with what other games do.
- Risks making the game too hard by virtue of saving in a state with no resources (e.g., after expending all ammo and bombs fighting a boss and then dying to it).
So, that’s what I’ve been thinking about. My default behavior is to be super resistant to any change that could destabilize the build this late in development, so left to my own devices, I’m most likely to leave things the way they are. But this also feels like it has the potential to be a big source of frustration and shelf moments in this game, which is why I’m opening it up to discussion.
Comments are now closed, but you can send me feedback on Twitter (@PirateHearts) or by email (jpittman at gmail dot com).