Welcome, Guest. Please login or register.
Did you miss your activation email?

Login with username, password and session length

Advanced search

1404748 Posts in 68420 Topics- by 62078 Members - Latest Member: Megafon

February 06, 2023, 02:50:09 PM

Need hosting? Check out Digital Ocean
(more details in this thread)
TIGSource ForumsCommunityDevLogsÉclaircies - Experimental running and racing sim set in a generated forest
Pages: [1]
Author Topic: Éclaircies - Experimental running and racing sim set in a generated forest  (Read 1276 times)
Level 0

View Profile WWW
« on: May 17, 2020, 04:52:40 AM »

Hi everyone,

I'm Simon, a French game developer from France making an experimental first person racing game set inside a generated forest. It's called Éclaircies.

I started working on this game during the 7 day FPS and ProcJam at the end of 2018. I spent a good part of 2019 working on other projects and could not spend as much time as I wanted on this game. Until fairly recently I was not even sure what it was truly about. Anyway, I'm starting this devlog a bit late I guess but I have some cool things to share I think and many more things to do. I hope to finish it by the end of the year.

The recordings and overall sound design is the work of Marie Muller but everything else is being made by myself using Unity and Blender. The devlog will thus cover all the technical aspects of the game, from rendering to procedural generation and 3D modeling as well as audio integration.

The three main features of the game worth mentioning right away are:

The forest generation

Generating the forest is probably the most time consuming task as well as the most challenging. So I guess this will be a big part of this devlog. I wanted a big forest that could not be generated beforehand (it's currently taking a radius of 3000 meters but could be bigger in the end) and that featured different areas of varying densities and tree kinds.

The alternative first person controller

Éclaircies is actually a kind of walking and running simulation that offers a different way to move. Instead of the classic mouse and keyboard inputs it uses only two keys: one for going left and another to go right. The goal was to try and enhance the feeling of movement and the degree to which it can be played with. I felt that the traditional control scheme emphasized looking rather than moving and Éclaircies was really not about looking at all.

The experimental racing gameplay

Finally, and this is the part that wasn't clear to me at first, this game is actually a racing game. Might feel weird to say this but even though I was making a running simulation I had no idea it would end up being a racing game Smiley. Éclaircies's gameplay revolves around the idea of a rogue-lite, race against the clock kind of approach that makes it pretty unique I guess because players first have to create the path they will have to take to leave the forest.


The game's website: https://www.simonchauvin.com/eclaircies/
My twitter, where I try to post regularly about the game: https://twitter.com/SimonChauvin

And that's it for now! The next blog posts should be about the forest generation.

Thank you for reading Smiley
« Last Edit: January 07, 2022, 01:20:15 AM by Chauchau » Logged

Level 0

View Profile WWW
« Reply #1 on: June 08, 2020, 07:34:09 AM »

Hi everyone!

Since I'm about to finish overhauling and optimizing the forest generation algorithm I thought that this would be a good starting point for this devlog.

Éclaircies is a first person game that allows players to freely roam a vast forest. I am a big fan of procedural generation in general and never really considered creating the forest by hand or by using terrain painting tools. I usually prefer programming than placing props around so I started, very early on, to build an intricate system to make this vast forest a reality. I also felt that this would be a better fit to my overall intentions. I wanted players to connect with the environment and make it progressively very familiar and unique while allowing them to come back to the game from time to time and experience a new forest.

Streaming system

The forest being quite vast, the time it took to generate it entirely on start was beyond reasonable. Additionally, it would have been a waste of time because most of the forest would never have to be generated in the first place since it would take forever to explore it and was not something the game would push for.

Thus I chose to build a kind of streaming system to allow tiny bits of forests to be generated in real time depending on where the player went. The whole area was divided into as many 50 meters wide chunks were necessary to fill it. Right now it's 6000 meters wide and made of a total of 14 400 chunks. The size of 50 meters came about mostly because it felt like a nice enough size for a forest chunk. Each chunk being generated independently from others it had to be sufficiently big to be a distinct area of its own without taking too long to be generated.

Forest layers

Each chunk is only generated if the player is at a specified distance and it led me to create a custom system of LOD to generate certain kind of trees before others because they could be seen from farther away.

Visibility of layers, from emergent (yellow) to floor (green).

As such, each chunk is divided into 4 layers. From my research on forests I found out that they tend to feature different classes of trees and shrubs depending on their height and overall sunlight access. Emergent layers, which are most often present in rainforests, regroup all the biggest and tallest trees. Even though the forest I aim for is a more humble European forest the emergent layer is still a good addition to divide the generation burden and to create a sense of perspective with tall trees in the background, which is something I happen to like in the forests I live close by. Below the emergent layer we find the canopy layer that actually covers most of the lower understory and floor layers.

Each tree belongs to a layer that determines its height (yellow for emergent, blue for canopy and purple for understory).

This division means that every chunk goes through a 5 steps generation. First, the ground itself is generated, it's made of a texture and the mesh it will be applied on. Then are generated all the emergent trees, the canopy trees and the understory trees. The last step is often the most time consuming and is responsible for generating the many shrubs, logs and plants that lay on the forest floor. Each generation step is using a custom Fast Poisson Disc sampling algorithm that I should cover in the next blog post.

Chunks gets generated then filled as the player gets nearer.

Nothing very fancy but a quite satisfying piece of code that makes use of a fundamental law of the architectural organization of forests!
« Last Edit: January 07, 2022, 01:20:47 AM by Chauchau » Logged

Level 0

View Profile WWW
« Reply #2 on: July 18, 2020, 02:57:07 AM »

Hi everyone!

It's been a while I think Smiley.

To conclude on my previous post and before I go on to talk about the generation of the forest itself I wanted to dig a little bit into the approach used to generate the ground.

As presented in the previous post, the whole forest is partitioned into squared chunks. This allows me to generate only the nodes that surround the player. The first step is to generate the ground itself as well as its texture. I took the lazy approach and used a simple fragment shader to generate a heightmap and a diffuse map whereas I could have dug into compute shaders for this I think. But I was interested in a low resolution kind of look and was not too worried about the performance. The mesh is made of 600 vertices and the diffuse has a width of 128. Once the heightmap is generated I then read every pixels and apply their values to a simple grid mesh's vertices. The diffuse itself is simply attached to the object's material.

Turns out, generating many chunks per seconds could be a problem at times so I ended up relying on a cool feature called AsyncGPUReadback. It is used to delay the reading of a render texture and thus avoids stalling the pipeline. After blitting my fragment shader to the render texture I then create a readback request that ends up being processed a few frames later when it's ready. It's a bit more involved than the use of ReadPixels because of the NativeArray data returned but it works flawlessly and is a perfect way to transfer data from the GPU to the CPU without wasting precious milliseconds. A good thing to know though is that, as the documentation says, the result is only available the frame it is received so that it is necessary to go through the whole queue of requests and process all the done ones. Without it I had random fails because I accessed the request one frame too late.

From above it is easy to see the many chunks that make up the ground.

While the heightmap is pretty straightforward and use a single Perlin noise, the diffuse map was a bit more tricky to generate. As can be seen on the screenshot above, each chunk corresponds to a specific kind of forest area. For instance, sparse areas have fewer trees and feature more grass on the ground. To achieve some level of diversity I chose to assign a different Perlin noise and probability to each kind of ground texture. Those can be leaves, grass or dirt for instance. Some areas are covered in moss whereas others like the sparse areas are made of big patches of grass. I also included a modifier that allows me to change the heightmap and make certain areas more hilly for instance.

The settings used on one of the areas (dirt is those yellowish patches).

Each forest area is configured by a scriptable object in which it is possible to select the ground kinds, their probabilities as well as their noise parameters. All the settings related to the generation of the forest are then stored in a collection of interrelated scriptable objects that tend to take the shape of a database in the end.

The configuration of the the forest through scriptable objects.
« Last Edit: January 07, 2022, 01:21:07 AM by Chauchau » Logged

Level 0

View Profile WWW
« Reply #3 on: August 23, 2020, 06:46:21 AM »

Hi everyone,

It's finally time to deal with the generation of the forest layers themselves. This was a big issue throughout the development because generating a dense chunk full of trees can take dozens of milliseconds if not properly done. And to avoid having trees pop in front of the player I had to make sure that many chunks were generated in advance.

Naive approach

A chunk is a 50 meters wide square of ground and typically features 60 floor elements, 120 understory trees, 15 canopy trees and a few emergent trees. Some special chunks can even contain up to 350 floor elements or 250 understory trees. The major issue was to prevent too much penetration between the crowns of the trees while allowing smaller trees below the bigger ones.

My first attempt at this was naive and used a dart throwing method where I would just try to spawn a tree somewhere on the chunk and process all the existing trees to check if the constraints are respected (height of the new tree is below the crown of the overlapping tree). This was innefficient, but I backed it up with an array of structs that allowed me to save the state of every possible positions on a chunk. Each time a new tree is created I update the  state of the positions covered by the trunk and crown to make sure nothing spawn on the trunk's position and only trees smaller to the crown base height are possible. Then, when generating a new tree I just check the state of the selected position to know if this particular tree can be spawn here.

Each position's state (yellow means anything can be spawned, blue forbids emergent trees, pink forbids emergent and canopy and the green color only allows for floor elements. White is unavailable.).

The switch to Fast Poisson Disc sampling

This approach worked surprisingly well with chunks moderately dense (less than 5ms to generate a 100 tree layer). But when it came to spawn a very dense layer (like 250 understory trees) it could take up to 20ms. Doing a single layer per frame was ok but it was still too much. In the meantime I came accross a popular algorithm called Fast Poisson Disc Sampling which, as the name implies, is a fast way to sample points in a circle.

I still had to adapt it to my specific constraints such as the fact that the points sampled (the trees) all have different radii. But overall, the switch to the new method was pretty easy. The logic behind it is very simple. When generating a new tree I select an existing tree and try to spawn around it at a distance that is greater than the sum of the radii of the existing tree and the tree to be spawned. If it can't be done I remove the position from the list but if it is in fact possible I then add this new position to the list. The algorithm ends when the list of possible positions is empty. This methods allows the generation of 250 trees in less than 5ms. The only issue with this is that it is less convincing when generating low density areas as it tends to create clusters of trees instead of diversly dense areas. By adding an additional spread radius parameter and predetermined starting positions I managed to make it look more random.

Overall statistics about the current generated forest (mean[min, max]).

In order to compare both methods I had to gather data about each chunk generated. This ended up as an inspector addon to visualize all the information relative to the generated forest. It tells me how many areas of each kind has been generated as well as the number of trees (elements) were spawn for each layer and the time it took. I'm pretty happy about this but I realize this is the kind of information I should have gathered as soon as I started building my generator. It's impossible to have a good overview of such a huge and rich environment without relying on objective data.
« Last Edit: January 07, 2022, 01:21:23 AM by Chauchau » Logged

Level 0

View Profile WWW
« Reply #4 on: October 11, 2020, 04:57:49 AM »

Hi everyone,

I talked a bit about scriptable objects throughout the devlog but I never really presented my approach. I started using them mainly because I needed to compartmentalize all the settings used for the generation of the forest. I also needed a quick way to create variations of the same set of parameters. Indeed, to handle the different areas that the forest is made of I wanted a way to simply create a new Area file and just change a set of predefined parameters. But I also needed a lot more settings to describe all the trees, rocks and shrubs that fill the different layers of the forest. Without them I'm not sure how I would have done it. They are great to store parameters that you would need in multiple places. Such as for instance the settings describing a layer of trees that was present in more than one area of the forest. Although, to be perfectly honest, I'm not sure my approach was very good because in the end it's kind of a mess :D

First things first, the top level, master of all scriptable objects, contains the settings related to the size and overall organization of the forest. It thus contains all the areas the forest can bear as well as the various kinds of ground surfaces that are available. As can be seen in the screenshot below I also use it to change the distribution of areas and make some areas more or less probable.

The point of entry into a deep hierarchy of scriptable objects containing scriptable objects containing parameters containing scriptables objects and so on.

Each area kind is actually a scriptable object that allows me to specify which layers an area should feature as well as the characteristics of its ground. For instance timber areas do not feature any emergent trees whereas sparse areas feature a very shallow understory layer.

The settings of an area mainly leading to other scriptable objects.

And again, thanks to another set of scriptable objects, I can describe the content of each layer and select the forest elements (trees, rocks, bushes, etc.) it should feature. For instance the floor layer of dense areas is full of bushes while most floor layers are usually more sparse or contain a very specific kind of forest element such as rocks or ferns.

The settings of a given layer can be used to specify the distribution of forest elements.

Of course, the ground settings themselves are stored in another scriptable object. They allow me for instance to specify the amount of leaves or sand that will be visible on the ground or even whether I want some variations of the terrain itself. And a lot of areas share the same ground settings which make these little files very handy.

The settings of a given ground will affect the texture and the terrain of the area.

Finally (not really), each forest element is described through a set of settings stored in yet another scriptable object. It might seem like a nightmare but you get used to it, I promise. Anyway, each kind of forest element is a scriptable object containing settings such as its range of possible sizes, its effect on the player or in relation to the wind. And depending on the needs of this specific kind it can also require some additional settings. For instance trees require another scriptable object to set their range of possible heights for instance.

The settings of a given kind of forest element and its specific settings (here those of a tree).

But this is not all. I also needed to be able to describe different kinds of trees, some large and tall, others large and round, some with a different set of possible crowns than others. So what did I do?

I added some more scriptable objects!

Basically, all forest elements are described by a scriptable object that contain at least another scriptable object to describe potential variations (sub kinds) of this specific kind but it can also contain a scriptable object that contain specific settings to this kind and its variations. Each sub kind is also represented through a scriptable object that is used to specify the actual object (the prefab) that will be selected for spawning. And again, it is also possible to add another scriptable object to describe specific settings to this sub kind that no other sub kinds has.

The end to this nightmare: the subkind and its specific settings.

This is a lot of parameters stored in a lot of files but this is the best method I've found to handle a collection of parameters that needs to be linked here and there. One of the good thing about these is that I can also use them as Enums. For instance, each kind of forest element (tree, boulder, bush, etc.) is represented by a specific scriptable object that is then passed around and checked against when needed to know the kind of a given object.

The only real issue I had in fact with this scriptable object approach is the retrieval of metadata. Calculating for instance the average radius of the forest elements of a specific area is kind of a pain. With a simple database this would have been very easy through a simple selection query. Instead with scriptable object I need to loop through lists, check types, access another list, loop through it, etc.
« Last Edit: January 07, 2022, 01:21:53 AM by Chauchau » Logged

Pages: [1]
Jump to:  

Theme orange-lt created by panic