Programing & Design
This project is a framework for procedurally generating a hexagonal tile-based map. It is designed to be used for 4X games such as Civalization & Humankind. The map is generated based on a seed system using Perlin Noise giving the ability to generate pseudo-random maps. The map utilises Perlin Noise to generate its features including, biomes, tiles, structures, rivers and map height. The framework also provides a world wrap system, where the player is able to go off the edge of the screen and the wrap around to the other side of the map, giving the illusion of the map being round.
2021 - 2022
This trailer for the framework demonstrates a all of the main in feature in action which include; the hexagonal grid, biomes, evenviromental structures, height varation, world wrap system, Perlin Worms river generation, weather system, day / night cycle, framework UI to control all aspects of the procedual generation and systems.
Ther grid generation is created through a Grid Manager, that when initalized will create a grid of chunks, each chunk is responsable for creating a specified amount of tiles. Once all the chunks have been collected this collectivly makes up the whole grided map. However, before the grid is generated, the Grid Manager createds two noise maps that use the settings that can be inputed by the user. These noise maps allow for a pseudo-random pattern of float values between 0-1. The values that are sampled from the perlin noise can be used to change and influence the type of tile that is placed.
The world map uses biomes to differentiate between different climates, it gives the landscape a more interesting and realistic style. Within the framework biomes are a collection of different tiles. Each biome has a noise value that is used when a chunk is finding the biome a tile belongs to. It does this by getting the tiles grid coordinate and entering that into the noise map, which returns the noise value (float 0–1). The chunk then finds the biome that has the closest noise value to the number just gotten from the noise map. Once the correct biome has been found the chunk finds the type of tile from the list of tiles with the chosen biome. To get the correct tile the chunk uses the same method but instead of using the biome noise map uses a tile noise map.
Structures are chosen after a tile has been spawned, firstly tiles are testing to check if a structure can be spawned on them. Once a structure can be spawned on a tile, like how a tile is found using the noise value, the same applies to the structures, except they do not have their own noise map and share the same noise map as the tiles. The structure that has the closest noise value to the maps noise value is then returned. Once a structure is picked the structure decided if it should be place or not based on a percentage chance of being spawned. If the structure can be spawned, its then place onto the tile. There are two different types of structures within the framework, single structures and multi structures. However, they both inherit from the same base structure. This means then when the function to place them is called the structure itself will find if it needs to place only one of itself in the centre of the tile or if it is a multi-structure it may need to place itself multiple times and maybe place different variations of itself.
The River Manager is responsable for creating the rivers throughout the map, Firstly, it needs to get all the points on the map where rivers can begin and where rivers can end, these are referred to as the minima and maxima. The maxima relate to the starting points of rivers and the minima refers to the end points of rivers. The river manager will then loop through all the maxima points, for each iteration the manager checks the percentage chance of rivers being spawned if the randomly generated value is more than the percentage chance of a river spawning, then the river is not spawned, and the loop continues. Once the randomly generated number is less than the percentage chance of a river spawning the closest, minima value two that maximum value is found, now we have a maxima value for the start off the river and a minima value for the end point of the river.Now a river can be created, and the minima and maxima points inputted to the river. The river itself is responsible for generating its path through the landscape, placing tiles adjacent to each other along the path and making sure each play style is aligned. Firstly, the river generates its path using Perlin Worms, this is like getting a random direction however there is more control as the user can change a weighting value that will determine how much the Perlin Worm influences the direction it goes. The next stage is to place the river tiles so that they follow the direction of the river.
The world is separated into chunks so that the world wrap can function. The player has a chunk rendering component and grid transform on it. The grid transform component handles the grid and chunk position the player is on. The chunk rendering component subscribes to an event on the grid transform that is called each time the player moves to a different chunk coordinate. When this event is triggered the subscribed function in the chunk render is called, this function manages the positions of the chunks based off the players current chunk coordinate. The chunk renderer has a rendering distance which is the number of chunks that should be activated from the player. For example, a render distance of three will render 3 chunks in each direction so the total number of chunks rendered would be 12 plus the chunk the player is currently on equalling 13 chunks loaded. Any other chunk that is not within the chunks render distance is deactivated, this will still exist in the world but will not be rendered.However, when the player reaches a chunk coordinate near the edges of the map, the chunk renderer will need to reposition some of the chunks. When moving a chunk, it either needs to move to the left or right side of the map the chunk then moves an entire maps length to the left or right. This is calculated by taking the grid X dimension (number of chunks in world) and multiplying it by the chunk tile dimension (number of tiles in chunk) multiplied again by the tile X dimension (The tiles X dimension). The resulting world position is then easy to work out as we use the same function to calculate the world position of the chunk as used at the initialisation stage. The only difference is this time the input grid coordinate will be different giving us the world position it should be at.
Later on in the development of my project I found an article by Catlike Coding. This article demonstrates a similar object to my final year project. I then begun to investigate how Catlike Coding had implemented similar features to mine. This allowed me to reflex and review my own work, I found that each tile for the grid itself was acctually created in the game engine rather than using static meshes for grid tiles like I had used. This allowed much more control when manipulating the tiles for features such as generating rivers, the tiles also merge togehter insted of having sharp edges like my project. In the image we can see how the tiles themselfs are generated and have diffrent height variation, colours / biomes, rivers cut through the tiled landscape. As a result, I would have liked to have taken this approch to genrating the grid if I was to complete this project again.