2D Procedural Generation In Unity With ScriptableObjects
The thoughts and opinions expressed are those of the writer and not Gamasutra or its parent company.
In this post, I am going to explain the approach that I've taken in creating my procedural generation tool for Unity called Strata. Strata allows for the generation of varied 2D levels composed of a mix of hand-authored and generated content in Unity using Tilemaps. Strata is available for sale via the Unity Asset Store and Itch.io.
First, a brief introduction to ScriptableObjects. In Unity, ScriptableObjects can be thought of as being like a MonoBehaviour which is never attached to a GameObject. They are frequently used to create custom assets which can either act as simple holders of data or that can execute code. In Strata, we are using ScriptableObjects in both ways, which I will explain. For those interested in digging deeper into the topic, I've written a blog post for the Unity blog which collects together some resources, including several talks and tutorials.
My goal in creating Strata was to create a 2D procedural generation tool which was usable by non-programmers and programmers alike. I wanted folks to be able to generate varied 2D levels without having to re-write the source code while also providing clean entry points for folks who wanted to add code to the system. This is achieved through the use of ScriptableObjects and the delegation pattern. The idea of the delegation pattern, for those who are unfamiliar, is that we use helper objects which we delegate functionality to. In this case, we use custom ScriptableObject assets containing executable code, called Generators in Strata. Generators can be added to a Board Generation Profile (another ScriptableObject) via drag and drop in the Unity inspector to add generation operations to the final level output.
In practice this means that a level designer who is interested in authoring procedural levels can choose from a variety of Generators, each containing a single algorithm or level generation operation and then add them to a Board Generation Profile via drag and drop. Generators included do things like generating cavelike shapes via cellular automata, generating sequences of connected hand-authored rooms, digging out random tunnels and rooms, scattering objects, and so on. The output of each Generator is written into (or over) the previous Generators and so by selecting and layering Generators, a designer can produce a variety of level outputs. For programmers, the goal here is to create a system which can be extended without rewriting existing code. All Generators are self-contained classes and adding new Generators to the system has almost zero impact on the rest of the code base.
Strata also uses ScriptableObjects as containers of data. One of the problems Strata seeks to solve is the 'Thousand Bowls Of Oatmeal' problem described by Kate Compton. In short: a thousand bowls of oatmeal may have a thousand unique rotations, shapes and color variations of individual oatmeal flakes but in the player's perception, it's a bowl of oatmeal like any other. Compton points out that one risk in procedural generation is that of generating things which are technically unique, but not perceptually or meaningfully unique to the player. The approach Strata takes to this problem is allowing creators to mix in delicious (crunchy, flavorful) bits of hand-authored content. These hand-authored pieces provide a kind of textural variation to the generated content allowing you to surprise and delight your players. This is achieved by allowing creators to draw chunks of level content using Unity's Tilemap features and to then to save those as data into ScriptableObjects, in Strata called RoomTemplates. RoomTemplates can contain data about the placement of tiles and also data about the potential connectivity of that room to other rooms (if applicable). The connectivity data is used when generating more constrained structures which must guarantee connectivity between level chunks, for example when generating a connected series of rooms between entrance and exit for a game. These hand authored chunks of content can then be integrated into more abstract procedural levels.
One of the risks when using hand-authored content is that each template is repeated too often and players readily recognize the pattern. To mitigate this, Strata uses a secondary layer of randomization, which I first saw in Derek Yu's (wonderful) Spelunky. In this case, the level is generated from hand-authored squares of tiles but certain tiles are specified as having the potential to change. In Strata I call these Chance Tiles. Each Chance Tile has a number of output tiles associated with it with an associated probability to spawn. So, for example, we could place a 'chance to spawn a monster' tile in a dungeon room. When the level is generated Strata effectively rolls a dice and based on the roll chooses a monster from a table in the BoardLibrary asset (another ScriptableObject). This allows us to add a layer of procedural fuzziness to our hand-authored content. We can make sure that the walls, doors and other key features remain the same, but apply variation to the contents of the room or area, thereby making it a bit less likely for players to recognize that chunk of content as being identical to their last playthrough.
One of the benefits of the ScriptableObject based architecture is that it means that it is easy to add additional Generators to Strata, and I have plans for several more algorithms to add in future updates. If you have questions, comments or feedback feel free to leave a note here or to reach out on Twitter, I'm @mattmirrorfish there. I've also made some YouTube videos which show how Strata works, which you can find in this YouTube playlist. Strata is available for sale via the Unity Asset Store and Itch.io.