Procedural 2D Terrain - Part 2: Creating Chunks

In my last post I wrote about how I set up rendering of chunk data in the TerrainComponent class of my procedural terrain experiment. Once I had this working it was time to start creating chunks on the fly, and then caching the data to be used when a chunk was revisited.

Resource Management
To handle the generation of chunk data the TerrainComponent is put in charge of ownership of the Chunk class instances, as well as any resources they require. When creating and destroying objects frequently memory fragmentation can be a concern, and repeatedly loading / saving assets from storage can also bottleneck performance. Chunks are allocated on the heap using std::unique_ptr, so to address the first issue xygine (my game framework within which I'm currently experimenting) provides an ObjectPool class, designed to manage allocated memory and efficiently recycle it, preventing any fragmentation. Depending on where the player is in the world up to 4 chunks are visible at one time, and generally 9 chunks are active at any time (the current chunk containing the player and 8 surrounding chunks) so a fixed allotment of memory can be used, from which the TerrainComponent can draw upon as it creates new chunks. When chunks are destroyed the memory is marked as free and automatically reallocated by the ObjectPool.
    Chunks also require at least one shader when being drawn (other shaders may be used for effects like water) and continually creating / destroying shaders is time consuming. The TerrainComponent has a single shader as a member, compiled on start up, a reference to which is passed to each new chunk as it is created. As outlined in part 1 each chunk requires a specially formatted texture which is used as a lookup table and, again, to keep creating and destroying these would be both time consuming and quite possibly cause memory fragmentation. The TerrainComponent therefore manages a pool of textures so that they may be reused rather than recreated. This is done by creating a ChunkTexture alias

using ChunkTexture = std::pair<sf::Texture, bool>;

and creating a vector of ChunkTexture. Textures are paired with a bool flag which marks whether or not the texture is currently in use by a chunk. The vector has to be initialised in the TerrainComponent constructor so that the textures are in the correct format and the flags have an initial value of false.

for (auto& tp : m_texturePool)
{
    tp.first.create(64, 64);
   glBindTexture(GL_TEXTURE_2D, tp.first.getNativeHandle());
    glTexImage2D(GL_TEXTURE_2D, 0, GL_R16UI, tp.first.getSize().x, tp.first.getSize().y, 0, GL_RED_INTEGER, GL_UNSIGNED_SHORT, 0);
    glBindTexture(GL_TEXTURE_2D, 0);
    tp.second = false; //texture not yet used
}


As m_texturePool is a std::vector a utility function can quickly find the first unused texture:

ChunkTexture& TerrainComponent::getTexture()
{
    return *std::find_if(std::begin(m_texturePool), std::end(m_texturePool),
        [](const ChunkTexture& ct)
    {
        return !ct.second;
    });
}


A reference to a free texture is then passed to a Chunk, along with the shader, when it is created (I'm aware of the assumption that the above function will always return a free texture - this is because the ObjectPool for the Chunk instances is a fixed size, which is the same size as the texture pool. If, at any point, I'm trying to create more chunks than I have free resources then I've got a bigger problem on my hands...). This takes care of the resource handling, but the TerrainComponent has a second job: to monitor the player's position in the world, and update the corresponding world chunks.

Creating Chunks On The Fly
Two things needed to be done to the Chunk class then. Firstly a chunk needs to have some positional data. It needs a position in world space, and a size. This is done by providing an sf::FloatRect which represents the global bounds of the chunk. The bounding rectangle can be used to test if it contains the player position, and compare it to the position from the last frame. If the current chunk differs from the previous chunk then the player has moved from one chunk to another, triggering a chunk update. Each active chunk is tested, via its global bounds, to see if it contains one of 8 points surrounding the current chunk (which contains the player). If the test fails the chunk is removed, the ObjectPool automatically freeing the memory, and the destructor of the chunk marks its texture as being free. The surrounding points are again tested to see which chunks do not yet exist, and any missing chunks are created in the active chunks list.
    Secondly a chunk has to be able to update its texture with an array of tile data. This is done by giving each chunk a unique ID based on a hash of its 2D position. When a chunk is created it looks to local storage for a file with the same name as its ID. If it is found the file is loaded and the contents copied to the chunk array. As the data is only 8kb in size I've found that this loads fast enough to not visibly block the execution of the program - although I have yet to implement any error checking. If a matching file is not found, however, then the chunk must be generated from scratch using the noise library. This, I have found, did cause some visual stutter, so the chunk creation function is called in its own thread. Some care is required for syncronisation, but an atomic bool flag is enough to tell the chunk when the data is ready to be uploaded to its texture. The chunk's position is needed here too as the noise function which generates terrain data does so based on a coordinate system. Once new chunk data has been created it is saved immediately to a file, so it can be reloaded next time the chunk is visited.

The Result
By this point I had a smooth, seemingly endless terrain generating on screen, with nice, tightly managed resources. I was able to leave a weight on the arrow keys of my keyboard for about 20 minutes so the 'player' kept on walking in a single direction, with absolutely no problems at all (don't worry the video isn't 20 minutes long...).



Now that the chunk system was working it was time to start working on generating biome data. Biomes, as I am sure many people are familiar with, describe the differences in terrain, such as forest areas, deserts or mountains. Using this post as a guide I set about generating further noise maps when a chunk was created for the first time, which create data that can be used to look up a biome ID from a static table of data. This ID is then OR'd into the data array value at that coordinate. This will eventually be used to select the correct texture for that biome, although currently I feel the noise generation part could do with some tweaking. In the above video the middle view of the minimaps on the right displays the different biomes, with which I am currently not personally happy. Some fine tuning will be required, before moving on to texturing...

Part 1
Part 3

Comments

Popular Posts