09 January 2020

RNGesus is an unruly troublemaker who'll cause you all kinds of grief if you let him off his leash.

"Find Your Car" includes, unsurprisingly, a lot of procedural generation and in turn a lot of pseudorandom number generation. As with my prior article about pseudorandom numbers, I felt like taking a few minutes here to share my experiences wrangling the laws of mathematics to achieve my goals.
A major purpose of this game is acting as a demo for my procedural universe chunk system, particularly its versatility "out of the box." I accordingly designed it with as much modularity as possible and used preexisting utility scripts I'd already written wherever I could. I ended up needing to upgrade a few of these, but in so doing I was careful to keep them generic and thus reusable.
A notable example is my "Swap With Random Prefab" script, which when attached to an instance of a prefab will, once it is spawned, choose from a user-specified list of prefabs, instantiate it in place of itself, and then despawn itself (or, in the upgraded version, an optional "target" object). This has been useful to me in the past for, for example, spawning a placeholder tree and then swapping it with a random other tree to add variety, or having a zombie drop a random item when it is killed. In "Find Your Car," it is used for walls and floors to randomly replace them with a variety of walls or floors, e.g. a floor with a ramp or a wall with a door, when a chunk is refreshed. This way I could construct one template parking garage chunk and then have it adopt a large variety of configurations as it was instantiated throughout the garage.
Some readers may, assuming I'm explaining this well, see the problem already. With this level of encapsulation, the wall and floor instances have no idea what chunk they occupy and thus no connection with its random seed, but they do call upon the built-in random number generator - meaning that the random swaps they perform will be truly unpredictable and not deterministic. The user-facing side of this problem is that a player could leave an area, walk a decent distance away so that the corresponding chunks are unloaded, head back so that they get reloaded, and find that they have regenerated completely differently! There might be walls where there weren't any before, cavernous voids where there had been floor, and the car that had been left comfortably parked in an open space now lodged halfway inside a wall!
Ways to fix this didn't come easily. The most obvious solutions were to refactor all of my utility scripts to use interfaces such as "seedable random" or to move all of the random generation that needed to be deterministic into the chunk refresh function, sacrificing modularity in favor of a complex monolithic algorithm. As may be inferrable from my tone I wasn't excited about either of these. I did find a solution, but it involved sacrificing performance instead (and what I imagine is less than professional-grade code) by creating a helper script that would scan chunks for specific scripts and call non-randomized versions of their swap methods, which it would randomize itself based on the chunks' random seeds and positions - a bit of a midway point between the other two options that I considered a workable compromise.
The reason I bring this up isn't to brag about my hacky workarounds to my own buggy code though. The reason I bring this up is because I learned a valuable lesson and I want to pass it on to everyone else who tries to do something like this: be very careful when dealing with random number generators if you want deterministic results. If you don't make sure that any and all objects using them are strictly controlled so that your random seeds (or states, etc.) are properly enforced, the generators will bite you in the butt with no hesitation and happily run off generating all manner of decidedly non-deterministic, unpredictable values, the end result of which is an unstable game world. Unless that's what you want, keep them under control!

No comments:

Post a Comment

Sorry this isn't a real post :C

I've entered graduate school and, as I expected, suddenly become far busier than I had been before, leaving no time to maintain my blog,...