Featured Blog | This community-written post highlights the best of what the game industry has to offer. Read more like it on the Game Developer Blogs or learn how to Submit Your Own Blog Post
2D procedurally generated world building in Unity
Description and post-mortem of a 2d procedural world generator.
I'm fascinated by the idea that it's possible to create an environment with interesting meaningful stories and interactions entirely procedurally.
During my time as a student at university I spent countless hours fiddling about trying to create structure from randomness. Most of my initial experiments would generate a small environment like a cave or a selection of interconnected rooms, eventually I figured time to go bigger.
This is the result:
Some worlds generated by the algorithm.
You can find a WebGL version of the game on my itchIo page.
Biome & flora generation
The first thing the algorithm does is generate the biomes and general geology of the world. To do so the algorithm generates multiple Perlin noise maps for rainfall, height and temperature by looping through arrays (float[,]) and using Unitys Mathf.PerlinNoise.
The generated noise maps are then modified a little, height is decreased along y and rainfall and temperature are increased along x, this results in maps that are usually relatively similar: the sea is to the north, mountains in the south, deserts in the east, etc.
Biomes are then generated as a combination of rainfall, height and temperature, to do so a method was created, this method has input for the biome index and biome parameters, it works by looping through the BiomeMap (int[,,]) and, if the biome parameters at BiomeMap[pos] are above the desired parameters, replaces whatever is in BiomeMap[pos] with the biome index.
The biome generation method is called multiple times, each time it will overlay the previous layer in areas that fit the right parameters, once for each biome type and in the order from left to right in the following table:
My biome generation parameters and Whittaker’s parameters.
The different biome types are stored in an array of GroundTiles[], which defines information such as what plants can exist in this biome and if it can exist underground.
GroundTiles class diagram & inspector view.
If you are wondering how the class GroundTiles is visible in the inspector, the class is seralized by placing the tag [System.Serializable] above the GroundTiles class declaration. An array of GroundTiles is then created in the level generation class like so: public GroundTiles[] groundTiles;
Lakes are created in areas with high rainfall and rivers are created starting at a random rainy point and with a preference to heading downhill and to rainier areas. These rivers are very simplistic in nature and do not perform any type of path-finding or have any knowledge of their surroundings apart from checking the tiles in their immediate vicinity.
Plants (GroundTiles.NonObstructivePlants) and trees (GroundTiles.NaturalBarriers) are then placed randomly, although slightly dependant on rainfall. These are then passed through a Cellular Automaton algorithm to cluster them up a little.
NPC's, buildings & cities
Now that the general world is generated, the algorithm moves on to populating it with NPC's and their creations(cities, buildings & roads) in the following manner:
1) A Perlin noise map for population density is created.
2) Loop through map testing every so often and place areas with a high enough population density into a list of possible city points, points too close to other city points don’t get added. This list is then cut down at random until it reaches the desired length.
3) Roads are generated between city centers, roads going through a lot of water get replaced by boats at either end that will transport the player. Roads are generated similarly to rivers, with no long term concern for obstacle avoidance.
4) Roads are generated randomly from other road points in areas with high population density these are our streets), these roads are generated within a distance of other roads generated in this step, to allow space for buildings to be placed in between.
5) Buildings are generated next to roads with a high enough population density
All buildings are all generated as rectangles, the building size is dependant on the space available and the buildingType, picked at random but dependant on population density and the biome index at the building start position, as certain buildings can’t be built in certain biomes.
Indoor walls are generated via recursive division.
Class diagram for BuildingType & Inspector view.
Wealthier and more cultural buildings are built in areas with a higher population density as per the following parameters:
Building type parameters.
6) NPC’s are randomly placed around the map depending on population density, many of these are part of an array of npcGroupType which is assigned in the inspector (same as the array of GroundTiles) and stores information as to what type of ground the group can spawn on.
Buildings and NPC’s are then grouped up into cities via a pseudo clustering algorithm, which sets certain buildings near city centers to different, randomly generated, cities, these cities then spread to nearby buildings, if there are too many cities, the cities nearest to eachother will join together.
For the moment cities don’t really do much apart from acting as higher level factions when it comes to NPC’s deciding who to attack. Ideally they would have their own motivations, goals and power hierarchies
Class diagram for cities.
These cities then hold/store information for interesting events that happen in the world, these are caused by NPC’s interacting with each other and the environment (once every 24 moves). For the moment the interactions are simple attacks on NPC's & buildings belonging to non allied cities.
Hopefully the interactions will eventually be more diverse with actions such as trading, finding loot, etc.
Class diagram for NPC's.
Instantiation
I quickly noticed that I could instantiate only the tiles in the vicinity of the player, meaning that the size of the world was only dependent on how long I wanted to wait for it to generate and how much memory I wanted the save data to occupy, seeing as I wanted to be able to show it off on an iPhone I decided to stick with a world size of 750x2x750 tiles.
To decide what to instantiate where I saved the data into multiple int[,,] arrays and various lists of classes, as follows:
-list pickupables: For keys, potions and other items the player can pickup.
-list treasureChests: For treasure chests that the player can interact with.
-list humansData: List of NPCInfo for the human NPC’s.
[…]
-int[,,] BiomeMap: stores what biome to spawn, ground is spawned from this map when TileTypeMap is not something that would replace the natural biome such as roads or walls, and is instanciated like so:
GameObject ground = Instantiate(groundTiles[BiomeMap[x,y, (int)Player.transform.position.z]].TileVariations[Random.Range(0, groundTiles[BiomeMap[x,y, (int)Player.transform.position.z]].TileVariations.Length)], new Vector3(x, y, 0), Quaternion.identity) as GameObject;
-int[,,] TileTypeMap: The information as to what type of tile is in which location, be it floor, walls, ground, stairs, doors, roads, plants, NPC's, etc.
-int[,,] VariantMap: The variation of the TileTypeMap at this point, for example which plant, NPC or wall tile to spawn.
This could be simplified to only two arrays(VariantMap & TileTypeMap), but I felt the need to know what biome is in what area, so that I could have biome specific enemies, roads, plants, walls, etc.
The array from which to choose the object to instantiate depends on the value of TileTypeMap, and is controlled by simple if statements like so (pseudocode):
for (int x = pos.x -spawnDist-10; x
for(int y = pos.y-spawnDist-10; y
if(xpos.x+spawnDist || y>pos.y+spawnDist) { Destroy(FloorTiles(x,y));}
if (x>=pos.x-spawnDist && y>=pos.y-spawnDist && x<=pos.x+spawnDist && y<=pos.y+spawnDist) {
if (TileTypeMap [pos] == 14 || 20 || 26) {
Instantiate(roomTiles[BiomeMap[pos]].RoomTiles[Random.Range(0,RoomTiles.Length)];
if (VariantMap[pos] < 200) { Instantiate(roadProps[VariantMap[pos]]); }
if ( VariantMap[pos] == 200 || 201) { Instantiate(Keys || Potions); }
}
if (TileTypeMap [pos] == […]
for (int i = 0; i < HumansData.Count; i++) {
if (HumansData[i].x == x && HumansData[i].y == y && HumansData[i].z == z) {
Instantiate(Humans[HumansData[i].spawnIndex); }
}
[…]
}
}
Read more about:
Featured BlogsAbout the Author
You May Also Like