This thread is an extension of my previous Map Generation threads(see signature). If I do not show something in this thread, then it may be explained in one of the other threads. The generator currently uses 200+ variables and 160+ functions so it would be impossible to cover in fine detail. This will be much more general in nature.
Overview:
The Map Generator procedurally generates hexagon maps using Voronoi diagrams(google it) for use in a Civilization style game, or really any game that can use a hexagonal map. The generator is fairly complex, taking into account tectonics, elevation, temperature bands(latitude), precipitation, river growth, etc, with more complicated climate systems to come(among many other things). There are currently 15 player settings that can change just about everything about the map from hellishly hot to snowball earth, Pangaea to global Oceania, dry bones desert world to endless jungle, flat earth to mountainous tectonic nightmare, etc, etc. Most settings have additional uniformity/diversity settings that let you determine how diverse the terrain is, such as having terrain features that stretch for vast distances, or more broken up by changes in precipitation, elevation, etc.
At 5000 tiles, this is a little bit bigger than your standard Civ Map. Currently, the largest map size is ~20,000 tiles which is similar to Gigantic.
For reference:
Arctic = Snow
Tundra = Snowy grass
Boreal Forest = Snowy Evergreens
Coniferous Rainforest = Evergreens
Deciduous Forest = The average Green trees
Jungle = Bluegreen Palm trees
Marsh = Light Teal plains
Grassland = Green plains
Steppe = Brownish plains
Desert = Sand colored plains
Arid Mountain = Reddish rock mountain
Mountain = Rocky with snowy peak
Arctic Mountain = Mostly snow covered with a little rock showing.
Lake = Deep Blue surrounded by land
Coast = Blue Green surrounding the land
Ocean = Dark Blue
Sea Ice = Light Blue
I will try to outline where the generator is and where it is going. Most systems are being upgraded, so that will be tracked here:
Continents/Oceans V3.0 - Tectonic Simulation using 3 layers of Voronoi diagrams.
Elevation/Topology V2.0 - Random Seed, single Voronoi layer. (V3.0 will involve plate collision and simulated tectonic activity driving mountain creation)
Temperature V1.0 - Based on latitude, each tile belongs to a temperature band. Any changes to this system will depend on how the Precipitation system evolves, but should generally be fairly static.
Precipitation V2.0 - Random Seed, single Voronoi layer. (V3.0 will involve a more complicated weather/climate system)
River/Lakes V1.0 - Rivers flow around the edges of the tiles, flow into and out of lakes, and eventually dump into an ocean. (V2.0 and beyond mostly involves tweaking the flow path and upgrading the start points)
Lowland/Marsh V2.0 - Random Seed, single Voronoi layer. Plains that have poor drainage characteristics and access to water turn into Marshes. (V3.0 depends on the Precipitation upgrades)
Resource Generator V1.0 - Random Seed. Resources currently spawn randomly after being filtered through several player settings. This is turned off while I work on other systems.
Here is the overall generator
I will explain more of each system in subsequent posts devoted to each system.
Right now, I will try to explain how the basics of how the the information is stored and accessed for vector field that drives everything.
Everything about the map is housed by arrays. Each tile in the map has 1 index in an array and every piece of information about that tile has the same index in every array that stores such information. The map arrays are Indexed as such:
As you can see, there are 2 examples of what a neighbor check looks like in both Even and Odd offsets, along with the math to to find their index. Since the map uses world wrap, the tiles on the other side of the map are shown for easy reference. Top array is a 10x20 map, and the bottom is 20x40. All map sizes scale to values of 10. Doubling the map size quadruples the number of tiles. A size 10 map has 1 tile per temperature band(per hemisphere), with a size 100 map having 10 tiles per temperature band.
One of the first things that I create is the Vector Field, which is indexed to the map size and is used in almost every other system in the generator. Depending on the size of the hexagon the math may turn out differently, but this creates all the vectors for the tiles in my map:
I looked at using Axial coordinates, and do have an Axial coordinate array implemented, but have yet to find a use for them that trumps my offset index methods. If I use axial, then at some point I have to convert back to index anyway. Since I am doing all the math with the offset indexes, then I don’t need to convert. The tricky part is ensuring you properly account for the offset, but once you have that down it works just fine. Both methods require checking for edges and world wrap, and that can be a serious pain in the butt, but once you create functions to handle those problems things become much easier.
The core of the generator uses Voronoi diagrams to plant and grow Seeds into landmasses, oceans, deserts, jungles, etc, etc. When a tile is checking to see whether it is land or water, wet or dry, etc, it is going to look at all the nearby Seeds of that type to see which one is the closest. Each tile in the map goes through this process one or more times, for each piece of terrain information that is being determined.
Distance checking without world wrap is pretty easy. Compare the length between the current tile vector, and each seed vector. The closest seed wins. Distance checking with World Wrap is a bit more complicated:
The world wrap is currently implemented by duplicating the main map into 2 shadow maps. The 2 shadow maps simple spawn with an offset on each tile. Each shadow map is also indexed the same way as the main map. If I cut down trees on any of the maps, I just call that index on the other maps and perform the same action. When you scroll east or west, you hit a volume that teleports you to the same position on the other side of the map. This allows you to scroll endlessly in any direction as if you were on a globe.
This wrap method has issues that I intend to fix someday. All meshes are multiplied times 3 and are always rendering. This is noticeable on maps with 10K+ tiles. Additionally, there is a slight motion blur during teleportation, but I am told that can be fixed in the post process.
Ideally, I work out a method where I can stream a single shadow map ahead of you in whatever direction you travel. Or in some cases, I may need to stream 2 of them if you zoom out far enough. There is also the case where a player clicks the minimap to snap to a location across the world which could require a 3rd map to be streamed in if it can’t appear quickly enough. These are problems for another day though. Also need the devs to add a few features to the streaming system/world browser to allow more control from blueprint anyway.
This is how the tiles are rendered and interacted with:
Each visible tile is an InstancedStaticMeshComponent. I add a mesh to the world, add that mesh to an instance variable, and then use that variable to spawn the instances in the world. Instanced rendering is key to stable performance with this many meshes spawned.
Anytime I change an instance, such as cutting down a forest, each other instance in the map is deleted and respawned. This has been seamless even with tens of thousands of instances on the map.
As you can see in the second of those 2 images, I use Enums to drive what tile is spawned at any given location. Each tile has information about sea level, precipitation, temperature, etc etc. Depending on the combination of that information, different tile types will spawn. For instance, a tile that is Land, Plains, Tropical, Wet, and a Lowland will spawn a Jungle over a Marsh. A tile that is Land, Hill, Tundra, Moderate, and Lowland will spawn a Boreal Foreston a Hill. When spawning the instances, it merely goes through a series of switches until it gets to the correct Add Instance node.
You can’t interact with that instance though, so each tile has a set of invisible volumes spawned on top that I hit with line traces to pull that tile’s index and then perform whatever actions I want on it.
As you can see in this picture, the red lines representing my tectonic boundaries are my volumes made visible and colored red. The volumes double as a great visual debugging tool.
So that is most of the general information. I will use separate posts below to go through how each of the systems generates the information that fills each terrain array.