recently as a way to take breaks from perpetually impending and terrifying deadlines i’ve been doing research (reading white papers, summaries of other games’ source code, etc) to understand procedural world generation in a 2d roguelike-ish context in anticipation of a possible future project (but mostly as a learning exercise). i don’t have a blog or anything but i want a place to organize my thoughts outside of my design journals where i can easily drop links to stuff and also get feedback from other game dev ppl if anyone finds this interesting.
i want to see to what extent i can take a (very impure) functional rather than imperative approach to generating worlds. to understand how feasible this is i’m trying to break down processes that show up across different games into rough categories and work towards generic, atomic functions. here is what i have as my schema right now:
World Map: The 2D matrix of cells at the highest level. Each cell is a World Tile. (A useful analogy for World Tiles is typical tiles in old rpg world maps, which could be described as “forest tile” or “mountain tile” or “sand tile” etc)
The World Map is generated from Landmasses, which are themselves made up of Biomes (which set the rules for the kinds of World Tiles that are generated) and Features like dungeons and towns (more on this later).
For our purposes, we will say that each World Tile that does not contain a Feature contains a Field. These Fields are 2D matrices of their own, all of the same size, and the parameters for their creation are set primarily (possibly only) by which World Tile they come from. (Each cell in a Field could be thought of as a “pixel” on the World Map; in a sense, a World Tile IS a Field.)
At a high level, most of the processes for generating the World and generating Fields are the same. Because we want to design around higher-order functions, the line between things and processes begins to blur. My current methodology is loosely inspired by this roguebasin article. I’m not completely satisfied with the terminology here and would like a more cohesive set of analogies to work with but I haven’t found anything that fits quite right.
- A Creator is a function that iterates through and modifies cells in a given Canvas (a collection of cells) based on Rules.
- A Rule takes a cell (origin) and optionally a type of Neighborhood (a set of neighboring cells) as input and returns a boolean value.
- A Painter is a Creator that iterates through space from some origin on the Canvas, “painting” cells (think random walk, flood fill). I think all their behavior can be elegantly captured with recursion but I need to research this further.
- An Organism is a Creator that iterates through space and time according to its Species (a set of cellular automota rules built up from Predicates). (this is useful for some “natural” features like Landmasses and cave systems but also in-game for stuff like fire spreading)
- A Transformer is a function that manipulates a Canvas in a non-iterative manner, for stuff like duplicating some cells somewhere else or applying geometric transformations (useful for symmetry)
Top-level imperative Designer functions that call on the Creators are probably necessary for program flow and incorporating pre-determined design elements but in a perfect world their program logic would amount to composing the I/O between Creators, basically turning them up and watching them go.
I’m not sure how flexible this design pattern actually is. It works well for geography, I think, but I’m not sure how useful it is for, say, room-and-corridor dungeons, or placing features after the world is built (all the roguelikes I’ve looked at so far brute-force their dungeon feature placement and evaluate the predicate for a given dungeon feature at random locations X times before giving up; I know stuff like this is “simple” but it feels like a hack and kind of defeats the purpose of this learning exercise). I’m still thinking about a way I can restructure this so that I can drop down some predetermined content and basically use that as the “input” for generating everything else. I have more to say about this but I’m going to save it for another post.